Files
cc2-spoolman-bridge/app/newspool.py
T
Jonas MenzelandClaude Opus 5.5 64ea91d107
CI / test (push) Successful in 2m29s
CI / image (push) Successful in 39s
Spool page for NFC links, bridge address; vendor-first new spool form
- /s/<id> shows a spool: rest in Spoolman and CC2 Control, where it is,
  last booking, history, links to Spoolman and the tag generator.
- Setting "Adresse der Bridge"; the tag generator can put <address>/s/<id>
  on the tag, so an iPhone held to the roll opens that page.
- New spool: vendor and material chosen from Spoolman, the vendor's
  filaments offered, values taken from similar filaments, duplicates
  pointed out and never created twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GcEk5tkfFSYD6NTF66NkC
2026-10-25 21:52:23 +01:00

144 lines
6.3 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"]
name = p["filament"]["name"].lower()
fil = next((f for f in client.filaments() if (f.get("vendor") or {}).get("id") == v["id"]
and (f.get("name") or "").strip().lower() == name), None)
if fil is None: # the same vendor and name again would only clutter Spoolman
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)