Multiple printers, printer/slot in Spoolman, kept in sync
CI / test (push) Successful in 1m6s
CI / image (push) Successful in 43s

- One worker per printer (name, IP, CC2 Control URL, own pinned host key);
  a spool sits in at most one slot of one printer, moves between printers
- Spoolman: location "<printer> Slot N" plus extra fields "drucker"/"slot"
  (created by the bridge), cleared and "Lager" on removal
- Full comparison every minute: the printer wins over manual edits of loaded
  spools; stale printer locations go to storage; own locations untouched
- Automatic migration of v1 settings, state and host key
- Status hint when a tag is still in a tray but unmapped (e.g. after
  "Take out" in CC2 Control)
- SSH test: connection drops during exec give a clean error, not a 500
- Mock environment with several printers; tests for all of the above

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GcEk5tkfFSYD6NTF66NkC
This commit is contained in:
Jonas MenzelandClaude Opus 5.5 committed 2026-10-25 03:53:01 +01:00
1 parent bf32667f77
commit a9041eb538
15 files changed
+1400 -684

No files matched your search

+34 -5
View File
@@ -113,12 +113,13 @@ class FakeSpoolman:
self.calls: list[tuple] = []
self.fail = False
self.fail_use_after_apply = False
self.field_defs: dict[str, dict] = {}
def add(self, sid, vendor="SUNLU", material="PLA+", color="000000", remaining=800.0, lot=None, **extra):
self.db[sid] = {
"id": sid, "lot_nr": lot if lot is not None else f"{vendor.upper()}{sid:03d}",
"remaining_weight": remaining, "initial_weight": 1000.0, "spool_weight": 150.0, "used_weight": 0.0,
"location": "", "archived": False,
"location": "", "archived": False, "extra": {},
"filament": {"id": sid, "name": f"{material} {color}", "material": material, "color_hex": color,
"diameter": 1.75, "density": 1.24, "weight": 1000.0, "vendor": {"name": vendor}},
**extra,
@@ -139,11 +140,39 @@ class FakeSpoolman:
self._check()
return [copy.deepcopy(s) for s in self.db.values() if archived or not s["archived"]]
def set_location(self, sid, location):
def update_spool(self, sid, body):
self._check()
self.calls.append(("location", sid, location))
self.db[sid]["location"] = location
return self.db[sid]
if sid not in self.db:
raise SpoolmanNotFound("nicht gefunden")
sp = self.db[sid]
for key, value in (body.get("extra") or {}).items():
if value is not None and key not in self.field_defs:
raise SpoolmanError("Spoolman PATCH: HTTP 400")
self.calls.append(("location", sid, body.get("location")))
if "extra" in body:
self.calls.append(("extra", sid, dict(body["extra"])))
if "location" in body:
sp["location"] = body["location"]
for key, value in (body.get("extra") or {}).items():
if value is None:
sp["extra"].pop(key, None)
else:
sp["extra"][key] = value
return sp
def fields(self):
self._check()
return [{"key": k, **v} for k, v in self.field_defs.items()]
def add_field(self, key, params):
self._check()
self.calls.append(("field", key))
self.field_defs[key] = dict(params)
return self.fields()
def extra(self, sid):
import json
return {k: json.loads(v) for k, v in self.db[sid]["extra"].items()}
def use(self, sid, grams):
self._check()