#!/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: 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 --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 from report_md import report_md 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, "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 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"] 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, "voller_lkw_energie_kWh": nutzlast * e_kg, } 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 haushalte(q_speicher_tag: float, cfg: dict) -> dict: """Versorgbare Haushalte aus der taeglich erzeugten Speicherwaerme. Warmwasser ganzjaehrig (konstanter Bedarf) sowie der Winterfall (Raumheizung + Warmwasser), der weit mehr Waerme braucht. """ h = cfg["haushalt"] ww = h["erwachsene"] * h["ww_erwachsener_kWh_tag"] + h["kinder"] * h["ww_kind_kWh_tag"] heiz = h["heizbedarf_winter_kWh_tag"] winter = ww + heiz return { "ww_pro_haushalt_kWh_tag": ww, "heizbedarf_winter_kWh_tag": heiz, "winter_pro_haushalt_kWh_tag": winter, "haushalte_warmwasser": q_speicher_tag / ww if ww > 0 else 0.0, "haushalte_winter": q_speicher_tag / winter if winter > 0 else 0.0, } def brennstoffkosten(e: dict, cfg: dict) -> dict: """Vergleichswert der Waerme gegenueber fossilen Brennstoffen / Pellets. Nutzwaermekosten = Brennstoffpreis / Kesselwirkungsgrad. Bewertet werden der Energieinhalt eines vollen LKW und die Jahresmenge. """ energie_lkw = e["speicher"]["voller_lkw_energie_kWh"] energie_jahr = e["waermepumpe"]["speicher_kWh_tag"] * TAGE_PRO_JAHR vergleich = [] for b in cfg.get("brennstoffe", []): nw_ct = b["preis_ct_kWh"] / b["wirkungsgrad"] vergleich.append({ "name": b["name"], "preis_ct_kWh": b["preis_ct_kWh"], "wirkungsgrad": b["wirkungsgrad"], "nutzwaerme_ct_kWh": nw_ct, "eur_pro_lkw": energie_lkw * nw_ct / 100.0, "eur_pro_jahr": energie_jahr * nw_ct / 100.0, }) return { "energie_lkw_kWh": energie_lkw, "energie_jahr_kWh": energie_jahr, "vergleich": vergleich, } def container_invest_total(sp: dict, cfg: dict) -> dict: """Investitionssumme aller Container-Module aus der container_invest-Config.""" ci = cfg["container_invest"] masse = sp["masse_kg"] energie = sp["energie_pro_zyklus_kWh"] anzahl = ci["anzahl_module_dauerbetrieb"] pcm_eur = masse * ci["pcm_natriumacetat"]["preis_eur_pro_kg"] * ci["pcm_natriumacetat"]["reserve_faktor"] behaelter_eur = max(ci["behaelter"]["mindestpreis_eur"], masse * ci["behaelter"]["preis_eur_pro_kg_pcm"]) wt_eur = (ci["waermetauscher_ladung"]["preis_eur"] + ci["waermetauscher_entladung"]["preis_eur"]) hydraulik_eur = ci["hydraulik"]["preis_eur"] el = ci["elektronik"] elektronik_eur = (el["steuerung_eur"] + el["sensoren_pt100_anzahl"] * el["sensor_preis_eur"] + el["ventile_schaltschrank_eur"] + el["fernueberwachung_4g_eur"] + el["montage_verdrahtung_eur"]) montage_eur = ci["montage_inbetriebnahme_eur"] pro_modul = pcm_eur + behaelter_eur + wt_eur + hydraulik_eur + elektronik_eur + montage_eur gesamt = pro_modul * anzahl eur_pro_kwh = gesamt / (energie * anzahl) if energie > 0 else 0.0 return { "anzahl_module": anzahl, "pcm_eur": pcm_eur * anzahl, "behaelter_eur": behaelter_eur * anzahl, "wt_hydraulik_eur": (wt_eur + hydraulik_eur) * anzahl, "elektronik_eur": elektronik_eur * anzahl, "montage_eur": montage_eur * anzahl, "gesamt_eur": gesamt, "eur_pro_kwh_kapazitaet": eur_pro_kwh, } def anbieterkosten(e: dict, cfg: dict) -> dict: """Jaehrliche Betriebskosten des Anbieters -> Gestehungskosten und Marge. Abschreibung: linear ohne Kapitalzins (konservativ). Strombedarf: Netzbezug zum Netzpreis, PV-Eigenanteil zu LCOE-Gestehungskosten. """ a = cfg["anbieter"] inv = a["invest"] wp = e["waermepumpe"] sp = e["speicher"] pv_data = e["pv"] netz = e["netzbezug"] bat = e["batterie"] energie_jahr = wp["speicher_kWh_tag"] * TAGE_PRO_JAHR fuhren_jahr = TAGE_PRO_JAHR / cfg["abholung"]["zyklus_tage"] kosten_transport = fuhren_jahr * a["transport_eur_pro_fuhre"] netz_eur = netz["netzbezug_kWh"] * a["netzstrompreis_ct_kWh"] / 100.0 pv_kwh = netz["jahresbedarf_kWh"] - netz["netzbezug_kWh"] pv_eur = pv_kwh * a["pv_gestehung_ct_kWh"] / 100.0 kosten_strom = netz_eur + pv_eur kosten_wartung = a["wartung_eur_pro_jahr"] kapital_pv = pv_data["kWp"] * inv["pv_eur_pro_kWp"] / inv["pv_nutzungsdauer_jahre"] kapital_bat = bat["nominal_kWh"] * inv["batterie_eur_pro_kWh"] / inv["batterie_nutzungsdauer_jahre"] kapital_wp = inv["waermepumpe_eur"] / inv["waermepumpe_nutzungsdauer_jahre"] ci = container_invest_total(sp, cfg) kapital_speicher = ci["gesamt_eur"] / inv["speicher_nutzungsdauer_jahre"] kosten_kapital = kapital_pv + kapital_bat + kapital_wp + kapital_speicher jahreskosten = kosten_transport + kosten_strom + kosten_wartung + kosten_kapital gestehung_ct = jahreskosten / energie_jahr * 100.0 if energie_jahr > 0 else 0.0 marge = [] for v in e.get("brennstoffkosten", {}).get("vergleich", []): m_ct = v["nutzwaerme_ct_kWh"] - gestehung_ct marge.append({ "name": v["name"], "nutzwaerme_ct_kWh": v["nutzwaerme_ct_kWh"], "marge_ct_kWh": m_ct, "marge_eur_jahr": m_ct / 100.0 * energie_jahr, }) return { "fuhren_jahr": fuhren_jahr, "energie_jahr_kWh": energie_jahr, "kosten_transport_eur": kosten_transport, "kosten_strom_eur": kosten_strom, "kosten_strom_detail": { "netz_kWh": netz["netzbezug_kWh"], "netz_ct_kWh": a["netzstrompreis_ct_kWh"], "netz_eur": netz_eur, "pv_kWh": pv_kwh, "pv_ct_kWh": a["pv_gestehung_ct_kWh"], "pv_eur": pv_eur, }, "kosten_wartung_eur": kosten_wartung, "kapital_pv_eur": kapital_pv, "kapital_batterie_eur": kapital_bat, "kapital_waermepumpe_eur": kapital_wp, "kapital_speicher_eur": kapital_speicher, "kosten_kapital_eur": kosten_kapital, "jahreskosten_eur": jahreskosten, "gestehungskosten_ct_kWh": gestehung_ct, "container_invest": ci, "marge": marge, } 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) hh = haushalte(wp["speicher_kWh_tag"], cfg) e = { "eingang": {"milch_l_tag": milch_l_tag}, "milch_kuehllast_kWh_tag": q_milch, "waermepumpe": wp, "speicher": sp, "pv": pv, "netzbezug": netz, "batterie": bat, "haushalte": hh, } if "brennstoffe" in cfg: e["brennstoffkosten"] = brennstoffkosten(e, cfg) if "anbieter" in cfg and "container_invest" in cfg: e["anbieter"] = anbieterkosten(e, cfg) return e 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"] hh = e["haushalte"] 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(f" -> Energieinhalt eines vollen LKW: ~{sp['voller_lkw_energie_kWh']:.0f} kWh") L.append("") L.append(f"Versorgte Haushalte (2 Erw. + 2 Kinder):") L.append(f" Warmwasser ganzjaehrig ... ~{hh['haushalte_warmwasser']:.0f} Haushalte") L.append(f" im Winter (Heizung + WW) . ~{hh['haushalte_winter']:.1f} Haushalte") 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"] hh = e["haushalte"] 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("VERSORGTE HAUSHALTE ({} Erw. + {} Kinder)".format( cfg["haushalt"]["erwachsene"], cfg["haushalt"]["kinder"])) L.append(f" Warmwasser je Haushalt ...... {hh['ww_pro_haushalt_kWh_tag']:8.1f} kWh/Tag") L.append(f" -> Warmwasser ganzjaehrig ... {hh['haushalte_warmwasser']:8.1f} Haushalte") L.append(f" max. Heizbedarf Winter ...... {hh['heizbedarf_winter_kWh_tag']:8.1f} kWh/Tag/HH") L.append(f" Winterbedarf (Heiz+WW) ...... {hh['winter_pro_haushalt_kWh_tag']:8.1f} kWh/Tag/HH") L.append(f" -> im Winter ................ {hh['haushalte_winter']:8.1f} Haushalte") 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(f" Energieinhalt voller LKW .... {sp['voller_lkw_energie_kWh']:8.0f} kWh") bk = e.get("brennstoffkosten") if bk and bk["vergleich"]: L.append("") L.append("WERT DER WAERME BEIM KAEUFER (Ersatzkosten fossil/Holz)") L.append(f" {'Brennstoff':<10} | {'ct/kWh':>7} | {'EUR/LKW':>9} | {'EUR/Jahr':>9}") L.append(f" {'-'*10}-+-{'-'*7}-+-{'-'*9}-+-{'-'*9}") for v in bk["vergleich"]: L.append(f" {v['name']:<10} | {v['nutzwaerme_ct_kWh']:>7.1f} | " f"{v['eur_pro_lkw']:>9.0f} | {v['eur_pro_jahr']:>9.0f}") L.append(f" Bewertet: voller LKW ~{bk['energie_lkw_kWh']:.0f} kWh, " f"Jahreserzeugung ~{bk['energie_jahr_kWh']:.0f} kWh") ak = e.get("anbieter") if ak: inv = cfg["anbieter"]["invest"] ci = ak["container_invest"] sd = ak["kosten_strom_detail"] L.append("") L.append("WAERME-GESTEHUNGSKOSTEN ANBIETER (linear, kein Kapitalzins)") L.append(f" Transport ({ak['fuhren_jahr']:.0f} Fuhren/a) ........" f" {ak['kosten_transport_eur']:>8.0f} EUR/a") L.append(f" WP-Strom ({sd['netz_kWh']:.0f} kWh Netz" f" @ {sd['netz_ct_kWh']:.0f} ct + {sd['pv_kWh']:.0f} kWh PV" f" @ {sd['pv_ct_kWh']:.0f} ct) .. {ak['kosten_strom_eur']:>6.0f} EUR/a") L.append(f" Wartung .......................... {ak['kosten_wartung_eur']:>8.0f} EUR/a") L.append(f" Kapital PV ({pv['kWp']:.1f} kWp, {inv['pv_nutzungsdauer_jahre']} J) ......" f" {ak['kapital_pv_eur']:>8.0f} EUR/a") L.append(f" Kapital Batterie ({bat['nominal_kWh']:.0f} kWh, {inv['batterie_nutzungsdauer_jahre']} J) ." f" {ak['kapital_batterie_eur']:>8.0f} EUR/a") L.append(f" Kapital WP ({inv['waermepumpe_nutzungsdauer_jahre']} J) ............." f" {ak['kapital_waermepumpe_eur']:>8.0f} EUR/a") L.append(f" Kapital Speicher ({ci['anzahl_module']} Module, {inv['speicher_nutzungsdauer_jahre']} J)" f" {ak['kapital_speicher_eur']:>8.0f} EUR/a") L.append(f" {'-'*46}") L.append(f" Jahreskosten gesamt .............. {ak['jahreskosten_eur']:>8.0f} EUR/a") L.append(f" Jahreserzeugung .................. {ak['energie_jahr_kWh']:>8.0f} kWh/a") L.append(f" -> Mindestverkaufspreis .......... {ak['gestehungskosten_ct_kWh']:>8.1f} ct/kWh") L.append(f" Speicher-Invest ({ci['anzahl_module']} Module): {ci['gesamt_eur']:,.0f} EUR" f" = {ci['eur_pro_kwh_kapazitaet']:.0f} EUR/kWh Kapazitaet") L.append(f" PCM {ci['pcm_eur']:,.0f} | Behaelter {ci['behaelter_eur']:,.0f}" f" | WT+Hydr. {ci['wt_hydraulik_eur']:,.0f}" f" | Elektr. {ci['elektronik_eur']:,.0f} | Montage {ci['montage_eur']:,.0f}") if ak["marge"]: L.append("") L.append("MARGE GEGEN ERSATZBRENNSTOFF") L.append(f" {'Brennstoff':<10} | {'ct/kWh':>7} | {'Marge ct/kWh':>12} | {'Marge EUR/Jahr':>14}") L.append(f" {'-'*10}-+-{'-'*7}-+-{'-'*12}-+-{'-'*14}") for m in ak["marge"]: haertester = " <== haertester Massstab" if m == ak["marge"][-1] else "" L.append(f" {m['name']:<10} | {m['nutzwaerme_ct_kWh']:>7.1f} |" f" {m['marge_ct_kWh']:>+12.1f} |" f" {m['marge_eur_jahr']:>+14.0f}{haertester}") L.append("=" * 62) return "\n".join(L) # report_md wurde nach lib/report_md.py ausgelagert und oben importiert. 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=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: text = vorschlag_report(ergebnis, cfg) ergebnis["vorschlag"] = vorschlag(ergebnis, cfg) else: 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) if args.verbose: print(f"JSON geschrieben: {args.json}") if __name__ == "__main__": main()