- Master data of CC2 Control's copies follows Spoolman (name, brand, colour, diameter, density, net, tare, price; not the material, which was fitted to the tray). - A loaded spool's rest in CC2 Control is set to Spoolman's via /api/spools/adjust once everything is booked (old copy after the roll printed in the other printer, weigh-in in Spoolman); the resulting correction is not booked back. - Copies of spools archived or deleted in Spoolman are deleted from CC2 Control when no tray holds them (library limit 128); an outage never counts as deleted. - A roll that ran out is archived in Spoolman (setting, default on); a scan of an archived spool maps nothing. - A roll scanned again with unchanged tag data and put back into its old slot (after printing in the other printer) is recognised. - Tag generator: "Neue Spule" creates spools in Spoolman from an existing or a new filament (and vendor), labelled vendor + id, then opens the tag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GcEk5tkfFSYD6NTF66NkC
140 lines
6.0 KiB
Python
140 lines
6.0 KiB
Python
"""New spools in Spoolman from the bridge UI: an existing filament, or a new one (and its vendor)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from .spoolman import Spoolman, make_label
|
|
|
|
MAX_COUNT = 20
|
|
|
|
|
|
class NewSpoolError(ValueError):
|
|
pass
|
|
|
|
|
|
def _num(body: dict, key: str, lo: float, hi: float, required: bool = True, what: str = "") -> float | None:
|
|
raw = body.get(key)
|
|
if raw in (None, ""):
|
|
if required:
|
|
raise NewSpoolError(f"{what or key} fehlt")
|
|
return None
|
|
try:
|
|
v = float(raw)
|
|
except (TypeError, ValueError):
|
|
raise NewSpoolError(f"{what or key}: keine Zahl") from None
|
|
if not lo <= v <= hi:
|
|
raise NewSpoolError(f"{what or key} muss zwischen {lo:g} und {hi:g} liegen")
|
|
return v
|
|
|
|
|
|
def _text(body: dict, key: str, what: str, limit: int = 64) -> str:
|
|
v = re.sub(r"\s+", " ", str(body.get(key) or "")).strip()
|
|
if not v:
|
|
raise NewSpoolError(f"{what} fehlt")
|
|
if len(v) > limit:
|
|
raise NewSpoolError(f"{what} ist zu lang (höchstens {limit} Zeichen)")
|
|
return v
|
|
|
|
|
|
def catalog(client: Spoolman) -> dict:
|
|
"""Vendors and filaments for the form, and the id Spoolman will most likely give the next spool."""
|
|
vendors = sorted(client.vendors(), key=lambda v: v["name"].lower())
|
|
spools = client.spools(archived=True)
|
|
counts: dict[int, int] = {}
|
|
for sp in spools:
|
|
if not sp.get("archived"):
|
|
fid = (sp.get("filament") or {}).get("id")
|
|
counts[fid] = counts.get(fid, 0) + 1
|
|
filaments = []
|
|
for f in client.filaments():
|
|
if f.get("archived"):
|
|
continue
|
|
filaments.append({
|
|
"id": f["id"], "name": f.get("name") or "", "vendor": (f.get("vendor") or {}).get("name") or "",
|
|
"material": f.get("material") or "", "color": (f.get("color_hex") or "").upper()[:6],
|
|
"density": f.get("density"), "diameter": f.get("diameter"), "weight": f.get("weight"),
|
|
"spool_weight": f.get("spool_weight"), "extruder_temp": f.get("settings_extruder_temp"),
|
|
"bed_temp": f.get("settings_bed_temp"), "price": f.get("price"), "spools": counts.get(f["id"], 0),
|
|
})
|
|
filaments.sort(key=lambda f: (f["vendor"].lower(), f["name"].lower()))
|
|
next_id = max((int(sp["id"]) for sp in spools), default=0) + 1
|
|
return {"vendors": [{"id": v["id"], "name": v["name"], "empty_spool_weight": v.get("empty_spool_weight")}
|
|
for v in vendors],
|
|
"filaments": filaments, "next_id": next_id}
|
|
|
|
|
|
def plan(body: dict) -> dict:
|
|
"""Checks the form; returns what to create."""
|
|
count = int(_num(body, "count", 1, MAX_COUNT, what="Anzahl"))
|
|
out: dict = {"count": count,
|
|
"initial_weight": _num(body, "weight", 1, 20000, required=False, what="Nettogewicht"),
|
|
"spool_weight": _num(body, "spool_weight", 0, 5000, required=False, what="Leergewicht"),
|
|
"price": _num(body, "price", 0, 1e6, required=False, what="Preis")}
|
|
if body.get("filament_id") not in (None, ""):
|
|
out["filament_id"] = int(body["filament_id"])
|
|
return out
|
|
color = str(body.get("color") or "").strip().lstrip("#").upper()
|
|
if not re.fullmatch(r"[0-9A-F]{6}", color):
|
|
raise NewSpoolError("Farbe als RRGGBB angeben")
|
|
out["vendor"] = _text(body, "vendor", "Hersteller")
|
|
out["filament"] = {
|
|
"name": _text(body, "name", "Name", 96),
|
|
"material": _text(body, "material", "Material", 32),
|
|
"color_hex": color,
|
|
"density": _num(body, "density", 0.5, 3, what="Dichte"),
|
|
"diameter": _num(body, "diameter", 1, 3.5, what="Durchmesser"),
|
|
"weight": out["initial_weight"],
|
|
"spool_weight": out["spool_weight"],
|
|
"settings_extruder_temp": _int(_num(body, "extruder_temp", 150, 450, required=False, what="Düsentemperatur")),
|
|
"settings_bed_temp": _int(_num(body, "bed_temp", 0, 150, required=False, what="Betttemperatur")),
|
|
"price": out["price"],
|
|
}
|
|
return out
|
|
|
|
|
|
def _int(v: float | None) -> int | None:
|
|
return None if v is None else int(round(v))
|
|
|
|
|
|
def describe(p: dict, filament_name: str, vendor: str) -> str:
|
|
n = p["count"]
|
|
weight = f", {p['initial_weight']:g} g" if p.get("initial_weight") else ""
|
|
return f"{n} Spule{'n' if n > 1 else ''} {vendor} {filament_name}{weight}"
|
|
|
|
|
|
def create(client: Spoolman, p: dict, location: str) -> tuple[list[dict], str]:
|
|
"""Creates what `plan` returned. Returns the new spools (id, label) and what was created, for the log."""
|
|
created_parts = []
|
|
if "filament_id" in p:
|
|
fil = next((f for f in client.filaments() if f["id"] == p["filament_id"]), None)
|
|
if fil is None:
|
|
raise NewSpoolError("Filament gibt es in Spoolman nicht mehr")
|
|
vendor = (fil.get("vendor") or {}).get("name") or ""
|
|
else:
|
|
vendor = p["vendor"]
|
|
v = next((v for v in client.vendors() if v["name"].strip().lower() == vendor.lower()), None)
|
|
if v is None:
|
|
body = {"name": vendor}
|
|
if p["filament"].get("spool_weight") is not None:
|
|
body["empty_spool_weight"] = p["filament"]["spool_weight"]
|
|
v = client.create_vendor(body)
|
|
created_parts.append(f"Hersteller {vendor}")
|
|
vendor = v["name"]
|
|
fbody = {k: val for k, val in p["filament"].items() if val is not None}
|
|
fbody["vendor_id"] = v["id"]
|
|
fil = client.create_filament(fbody)
|
|
created_parts.append(f"Filament {fil.get('name')}")
|
|
spools = []
|
|
for _ in range(p["count"]):
|
|
body: dict = {"filament_id": fil["id"], "location": location}
|
|
for key in ("initial_weight", "spool_weight", "price"):
|
|
if p.get(key) is not None:
|
|
body[key] = p[key]
|
|
sp = client.create_spool(body)
|
|
lot = make_label(vendor, int(sp["id"]))
|
|
client.update_spool(int(sp["id"]), {"lot_nr": lot})
|
|
spools.append({"id": int(sp["id"]), "label": lot})
|
|
created_parts.insert(0, describe(p, fil.get("name") or "", vendor) + " (" + ", ".join(s["label"] for s in spools) + ")")
|
|
return spools, "; ".join(created_parts)
|