173 lines
5.7 KiB
Python
173 lines
5.7 KiB
Python
"""Ist-Gramm aus Moonraker-Filamentzählern und Spulenanteilen."""
|
||
|
||
from __future__ import annotations
|
||
|
||
|
||
def as_float(value, default=0.0):
|
||
if value is None or value is False:
|
||
return float(default)
|
||
if isinstance(value, bool):
|
||
return float(default)
|
||
if isinstance(value, (int, float)):
|
||
return float(value)
|
||
if isinstance(value, str):
|
||
try:
|
||
return float(value.replace(",", ".").strip())
|
||
except ValueError:
|
||
return float(default)
|
||
if isinstance(value, list):
|
||
return sum(as_float(item) for item in value)
|
||
return float(default)
|
||
|
||
|
||
def weight_list(metadata):
|
||
"""Gramm je Farbe aus filament_weight_total (Zahl oder Liste)."""
|
||
raw = (metadata or {}).get("filament_weight_total")
|
||
if isinstance(raw, list):
|
||
return [as_float(item) for item in raw]
|
||
total = as_float(raw)
|
||
return [total] if total > 0 else []
|
||
|
||
|
||
def weight_sum(metadata):
|
||
return sum(weight_list(metadata))
|
||
|
||
|
||
def filament_total_mm(metadata, job=None):
|
||
raw = (metadata or {}).get("filament_total")
|
||
if raw in (None, "", [], 0, 0.0) and isinstance(job, dict):
|
||
raw = job.get("filament_total")
|
||
if isinstance(raw, list):
|
||
return sum(as_float(item) for item in raw)
|
||
return as_float(raw)
|
||
|
||
|
||
def filament_used_mm(job):
|
||
if not isinstance(job, dict):
|
||
return 0.0
|
||
return as_float(job.get("filament_used"))
|
||
|
||
|
||
def planned_shares(metadata, ace_map, target_count):
|
||
"""Geplante Gramm je Ziel-Spule. Index entspricht targets-Reihenfolge."""
|
||
weights = weight_list(metadata)
|
||
mapping = ace_map if isinstance(ace_map, list) else []
|
||
if weights and mapping and len(weights) >= len(mapping) and target_count == len(mapping):
|
||
paints = []
|
||
for item in mapping:
|
||
try:
|
||
paints.append(int(item.get("paint_index")))
|
||
except (TypeError, ValueError):
|
||
paints.append(-1)
|
||
# Nur bei dichter 0…n−1-Map paint_index = Gewichtsindex; sonst Map-Reihenfolge
|
||
# (Bambu/Anycubic oft T1+T10 bei zwei kompakten Gewichten).
|
||
dense = sorted(paints) == list(range(len(paints))) and all(
|
||
0 <= paint < len(weights) for paint in paints
|
||
)
|
||
shares = []
|
||
for order, paint in enumerate(paints):
|
||
if dense:
|
||
shares.append(weights[paint])
|
||
elif order < len(weights):
|
||
shares.append(weights[order])
|
||
else:
|
||
shares.append(0.0)
|
||
if sum(shares) > 0:
|
||
return shares
|
||
if weights and target_count == len(weights):
|
||
return list(weights)
|
||
total = sum(weights) if weights else 0.0
|
||
if target_count <= 0:
|
||
return []
|
||
if total <= 0:
|
||
return [0.0] * target_count
|
||
each = total / target_count
|
||
return [each] * target_count
|
||
|
||
|
||
def resolve_used_grams(planned_g, used_mm, total_mm, progress, state, duration_s):
|
||
"""
|
||
Ist-Gramm und Herkunft.
|
||
measured = filament_used, estimate = Fortschritt oder Soll, none = nichts.
|
||
"""
|
||
planned = max(0.0, as_float(planned_g))
|
||
used = max(0.0, as_float(used_mm))
|
||
total = max(0.0, as_float(total_mm))
|
||
prog = max(0.0, min(1.0, as_float(progress)))
|
||
duration = max(0.0, as_float(duration_s))
|
||
terminal_abort = state in ("cancelled", "error", "interrupted")
|
||
|
||
if used > 0.5 and total > 0:
|
||
grams = planned * (used / total) if planned > 0 else 0.0
|
||
if grams <= 0 and used > 0.5:
|
||
# Länge ohne Gewicht: grob 1.24 g/cm³ · 1.75 mm → ~3 g/m
|
||
grams = used * 0.003
|
||
return max(0.0, grams), "measured"
|
||
|
||
if used > 0.5 and planned > 0 and total <= 0:
|
||
# Millimeter da, Gesamtlänge fehlt — Fortschritt oder Dauer helfen nicht
|
||
return used * 0.003, "estimate"
|
||
|
||
if terminal_abort and used <= 0.5:
|
||
if prog > 0.005 and planned > 0:
|
||
return planned * prog, "estimate"
|
||
return 0.0, "none"
|
||
|
||
if planned > 0:
|
||
if prog > 0.005 and prog < 0.995 and state != "completed":
|
||
return planned * prog, "estimate"
|
||
return planned, "estimate" if state == "completed" else "none"
|
||
|
||
return 0.0, "none"
|
||
|
||
|
||
def split_used(used_g, planned_shares_list, multi):
|
||
"""
|
||
Ist-Gramm auf Spulen verteilen.
|
||
multi=True → Anteile sind Schätzwerte, Summe bleibt used_g.
|
||
"""
|
||
used = max(0.0, as_float(used_g))
|
||
shares = [max(0.0, as_float(s)) for s in (planned_shares_list or [])]
|
||
if not shares:
|
||
return [], False
|
||
total_plan = sum(shares)
|
||
if total_plan <= 0:
|
||
each = used / len(shares)
|
||
parts = [each] * len(shares)
|
||
else:
|
||
parts = [used * (share / total_plan) for share in shares]
|
||
is_estimate = bool(multi) and len(shares) > 1
|
||
return parts, is_estimate
|
||
|
||
|
||
def segment_deltas(segments, final_mm):
|
||
"""
|
||
Abschnitte mit mm_start/mm_end → Delta-mm je (spool_id, ace_slot).
|
||
Offene Abschnitte schließen auf final_mm.
|
||
"""
|
||
end = max(0.0, as_float(final_mm))
|
||
totals = {}
|
||
order = []
|
||
for seg in segments or []:
|
||
start = max(0.0, as_float(seg.get("mm_start")))
|
||
raw_end = seg.get("mm_end")
|
||
stop = end if raw_end is None else max(0.0, as_float(raw_end))
|
||
delta = max(0.0, stop - start)
|
||
key = (seg.get("spool_id"), seg.get("ace_slot"))
|
||
if key not in totals:
|
||
totals[key] = 0.0
|
||
order.append(key)
|
||
totals[key] += delta
|
||
return [(key[0], key[1], totals[key]) for key in order]
|
||
|
||
|
||
def grams_from_mm(delta_mm, total_mm, planned_g):
|
||
delta = max(0.0, as_float(delta_mm))
|
||
total = max(0.0, as_float(total_mm))
|
||
planned = max(0.0, as_float(planned_g))
|
||
if delta <= 0:
|
||
return 0.0
|
||
if total > 0 and planned > 0:
|
||
return planned * (delta / total)
|
||
return delta * 0.003
|