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.
131 lines
4.4 KiB
Python
131 lines
4.4 KiB
Python
"""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"]
|