diff --git a/README.md b/README.md index 121258a..23d097b 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,10 @@ docker build -t cc2-spoolman-bridge:dev . # Image python tools/mock_env.py --host mock --printers 2 # Mock: 2 Drucker (SSH :2222/2223, CC2 :8081/8082), Spoolman ``` +Einen Test-Container der Bridge gegen den Mock nur in einem Netz ohne Außenverbindung starten +(`docker network create --internal …`) und mit `SPOOLMAN_URL=http://mock:7912`. Sonst spricht er bis zum Speichern +der Einstellungen mit der voreingestellten echten Spoolman-Instanz. + Die Tests decken ab: Tag-Kodierung gegen die verifizierte Vorlage, ID-Dekodierung, Palette und Materialvorschlag, Zuordnung bei verschwindender ID, Verbrauchs-Delta und Idempotenz (auch nach Abbruch), Dry-Run, mehrere Drucker (Umzug, Ausfall eines Druckers, Entfernen, Umbenennen), Spoolman-Ort und Zusatzfelder inkl. Korrektur von Hand- diff --git a/app/bridge.py b/app/bridge.py index a1f1586..34b27b5 100644 --- a/app/bridge.py +++ b/app/bridge.py @@ -948,6 +948,10 @@ class Bridge: ours.discard(self.settings.location_storage) return ours + def _our_printers(self) -> set[str]: + """Printer names this bridge runs or has written. Spoolman's "drucker" field naming anyone else is not ours.""" + return set(self.state.data.get("written_printers", [])) | {w.name for w in self.workers.values()} + def _ensure_fields(self) -> bool: if self.fields_ok or not self.settings.spoolman_fields: return True @@ -979,14 +983,15 @@ class Bridge: if self.clock() - self.last_sync >= SYNC_SECONDS: # Full comparison: the printer wins over manual edits in Spoolman. ours = self._our_locations() + our_printers = self._our_printers() for sp in self.spoolman.spools(): sid = int(sp["id"]) names[sid] = label(sp) actual = self._actual(sp) want = desired.get(sid) if want is None: - if actual["location"] not in ours and actual["printer"] is None: - # a place of yours (e.g. a shelf): not ours to touch + if actual["location"] not in ours and actual["printer"] not in our_printers: + # a place of yours (e.g. a shelf), or a printer this bridge never ran: not ours to touch targets.pop(sid, None) applied.pop(str(sid), None) continue @@ -1016,6 +1021,9 @@ class Bridge: if t["location"] != storage["location"]: written = set(self.state.data["written_locations"]) | {t["location"]} self.state.data["written_locations"] = sorted(written) + if t.get("printer"): + printers = set(self.state.data.get("written_printers", [])) | {t["printer"]} + self.state.data["written_printers"] = sorted(printers) except SpoolmanError as e: self.fields_ok = False self._spoolman_error(str(e)) diff --git a/app/storage.py b/app/storage.py index 2899bfd..9bdb59b 100644 --- a/app/storage.py +++ b/app/storage.py @@ -267,4 +267,6 @@ def migrate_state(data: dict, first_printer: str) -> dict: data["version"] = 2 data.setdefault("applied", {}) data.setdefault("written_locations", []) + if "written_printers" not in data: + data["written_printers"] = sorted({a["printer"] for a in data["applied"].values() if a.get("printer")}) return data diff --git a/tests/test_bridge.py b/tests/test_bridge.py index 106810f..3d30cac 100644 --- a/tests/test_bridge.py +++ b/tests/test_bridge.py @@ -417,6 +417,7 @@ def test_stale_printer_locations_go_to_storage(env): env["sm"].add(3, location="Lager", extra={"drucker": '"Links"', "slot": "1"}) env["sm"].add(4, location="Regal 1") env["bridge"].state.data["written_locations"] = ["CC2 Slot 2"] + env["bridge"].state.data["written_printers"] = ["Links"] # a printer this bridge used to run poll(env) assert env["sm"].db[1]["location"] == "Lager" assert env["sm"].db[2]["location"] == "Lager" @@ -773,3 +774,21 @@ def test_job_progress_only_read_while_printing(env): job = b.snapshot["printers"][0]["job"] assert job["file"] == "cube.gcode" and job["grams"] == 1.23 and job["slots"] == [{"slot": 1, "grams": 1.23}] assert job["progress"] == 40 and job["layer"] == 12 and job["layers"] == 30 and job["remaining"] == 600 + + +def test_fresh_bridge_leaves_other_bridges_spools_alone(env): + """A new instance (empty data volume, no printers yet) must not pull spools out of printers it never ran.""" + env["settings"].save({"printers": []}) + env["sm"].add(11, location="CC2 Links Slot 4", extra={"drucker": '"CC2 Links"', "slot": "4"}) + env["sm"].add(1, location="CC2 Rechts Slot 2", extra={"drucker": '"CC2 Rechts"', "slot": "2"}) + poll(env, 13) + assert env["sm"].db[11]["location"] == "CC2 Links Slot 4" and env["sm"].extra(11)["drucker"] == "CC2 Links" + assert env["sm"].db[1]["location"] == "CC2 Rechts Slot 2" + assert not [c for c in env["sm"].calls if c[0] in ("location", "extra")] + + +def test_printer_names_written_are_remembered(env): + env["sm"].add(5) + insert(env, 0, 5) + poll(env) + assert env["bridge"].state.data["written_printers"] == ["Rechts"]