Files
grotti/grotti/cost.py
T
Dominic Gundel 9e22cebb81 Grotti 2.10.0: Orca-Senden, Slice-Ordner, ED-Strom, Hygiene.
Slice-Watch mit Ordnern/Filtern, Orca-Nachbearbeitung, Cockpit-Verlauf
mit ED, Zeitzone im Browser. Tasten-Grenze und README-API angeglichen,
toter Code entfernt, Requirements gepinnt.
2026-10-18 00:41:15 +02:00

131 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Dieselbe Rechnung wie in der Druckkalkulation."""
from datetime import datetime
def calculate(job, material):
part_g = float(job.get("weight_g") or 0) * max(1, float(job.get("qty") or 1))
waste = float(job.get("waste_percent") or 0)
filament_g = part_g * (1 + waste / 100)
price = float(material.get("price_per_kg") or 0)
filament_cost = (filament_g / 1000) * price
util = min(100, max(1, float(job.get("utilization") or 1))) / 100
density = float(material.get("density") or 0) or 1
volumetric = float(material.get("volumetric") or 0)
cm3_per_hour = volumetric * 3.6 * util
hours = (filament_g / density) / cm3_per_hour if cm3_per_hour else 0
watts = float(job.get("watts") or 0)
euro_per_kwh = float(job.get("euro_per_kwh") or 0)
print_kwh = (watts / 1000) * hours
dry_kwh = (float(job.get("dryer_watts") or 0) / 1000) * float(job.get("dry_hours") or 0)
energy_cost = (print_kwh + dry_kwh) * euro_per_kwh
if job.get("price_mode") == "perKg":
revenue = (part_g / 1000) * float(job.get("price") or 0)
else:
revenue = float(job.get("price") or 0) * max(1, float(job.get("qty") or 1))
margin = revenue - filament_cost - energy_cost
per_hour = margin / hours if hours else 0
return {
"part_g": part_g,
"filament_g": filament_g,
"filament_cost": filament_cost,
"hours": hours,
"energy_cost": energy_cost,
"revenue": revenue,
"margin": margin,
"per_hour": per_hour,
"print_kwh": print_kwh,
"dry_kwh": dry_kwh,
}
def _parse_ts(value):
if value is None:
return None
if isinstance(value, datetime):
return value.timestamp()
text = str(value).strip()
if not text:
return None
try:
return datetime.fromisoformat(text.replace("Z", "+00:00")).timestamp()
except ValueError:
return None
def _avg_power(a, b):
vals = [float(v) for v in (a, b) if isinstance(v, (int, float)) and not isinstance(v, bool)]
if not vals:
return 0.0
return max(0.0, min(1.0, sum(vals) / len(vals)))
def _ed_print_kwh(samples, base_w, nozzle_w, bed_w):
"""Trapez aus Grundlast + ED×Heizerleistung. None wenn zu wenig ED-Proben."""
if not samples or len(samples) < 2:
return None
total = 0.0
ed_steps = 0
# Max. Intervall: 3× Normal-Sample (90 s), damit Lücken nicht die ED fortschreiben
max_dt = 90.0
for i in range(len(samples) - 1):
a, b = samples[i], samples[i + 1]
t0 = _parse_ts(a.get("sampled_at"))
t1 = _parse_ts(b.get("sampled_at"))
if t0 is None or t1 is None or t1 <= t0:
continue
dt = min(t1 - t0, max_dt)
pn0, pn1 = a.get("nozzle_power"), b.get("nozzle_power")
pb0, pb1 = a.get("bed_power"), b.get("bed_power")
has_ed = any(
isinstance(v, (int, float)) and not isinstance(v, bool)
for v in (pn0, pn1, pb0, pb1)
)
if has_ed:
ed_steps += 1
ed_n = _avg_power(pn0, pn1)
ed_b = _avg_power(pb0, pb1)
watts = float(base_w) + float(nozzle_w) * ed_n + float(bed_w) * ed_b
total += (watts / 1000.0) * (dt / 3600.0)
if ed_steps < 1:
return None
return total
def energy_estimate(duration_s, energy, samples=None):
"""Strom aus ED-Proben oder pauschal Dauer × Watt.
Quelle ``ed`` nur wenn Düse-/Bett-Watt gesetzt und Proben mit power vorliegen.
"""
energy = energy or {}
euro = float(energy.get("euro_per_kwh") or 0)
dry_kwh = (float(energy.get("dryer_watts") or 0) / 1000) * float(energy.get("dry_hours") or 0)
base_w = float(energy.get("watts") or 0)
nozzle_w = float(energy.get("nozzle_watts") or 0)
bed_w = float(energy.get("bed_watts") or 0)
print_kwh = None
source = "flat"
if (nozzle_w > 0 or bed_w > 0) and samples:
print_kwh = _ed_print_kwh(samples, base_w, nozzle_w, bed_w)
if print_kwh is not None:
source = "ed"
if print_kwh is None:
hours = float(duration_s or 0) / 3600
print_kwh = (base_w / 1000) * hours
source = "flat"
kwh = print_kwh + dry_kwh
return {
"kwh": kwh,
"print_kwh": print_kwh,
"dry_kwh": dry_kwh,
"cost": kwh * euro,
"source": source,
}
def energy_cost(duration_s, energy, samples=None):
return energy_estimate(duration_s, energy, samples)["cost"]