"""In-memory stand-ins for CC2 Control, Spoolman and the SSH record reader.""" from __future__ import annotations import copy import itertools from app.cc2control import CC2Error from app.spoolman import SpoolmanError, SpoolmanNotFound def channel(weight="", type_="PLA", detailed="PLA+", color="0x000000"): if weight == "": return {k: "" for k in ("brand", "code", "color", "date", "detailed_type", "diameter", "manufacturer", "nozzle_max_temp", "nozzle_min_temp", "type", "weight")} return {"brand": "0xEEEEEEEE", "code": "0x0001", "color": color, "date": "2000/00", "detailed_type": detailed, "diameter": "1.750000", "manufacturer": "ELEGOO", "nozzle_max_temp": "230", "nozzle_min_temp": "190", "type": type_, "weight": str(weight)} def record(*weights): rec = {"auto_plug_in_enable": 0, "color_table_size": 0, "extern_channel": {}} for n in range(4): w = weights[n] if n < len(weights) else "" rec[f"channels{n}"] = w if isinstance(w, dict) else channel(w) return rec class FakeReader: def __init__(self, rec=None): self.record = rec or record() self.fail = False def read(self): from app.sshsource import SSHError if self.fail: raise SSHError("Drucker nicht erreichbar") return copy.deepcopy(self.record) def close(self): pass class FakeCC2: def __init__(self, enabled=True): self.enabled = enabled self.slots = [{"slot": i, "spool": "", "last": "", "question": "", "printer": {"status": 0}} for i in range(5)] self.slots[4]["printer"] = None self.library: list[dict] = [] self.job = {"active": False} self.print_info = {"machine": {"progress": 40}, "print": {"current_layer": 12, "total_layers": 30, "remaining": 600}} self.log: list[dict] = [] # newest first, as CC2 Control lists it self.calls: list[tuple] = [] self.fail = False self.clock = None # set by tests that need log times for adjust self._ids = (f"s{i:04d}" for i in itertools.count(1)) def _check(self): if self.fail: raise CC2Error("CC2 Control nicht erreichbar") def spools_json(self): return {"enabled": self.enabled, "slots": self.slots, "spools": self.library, "job": self.job, "log": self.log} def spools(self): self._check() return copy.deepcopy(self.spools_json()) def printer(self): self._check() return copy.deepcopy(self.print_info) def canvas(self): self._check() return {} def save_spool(self, fields): self._check() self.calls.append(("save", dict(fields))) if fields.get("id"): sp = next(s for s in self.library if s["id"] == fields["id"]) for key, value in fields.items(): if key == "archived": sp["archived"] = value in ("1", 1, True) elif key != "id": sp[key] = value return {"ok": True, "id": sp["id"]} sp = {"id": next(self._ids), "name": fields.get("name", ""), "brand": fields.get("brand", ""), "material": fields["material"], "color": fields["color"], "diameter": fields.get("diameter", 1.75), "density": fields.get("density", 1.24), "net": fields.get("net", 1000), "remaining": fields.get("remaining", fields.get("net", 1000)), "tare": fields.get("tare", 0), "low": 0, "price": 0, "note": fields.get("note", ""), "created": 0, "used": 0, "archived": False} self.library.append(sp) return {"ok": True, "id": sp["id"]} def adjust(self, cc2_id, remaining): self._check() self.calls.append(("adjust", cc2_id, remaining)) self.weigh(cc2_id, remaining, self.clock() if self.clock else 0) return {"ok": True, "remaining": remaining} def delete(self, cc2_id): self._check() self.calls.append(("delete", cc2_id)) self.library = [s for s in self.library if s["id"] != cc2_id] return {"ok": True} def assign(self, slot, spool_id): self._check() self.calls.append(("assign", slot, spool_id)) for s in self.slots: if spool_id and s["spool"] == spool_id: s["spool"] = "" self.slots[slot]["spool"] = spool_id or "" self.slots[slot]["question"] = "" return {"ok": True} def enable(self, on): self._check() self.calls.append(("enable", on)) self.enabled = on return {"ok": True, "enabled": on} def consume(self, cc2_id, grams): sp = next(s for s in self.library if s["id"] == cc2_id) sp["remaining"] -= grams def add_log(self, time, kind="print", spool="", slot=0, grams=0.0, mm=0.0, job="", result=""): self.log.insert(0, {"time": int(time), "spool": spool, "slot": slot, "kind": kind, "grams": grams, "mm": mm, "job": job, "result": result}) def weigh(self, cc2_id, remaining, time): sp = next(s for s in self.library if s["id"] == cc2_id) self.add_log(time, "correction", cc2_id, -1, remaining - sp["remaining"], 0, "", "weighed") sp["remaining"] = remaining def close(self): pass class FakeSpoolman: def __init__(self): self.db: dict[int, dict] = {} 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, "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, } return self.db[sid] def _check(self): if self.fail: raise SpoolmanError("Spoolman nicht erreichbar") def spool(self, sid): self._check() if sid not in self.db: raise SpoolmanNotFound("nicht gefunden") return copy.deepcopy(self.db[sid]) def spools(self, archived=False): self._check() return [copy.deepcopy(s) for s in self.db.values() if archived or not s["archived"]] def update_spool(self, sid, body): self._check() 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") if "location" in body: self.calls.append(("location", sid, body.get("location"))) if "used_weight" in body: self.calls.append(("used", sid, body["used_weight"])) sp["used_weight"] = body["used_weight"] sp["remaining_weight"] = sp["initial_weight"] - body["used_weight"] if "extra" in body: self.calls.append(("extra", sid, dict(body["extra"]))) if "location" in body: sp["location"] = body["location"] if "lot_nr" in body: sp["lot_nr"] = body["lot_nr"] if "archived" in body: self.calls.append(("archived", sid, body["archived"])) sp["archived"] = bool(body["archived"]) 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()] vendor_list: list | None = None filament_list: list | None = None def vendors(self): self._check() if self.vendor_list is None: self.vendor_list = [{"id": 1, "name": "SUNLU"}] return copy.deepcopy(self.vendor_list) def filaments(self): self._check() if self.filament_list is None: self.filament_list = [{"id": 1, "name": "PLA+ Black", "vendor": {"id": 1, "name": "SUNLU"}, "material": "PLA+", "color_hex": "000000", "density": 1.24, "diameter": 1.75, "weight": 1000.0}] return copy.deepcopy(self.filament_list) def create_vendor(self, body): self._check() self.calls.append(("create_vendor", dict(body))) v = {"id": len(self.vendors()) + 1, **body} self.vendor_list.append(v) return v def create_filament(self, body): self._check() self.calls.append(("create_filament", dict(body))) vendor = next(v for v in self.vendors() if v["id"] == body["vendor_id"]) f = {"id": len(self.filaments()) + 1, "vendor": vendor, **body} self.filament_list.append(f) return f def create_spool(self, body): self._check() self.calls.append(("create_spool", dict(body))) sid = max(self.db, default=0) + 1 fil = next(f for f in self.filaments() if f["id"] == body["filament_id"]) self.db[sid] = {"id": sid, "lot_nr": None, "remaining_weight": body.get("initial_weight") or fil.get("weight"), "initial_weight": body.get("initial_weight"), "used_weight": 0.0, "location": body.get("location", ""), "archived": False, "extra": {}, "filament": fil} return copy.deepcopy(self.db[sid]) 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() self.calls.append(("use", sid, round(grams, 3))) sp = self.db[sid] sp["used_weight"] += grams sp["remaining_weight"] -= grams if self.fail_use_after_apply: raise SpoolmanError("Timeout") return sp def close(self): pass