No description
Find a file
2026-11-04 10:13:16 +01:00
src printer-watch: tests, README, and a repo without the live config 2026-11-03 09:11:31 +01:00
.gitignore printer-watch: tests, README, and a repo without the live config 2026-11-03 09:11:31 +01:00
Cargo.lock chore(deps): ureq 3.5.1 (connection reuse and cookie header fixes) 2026-11-04 10:13:16 +01:00
Cargo.toml printer-watch: tests, README, and a repo without the live config 2026-11-03 09:11:31 +01:00
config.example.json printer-watch: tests, README, and a repo without the live config 2026-11-03 09:11:31 +01:00
README.md printer-watch: tests, README, and a repo without the live config 2026-11-03 09:11:31 +01:00

printer-watch

Kleiner Wächter, der Kamerabilder von 3D-Druckern an einen Bildklassifikator gibt und meldet, wenn ein Fehldruck wahrscheinlich ist. Gedacht für Drucker, die kein Obico-Sentinel können: das Programm holt die Bilder selbst aus Home Assistant und schickt am Ende nur einen Webhook.

Warum

Ein Fehldruck kostet Filament, Strom und Druckzeit — und man merkt es oft erst Stunden später. Obico/Spaghetti-Detection läuft normalerweise nur dort, wo OctoPrint oder Moonraker die Bilder liefert. Dieses Programm schließt die Lücke: es fragt Home Assistant nach dem Druckstatus, holt das aktuelle Kamerabild und lässt es von einem obico-ml-Dienst bewerten. Erst wenn mehrere Frames in Folge über der Schwelle liegen, geht ein Alarm raus — ein einzelner schiefer Frame soll nicht die Nacht ruinieren.

Das Programm pausiert nicht selbst. Es schickt nur den Webhook; was daraus wird (Pause, Benachrichtigung, Lampe), entscheidet eine Automation in Home Assistant.

Ablauf

