Grotti 3.7.0: Treiber für Kobra 4 und Marlin, smartere Dateien-Auswahl und einstellbare Etiketten.
Senden und Abbruch laufen über den Treiber, Orca-Doppelzeilen und tote Ideen-API fallen weg, Drucker folgen der Datei, Etikettenformat liegt in den Einstellungen.
This commit is contained in:
1 parent
5081212212
commit
db6a85a703
54 files changed
+3379
-384
No files matched your search
+77
-51
@@ -40,10 +40,13 @@ DOCS_DIR = TOOL_ROOT / "docs"
|
||||
PROJECT_STATUS = {"idee", "bereit", "druckt", "fertig"}
|
||||
VERDICTS = {"", "gut", "ausschuss"}
|
||||
VERDICT_REASONS = sorted(grotti_verdict.VERDICT_REASONS)
|
||||
IDEA_STATUS = {"offen", "spaeter", "erledigt"}
|
||||
PRINTER_FAMILIES = set(FAMILY_LABELS) | {""}
|
||||
UPLOAD_SUFFIXES = (".gcode", ".3mf", ".gcode.3mf")
|
||||
HOST_RE = re.compile(r"^[A-Za-z0-9.\-]+(?::\d{1,5})?$")
|
||||
SERIAL_RE = re.compile(
|
||||
r"^(?:COM\d+|/dev/tty(?:USB|ACM)\d+|/dev/serial/by-id/[A-Za-z0-9._\-]+)(?:@\d{3,7})?$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
TOKEN_QUERY_PATHS = (
|
||||
"/api/v1/kamera",
|
||||
"/api/v1/kamera/stream",
|
||||
@@ -59,13 +62,11 @@ STEER_ACTIONS = {
|
||||
|
||||
def validate_printer_host(host):
|
||||
host = (host or "").strip()
|
||||
if not host or not HOST_RE.fullmatch(host):
|
||||
if not host or ".." in host or not (HOST_RE.fullmatch(host) or SERIAL_RE.fullmatch(host)):
|
||||
raise GrottiError(
|
||||
"Host nur als Hostname oder IP, optional mit Port (z. B. 10.10.1.60).",
|
||||
"Host als IP, Hostname, COM-Port (COM5 oder COM5@250000) oder /dev/tty…",
|
||||
"host",
|
||||
)
|
||||
if ".." in host:
|
||||
raise GrottiError("Ungültiger Host.", "host")
|
||||
return host
|
||||
|
||||
|
||||
@@ -732,6 +733,7 @@ def create_app(cfg):
|
||||
token="",
|
||||
spool=spool,
|
||||
qr_url=qr_url,
|
||||
etikett=cfg.get("etikett") or grotti_config._etikett({}),
|
||||
)
|
||||
|
||||
@app.get("/protokoll")
|
||||
@@ -792,6 +794,7 @@ def create_app(cfg):
|
||||
sheets=sheets,
|
||||
spools=spools,
|
||||
target_g=target_g,
|
||||
etikett=cfg.get("etikett") or grotti_config._etikett({}),
|
||||
)
|
||||
|
||||
@app.get("/ideen")
|
||||
@@ -879,6 +882,7 @@ def create_app(cfg):
|
||||
auto_retry = data.get("auto_retry") if "auto_retry" in data else None
|
||||
fqdn = data.get("fqdn") if "fqdn" in data else None
|
||||
watch_dir = data.get("watch_dir") if "watch_dir" in data else None
|
||||
etikett = data.get("etikett") if isinstance(data.get("etikett"), dict) else None
|
||||
return jsonify(grotti_config.save_settings(
|
||||
cfg,
|
||||
energy=energy,
|
||||
@@ -889,6 +893,7 @@ def create_app(cfg):
|
||||
auto_retry=auto_retry,
|
||||
fqdn=fqdn,
|
||||
watch_dir=watch_dir,
|
||||
etikett=etikett,
|
||||
))
|
||||
|
||||
@app.post("/api/v1/telegram/test")
|
||||
@@ -931,8 +936,12 @@ def create_app(cfg):
|
||||
|
||||
by_id = {row["host"]: row for row in rows}
|
||||
if request.args.get("live", "1") == "0":
|
||||
from grotti.drivers import for_printer
|
||||
for row in rows:
|
||||
row["mainsail"] = "http://%s/" % row["host"]
|
||||
if for_printer(row).transport == "moonraker":
|
||||
row["mainsail"] = "http://%s/" % row["host"]
|
||||
else:
|
||||
row["mainsail"] = ""
|
||||
attach(row)
|
||||
return jsonify(rows)
|
||||
# Eine Live-Quelle: Watcher. Frischer als 60 s, sonst einmal selbst abfragen.
|
||||
@@ -940,7 +949,8 @@ def create_app(cfg):
|
||||
cached = watcher.by_host()
|
||||
age = watcher.age_s()
|
||||
if not cached or age is None or age > 60:
|
||||
lives = moonraker.snapshot(rows)
|
||||
from grotti.drivers import snapshot
|
||||
lives = snapshot(rows)
|
||||
else:
|
||||
lives = []
|
||||
for row in rows:
|
||||
@@ -955,7 +965,11 @@ def create_app(cfg):
|
||||
item = dict(row)
|
||||
item["ok"] = False
|
||||
item["state"] = "offline"
|
||||
item["mainsail"] = "http://%s/" % row["host"]
|
||||
from grotti.drivers import for_printer as _for_printer
|
||||
if _for_printer(row).transport == "moonraker":
|
||||
item["mainsail"] = "http://%s/" % row["host"]
|
||||
else:
|
||||
item["mainsail"] = ""
|
||||
lives.append(item)
|
||||
for live in lives:
|
||||
meta = by_id.get(live.get("host") or "") or {}
|
||||
@@ -963,6 +977,53 @@ def create_app(cfg):
|
||||
attach(live)
|
||||
return jsonify(lives)
|
||||
|
||||
@app.get("/api/v1/marlin-dienst")
|
||||
def marlin_dienst():
|
||||
from grotti.marlin_supervisor import current_status, ensure_if_enabled
|
||||
|
||||
ensure_if_enabled()
|
||||
return jsonify(current_status())
|
||||
|
||||
@app.post("/api/v1/meldungen")
|
||||
def meldung_create():
|
||||
from grotti import meldung as grotti_meldung
|
||||
|
||||
require_role("operator")
|
||||
data = body()
|
||||
try:
|
||||
row = grotti_meldung.create(
|
||||
conn(),
|
||||
cfg,
|
||||
text=data.get("text") or "",
|
||||
page=data.get("page") or "",
|
||||
printer_host=data.get("printer_host") or "",
|
||||
error_text=data.get("error_text") or "",
|
||||
user=g.user,
|
||||
)
|
||||
conn().commit()
|
||||
except ValueError as exc:
|
||||
raise GrottiError(str(exc), "meldung") from exc
|
||||
return jsonify(row), 201
|
||||
|
||||
@app.get("/api/v1/meldungen")
|
||||
def meldung_list():
|
||||
from grotti import meldung as grotti_meldung
|
||||
|
||||
require_role("admin")
|
||||
status = (request.args.get("status") or "").strip() or None
|
||||
return jsonify(grotti_meldung.list_rows(conn(), status=status))
|
||||
|
||||
@app.post("/api/v1/meldungen/<int:meldung_id>/erledigt")
|
||||
def meldung_close(meldung_id):
|
||||
from grotti import meldung as grotti_meldung
|
||||
|
||||
require_role("admin")
|
||||
row = grotti_meldung.close(conn(), meldung_id)
|
||||
if not row:
|
||||
raise GrottiError("Die Meldung fehlt.", "meldung", 404)
|
||||
conn().commit()
|
||||
return jsonify(row)
|
||||
|
||||
@app.post("/api/v1/drucker")
|
||||
def printer_create():
|
||||
data = body()
|
||||
@@ -1555,6 +1616,7 @@ def create_app(cfg):
|
||||
optional_id(data.get("spool_id")),
|
||||
ace_maps,
|
||||
bed_leveling=bool(data.get("bed_leveling")),
|
||||
marlin_mode=(data.get("marlin_mode") or "stream"),
|
||||
)
|
||||
return jsonify({"jobs": created, "count": len(created), "mode": "now"}), 202
|
||||
|
||||
@@ -1973,8 +2035,13 @@ def create_app(cfg):
|
||||
# laufenden Druck auf dem Host best-effort stoppen
|
||||
if was_running and row.get("host"):
|
||||
try:
|
||||
moonraker.command(row["host"], "abbrechen")
|
||||
stopped = ", Druck gestoppt"
|
||||
from grotti.drivers import for_printer
|
||||
printer = store.printer_by_host(conn(), row["host"])
|
||||
if printer:
|
||||
for_printer(printer).command(row["host"], "abbrechen")
|
||||
stopped = ", Druck gestoppt"
|
||||
else:
|
||||
stopped = ", Druck nicht gestoppt"
|
||||
except Exception:
|
||||
stopped = ", Druck nicht gestoppt"
|
||||
store.add_event(
|
||||
@@ -2142,47 +2209,6 @@ def create_app(cfg):
|
||||
def color_list_api():
|
||||
return jsonify(color_catalog())
|
||||
|
||||
@app.get("/api/v1/ideen")
|
||||
def idea_list():
|
||||
return jsonify(store.list_ideas(conn()))
|
||||
|
||||
@app.post("/api/v1/ideen")
|
||||
def idea_create():
|
||||
data = body()
|
||||
title = (data.get("title") or "").strip()
|
||||
if not title:
|
||||
raise GrottiError("Die Idee braucht einen Titel.", "titel")
|
||||
status = data.get("status") or "offen"
|
||||
if status not in IDEA_STATUS:
|
||||
raise GrottiError("Unbekannter Status.", "status")
|
||||
return jsonify(store.create_idea(conn(), title, (data.get("body") or "").strip(), status)), 201
|
||||
|
||||
@app.patch("/api/v1/ideen/<int:idea_id>")
|
||||
def idea_patch(idea_id):
|
||||
data = body()
|
||||
fields = {}
|
||||
if "title" in data:
|
||||
title = (data.get("title") or "").strip()
|
||||
if not title:
|
||||
raise GrottiError("Die Idee braucht einen Titel.", "titel")
|
||||
fields["title"] = title
|
||||
if "body" in data:
|
||||
fields["body"] = (data.get("body") or "").strip()
|
||||
if "status" in data:
|
||||
if data.get("status") not in IDEA_STATUS:
|
||||
raise GrottiError("Unbekannter Status.", "status")
|
||||
fields["status"] = data.get("status")
|
||||
row = store.update_idea(conn(), idea_id, fields)
|
||||
if not row:
|
||||
raise GrottiError("Die Idee fehlt.", "idee", 404)
|
||||
return jsonify(row)
|
||||
|
||||
@app.delete("/api/v1/ideen/<int:idea_id>")
|
||||
def idea_delete(idea_id):
|
||||
if not store.delete_idea(conn(), idea_id):
|
||||
raise GrottiError("Die Idee fehlt.", "idee", 404)
|
||||
return "", 204
|
||||
|
||||
@app.get("/api/v1/kunden")
|
||||
def kunden_list():
|
||||
return jsonify(grotti_orders.list_customers(conn()))
|
||||
|
||||
Reference in new issue
Block a user