Pro Drucker läuft ein eigener Thread. Pro Runde (poll_secs):

  1. GET {ha_url}/api/states/{state_entity} → state als String. Fehler oder fehlendes Feld ergeben einen leeren State.

  2. Aktiv? Nein, wenn der State leer ist oder (case-insensitive) in inactive_values steht. Alles andere — printing, preheating, homing, unbekannte Werte — gilt als laufender Druck. Eine Positivliste wäre bei Herstellern, die ihre Statusstrings ständig ändern, zu löchrig.

  3. Nicht aktiv: Streak zurücksetzen, Score 0.0 an HA pushen, schlafen.

  4. Aktiv und Warmup noch nicht abgelaufen: nur schlafen. Der Warmup gilt für Drücke, die während der Laufzeit starten; läuft beim Start schon einer (Container-Neustart mitten im Druck), wird sofort bewertet.

  5. GET {ha_url}/api/camera_proxy/{camera} → JPEG-Bytes, die im Speicher unter dem Druckernamen liegen bleiben (maximal der letzte Frame).

  6. Der ML-Dienst wird gefragt: GET {ml_url}/p/?img=<urlencoded self_url/frame/<name>>. Wichtig: obico-ml holt sich das Bild selbst über self_url. Deshalb muss dieser Dienst aus Sicht des ML-Containers erreichbar sein (im gleichen Docker-Netz z.B. http://watcher:8000) — nicht aus Sicht deines Browsers.

  7. Aus der Antwort (detections) wird die höchste Konfidenz genommen. Fehler, Timeout, kaputtes JSON: Score 0.0 und ml_ok = false.

  8. Der Score geht als Sensor nach HA: sensor.<name>_sentinel_score, State = Score × 100 mit einer Dezimalen, Attribute u.a. unit_of_measurement: %, threshold, printer.

  9. Streak: Score < threshold → 0; sonst +1. Bei confirm Treffern in Folge wird ausgelöst und der Zähler auf 0 gesetzt (nicht bei jedem Frame feuern).

  10. Auslösen nur, wenn der Cooldown (cooldown_secs seit dem letzten Alarm) abgelaufen ist → POST {ha_url}/api/webhook/{webhook_id} mit:

    {
      "printer": "cc1",
      "title": "⚠️ Fehldruck erkannt – Centauri Carbon",
      "message": "3 Frames in Folge über 40 % (Score 87 %).",
      "score": 0.87,
      "event": "detection",
      "pause_entity": "button.…",
      "camera": "camera.…",
      "state_entity": "sensor.…"
    }
    

Alle HTTP-Aufrufe haben ein globales Timeout von 15 s, damit ein toter Host nicht den Thread blockiert.

HTTP-Endpunkte

Der Dienst bringt einen minimalen HTTP-Server mit (Standard 0.0.0.0:8000). Er existiert, damit obico-ml die Frames abholen kann.

Pfad Antwort
/healthz 200, text/plain, Body ok
/status 200, application/json, Abbild Druckername → [score, ml_ok, frames_ok, alter]
/diag 200, text/plain, die letzten ≤ 25 Meldungen, zeilenweise
/frame/<name> 200, image/jpeg, der zuletzt geholte Frame; unbekannt/leer → 404
alles andere 404

Zu /status: frames_ok ist derzeit immer 1 und alter immer 0 — die Felder sind im Tupel vorhanden, werden aber noch nicht fortgeschrieben. Der Eintrag erscheint erst, nachdem ein Drucker einmal bewertet wurde.

Der Server hat keine Authentifizierung und kein TLS. Nur im internen Netz betreiben.

Konfiguration

Pfad: erstes Kommandozeilen-Argument, sonst /config/config.json. Vorlage: config.example.json kopieren und anpassen.

cp config.example.json config.json

config.json, config.test.json und *.local.json sind per .gitignore ausgeschlossen — dort stehen echte LAN-Adressen und ggf. Tokens. Nicht einchecken.

Umgebungsvariablen

Variablen schlagen die Werte aus der Datei.

Variable Wirkung Default
HA_URL Home-Assistant-Basis-URL "" (aus Datei)
HA_TOKEN Bearer-Token für HA; besser hier als in der Datei ""
ML_URL Basis-URL des obico-ml-Dienstes http://obico-ml:3333
SELF_URL wie obico-ml diesen Dienst erreicht http://watcher:8000
LISTEN Bind-Adresse des HTTP-Servers 0.0.0.0:8000

LISTEN wird ausschließlich aus der Umgebung gelesen; ein listen-Feld in config.json wird ignoriert. Der Default 0.0.0.0:8000 ist für den Betrieb im Container gedacht.

Felder

Feld Bedeutung Default
ha_url Home-Assistant-Basis-URL ""
ha_token Bearer-Token (oder HA_TOKEN) ""
ml_url obico-ml, Endpunkt /p/?img=… http://obico-ml:3333
self_url eigene URL, wie obico-ml sie sieht http://watcher:8000
listen wird ignoriert, siehe LISTEN —
threshold Score, ab dem ein Frame zählt (0…1) 0.4
confirm so viele Treffer in Folge lösen aus 3
poll_secs Pause zwischen den Runden 10
warmup_secs Schonzeit nach Druckstart 300
cooldown_secs Mindestabstand zwischen zwei Alarmen 900
webhook_id ID des HA-Webhooks printer-sentinel
inactive_values States, die „kein Druck" bedeuten siehe unten
printers[] Liste der Drucker (Pflicht) —

Default für inactive_values: idle, standby, unavailable, unknown, offline, complete, completed, finished, stopped, stop, paused, error.

Pro Drucker: name (Pflicht, landet in Entitätsnamen und URLs), camera (Pflicht), state_entity (Pflicht), title (Anzeigename, optional), pause_entity (optional, wird nur in den Webhook durchgereicht).

Fehlende Datei, kaputtes JSON, fehlendes printers[] oder ein Drucker ohne name/camera/state_entity: das Programm bricht beim Start mit einer Klartext-Meldung ab (config file, config json, printers[], camera, …). Lieber laut sterben als stumm nichts tun.

Bauen und starten

cargo build --release
./target/release/printer-watch-rs /pfad/zu/config.json

Im Container: config.json nach /config/config.json mounten (dort sucht das Programm ohne Argument), HA_TOKEN als Umgebungsvariable setzen, Port 8000 im gemeinsamen Docker-Netz erreichbar machen. Ein Dockerfile liegt nicht bei.

Tests

cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test

Alles offline, ohne Netzzugriffe. Abgedeckt sind:

  • load_config: vollständige Datei, Defaults für fehlende Felder, Env schlägt Datei, LISTEN ignoriert die Datei, Panics bei fehlender/kaputter Config.
  • Aktiv-Erkennung: case-insensitive Inaktiv-Liste, leerer State, unbekannte States gelten als Druck.
  • urlencode: Leerzeichen, Umlaute (UTF-8, zwei Bytes), Sonderzeichen, Unreserved-Zeichen, leerer String.
  • next_streak: Schwelle exakt getroffen, confirm == 0, Streak über confirm hinaus, Abbruch durch einen Frame unter der Schwelle.
  • HTTP-Server: echte Anfragen über einen Socket auf 127.0.0.1:0 gegen die Handler-Logik — /healthz → 200 ok, /status → 200 JSON, unbekannter Pfad und unbekannter Frame → 404.

Die Tests, die load_config prüfen, laufen serialisiert (gemeinsamer Mutex), weil sie Prozess-Umgebungsvariablen setzen.

Was das Tool nicht kann

Ehrlich, damit niemand mehr erwartet als drin ist:

  • Keine eigene Bildanalyse. Der Score ist die höchste Konfidenz, die obico-ml für einen Frame liefert. Was das Modell erkennt (und wie gut), ist komplett Sache des ML-Dienstes. Dieses Programm zählt und meldet nur.
  • Keine Spaghetti-Erkennung im engeren Sinn. Kein Vergleich mit dem Soll-Druck, keine Layer- oder G-Code-Prüfung, keine Erkennung von Verzug/Detachment/Binderfehlern — nur das, was das Modell im Bild sieht.
  • Ein Frame pro Intervall, kein Video. Kurze Ereignisse zwischen zwei Polls können komplett übersehen werden.
  • ML-Ausfall sieht aus wie „alles gut". Bei Fehler wird Score 0.0 gepusht, damit die Streak nicht stehen bleibt. Sichtbar wird das nur in /diag und an ml_ok = false in /status. Es gibt keinen eigenen Alarm dafür.
  • Pausiert nicht selbst. Ohne passende HA-Automation passiert auf den Webhook hin nichts.
  • Kein Zustand über einen Neustart. Streak, Cooldown und Warmup-Zeitpunkt leben nur im Speicher; nach einem Neustart beginnt alles von vorn.
  • Nur der letzte Frame liegt vor. /frame/<name> liefert genau ein Bild, keine Historie.
  • Keine Retries, kein Backoff. Ein einzelner fehlgeschlagener Request kostet eine Runde, nicht mehr.
  • Keine Authentifizierung am eigenen HTTP-Server, kein TLS, ein Request pro Verbindung, 2048-Byte-Puffer für die Anfrage. Fürs interne Netz gebaut, nicht fürs Internet.