Automatisches Starten von 3D-Druckern mit HomeAssistant

Diesmal hab ich was richtig cooles für euch! Ich hab leider das Problem, dass ich den Drucker nicht wirklich über Nacht laufen lassen kann, da die Geräusche so stören. Die stehen zwar im Keller, der SLA Drucker ist auch leise, aber der Anycubic ist schon laut. Aber es gibt im Untergeschoss halt bewohnte Räume der Nachbarn. Und morgens vergesse ich dann oft einfach, dass ich noch den Druck starten wollte. Also habe ich mir mit HomeAssistant eine 3D-Drucker Start Automatik für Octoprint und NanoDLP gebastelt!

Octoprint

Octoprint kennt eigentlich jeder, der sich hobbymäßig mit 3D-Druckern beschäftigt. Mittlerweile hat das Projekt sogar Konkurrenz von anderen 3D-Drucker Servern bekommen. Never Change a running System! Also bin ich dabei geblieben 🙂

Aufbau

Wir nutzen für unsere Startautomatik die REST API von Octoprint. Mit HomeAssistant können wir mit einem rest Sensor die vorhanden Files und Pfade auslesen und mit einem rest Command können wir den Drucker steuern.

Im HomeAssistant brauchen wir nur ein paar Helfer: Einen Input_boolean um die Startautomatik generell zu aktivieren / zu deaktivieren, einen input_select um den Drucker auszuwählen und einen Datums-Helfer um das Datum und die Uhrzeit zu dem der Drucker starten soll einzustellen. Den Rest machen wir innerhalb der Automatisierung mit Wenn-Dann Bedingungen.

Zum Auslesen der Files aus Octoprint habe ich tatsächlich zu NodeRed gegriffen, da mir es einfacher erschien, die Aufgaben im Hintergrund damit umzusetzen.

Vorraussetzungen

Damit das ganze Funktioniert brauchen wir Octoprint am 3D-Drucker. Octoprint läuft auf einem RaspberryPi. Bei mir hängt der Anycubic selbst hinter einer W-Lan Steckdose, die mit Tasmota geflasht ist und mit MQTT gesteuert wird. So auch eine Lampe. Daher werden in der Automation später beide switches auf On gesetzt. Wie ihr Octoprint als OctoPi installiert, könnt ihr hier lesen.

Damit wir mit Octoprints API interagieren können, müssen wir einen Key generieren. Es gibt einen globalen API-Key, den man aber nicht generell nutzen sollte. Man kann unter den Einstellungen (Schraubzieher Icon oben – bzw. Settings) und dann auf Application Keys sehen, welche man schon generiert hat. Unten im Fenster können wir einen neuen erstellen, als Identifier können wir z.B: HomeAssistant eingeben:

Bei mir ist Octoprint durch viele Plug-Ins sehr anders als eine Standard-Installation, lasst euch davon nicht verwirren 🙂

Der neue Key taucht in der Liste auf und kann hier kopiert werden. Den brauchen wir aber erst später.

HomeAssistant Helfer

Wir können im HomeAssistant entweder über die GUI neue Helfer anlegen, oder im FileEditor. Ich füge hier die Code-Bausteine ein.

Zunächst brauchen wir einen input_boolean, der steuert ob die Start-automatik aktiv sein soll. Dazu gehen wir in unseren HomeAssistant File-Editor. (Das ist ein Add-On, welches man sich über Einstellungen – Add Ons installieren kann). Wir öffnen hier die configuration.yaml und fügen den Code hinzu:

input_boolean:
  printer_auto_start:
    name: Drucker Start Automatik
    initial: off
  octoprint_update_files:
    name: Octoprint Update Files
    initial: off

Falls ihr schon andere input_booleans habt, könnt ihr den Code natürlich ab printer_auto_start in den entsprechenden Block kopieren.

Wenn wir schon dabei sind, legen wir auch uneren input_select für die Druckerauswahl an.

input_select:
    printer_select:
        name: Drucker auswahl
        options:
         - Kudo Bean
         - Anycubic i3 Mega
         - Ultimaker
        initial: Kudo Bean

Falls es bei euch nötig ist, das mehrere zur gleichen Zeit starten, könnt ihr das z.B. über mehrere Automatisierungen lösen und diesen Input Select weglassen. Oder ihr macht mehrere input_booleans für jeden Drucker. (A la KudoBean – Autostart on / off).

Ein zweiter input_select ist der für die Auswahl der Datei, die wir drucken wollen. Keine Sorge, ihr müsst euch jetzt nicht vorher Überlegen, wie die Daten heißen müssen, die ihr drucken wollt. Der wird später durch NodeRED befüllt.

input_select:
#  hier sind vermutlich schon andere :)
    octoprint_files:
        name: Octoprint Files
        options:
         - none
        initial: none

Und wir brauchen noch einen input_text in dem wir später den Pfad speichern.

input_text:
  octoprint_origin_of_selection:
    name: Octoprint Origin of Selection
    initial: local

Wichtig ist auf jedenfall der Datetime Helfer, der den Zeitpunkt des Autostarts speichert:

input_datetime:
  printer_autostart:
    name: Printer Autostart
    has_date: true
    has_time: true

Alternativ kann man das auch über das GUI unter Einstellungen – Integrationen – Helfer anlegen:

NodeRed

Im NodeRed ist es bei mir etwas chaotisch geworden, hier ist aber der ganze Flow zum copy&pasten:

[{"id":"1bb568bc7ce1ed92","type":"tab","label":"OctoPrint REST","disabled":false,"info":"","env":[]},{"id":"e4cea3de5791f417","type":"inject","z":"1bb568bc7ce1ed92","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"59 23 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":345.1999816894531,"y":301.1999816894531,"wires":[["702c092b7eb1e44a"]]},{"id":"702c092b7eb1e44a","type":"http request","z":"1bb568bc7ce1ed92","name":"GET Files","method":"GET","ret":"txt","paytoqs":"ignore","url":"http://192.168.178.39/api/files","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[{"keyType":"User-Agent","keyValue":"","valueType":"Mozilla/5.0","valueValue":""},{"keyType":"other","keyValue":"X-Api-Key","valueType":"other","valueValue":"D8A2F028159047328A1AA047AA9DC187"}],"x":520,"y":300,"wires":[["d3fab574dbac7a00","f99b5433816aa142"]]},{"id":"d3fab574dbac7a00","type":"debug","z":"1bb568bc7ce1ed92","name":"debug 8","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":680,"y":300,"wires":[]},{"id":"5bc59365b31b27cd","type":"function","z":"1bb568bc7ce1ed92","name":"get names and paths","func":"msg.headers = msg.originalHeaders;\nvar input = msg.payload;\n\nvar names = [];\nvar paths = [];\nvar origin = [];\nfor (var i = 0; i < msg.payload.files.length; i++) {\n       \n    names.push(input.files[i].display);\n    paths.push(input.files[i].path);\n    origin.push(input.files[i].origin);\n}\n\nmsg.payload.files = [];\nmsg.payload.names = names;\nmsg.payload.paths = paths;\nmsg.payload.origin = origin;\nmsg.topic = \"output\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":660,"y":360,"wires":[["b2555664ccb02b27","79299935f218d264","72fccd4b1e8f4cc4"]]},{"id":"b2555664ccb02b27","type":"debug","z":"1bb568bc7ce1ed92","name":"debug 9","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":880,"y":300,"wires":[]},{"id":"f99b5433816aa142","type":"json","z":"1bb568bc7ce1ed92","name":"","property":"payload","action":"","pretty":false,"x":450,"y":360,"wires":[["5bc59365b31b27cd"]]},{"id":"39d53cdc89596b25","type":"api-call-service","z":"1bb568bc7ce1ed92","name":"Store to Input Select","server":"c1eed9d5.7b0d28","version":5,"debugenabled":false,"domain":"input_select","service":"set_options","areaId":[],"deviceId":[],"entityId":["input_select.octoprint_files"],"data":"payload","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"all","x":1440,"y":360,"wires":[["a59f5cf92c827e40"]]},{"id":"a59f5cf92c827e40","type":"debug","z":"1bb568bc7ce1ed92","name":"debug 10","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1680,"y":360,"wires":[]},{"id":"79299935f218d264","type":"function","z":"1bb568bc7ce1ed92","name":"convert to comma seperated list","func":"var names = msg.payload.names.join('\",\"').toString();\n\nmsg.payload = '{\"options\": [\"' +  names + '\"]}';\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1010,"y":360,"wires":[["5326a9cc9934bb7f","1efc5a40c4e502ad"]]},{"id":"5326a9cc9934bb7f","type":"debug","z":"1bb568bc7ce1ed92","name":"debug 11","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1240,"y":300,"wires":[]},{"id":"1efc5a40c4e502ad","type":"json","z":"1bb568bc7ce1ed92","name":"","property":"payload","action":"","pretty":false,"x":1250,"y":360,"wires":[["39d53cdc89596b25"]]},{"id":"ef09e9964e8a6255","type":"poll-state","z":"1bb568bc7ce1ed92","name":"Manual File Update","server":"c1eed9d5.7b0d28","version":2,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"updateinterval":"60","updateIntervalType":"num","updateIntervalUnits":"seconds","outputinitially":false,"outputonchanged":true,"entity_id":"input_boolean.octoprint_update_files","state_type":"habool","halt_if":"true","halt_if_type":"bool","halt_if_compare":"is","outputs":2,"x":410,"y":520,"wires":[["d7ba997212d207d6"],["d7ba997212d207d6"]]},{"id":"d7ba997212d207d6","type":"trigger","z":"1bb568bc7ce1ed92","name":"","op1":"1","op2":"0","op1type":"str","op2type":"str","duration":"0","extend":false,"overrideDelay":false,"units":"ms","reset":"false","bytopic":"all","topic":"topic","outputs":1,"x":340,"y":760,"wires":[["702c092b7eb1e44a"]]},{"id":"72fccd4b1e8f4cc4","type":"function","z":"1bb568bc7ce1ed92","name":"compare to selection","func":"msg.headers = msg.originalHeaders;\nvar input = msg.payload;\n\nvar name;\nvar path;\nvar origin;\nvar comp = global.get('homeassistant.homeAssistant.states[\"input_select.octoprint_files\"].state');\nfor (var i = 0; i < input.names.length; i++) {\n    if (input.names[i] == comp ){\n        origin = input.origin[i];\n        path = input.paths[i];\n        name = input.names[i];\n    } \n}\n\n// msg.payload.files = [];\nmsg.payload.name = name;\nmsg.payload.comp = comp;\nmsg.payload.path = path;\nmsg.payload.origin = origin;\nmsg.topic = \"selection\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":980,"y":400,"wires":[["42817f3043c9eda7","7781c7c80458805f"]]},{"id":"99793616ae6fbe81","type":"poll-state","z":"1bb568bc7ce1ed92","name":"Print Automatic On","server":"c1eed9d5.7b0d28","version":2,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"updateinterval":"60","updateIntervalType":"num","updateIntervalUnits":"seconds","outputinitially":false,"outputonchanged":true,"entity_id":"input_boolean.drucker_start_automatik","state_type":"habool","halt_if":"true","halt_if_type":"bool","halt_if_compare":"is","outputs":2,"x":630,"y":720,"wires":[["f73a10ed2154a386"],["f73a10ed2154a386"]]},{"id":"f73a10ed2154a386","type":"trigger","z":"1bb568bc7ce1ed92","name":"","op1":"1","op2":"0","op1type":"str","op2type":"str","duration":"0","extend":false,"overrideDelay":false,"units":"ms","reset":"false","bytopic":"all","topic":"topic","outputs":1,"x":820,"y":720,"wires":[["702c092b7eb1e44a"]]},{"id":"42817f3043c9eda7","type":"debug","z":"1bb568bc7ce1ed92","name":"debug 12","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1260,"y":400,"wires":[]},{"id":"037aa605f51ebdcc","type":"poll-state","z":"1bb568bc7ce1ed92","name":"","server":"c1eed9d5.7b0d28","version":2,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"updateinterval":"60","updateIntervalType":"num","updateIntervalUnits":"seconds","outputinitially":true,"outputonchanged":true,"entity_id":"input_select.octoprint_files","state_type":"str","halt_if":"","halt_if_type":"str","halt_if_compare":"is","outputs":1,"x":920,"y":780,"wires":[["f296e562ef4f9918"]]},{"id":"f296e562ef4f9918","type":"debug","z":"1bb568bc7ce1ed92","name":"debug 13","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1260,"y":760,"wires":[]},{"id":"7781c7c80458805f","type":"api-call-service","z":"1bb568bc7ce1ed92","name":"Store to Input Text","server":"c1eed9d5.7b0d28","version":5,"debugenabled":false,"domain":"input_text","service":"set_value","areaId":[],"deviceId":[],"entityId":["input_text.octoprint_origin_of_selection"],"data":"{\"value\":msg.payload.origin}","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"all","x":1290,"y":440,"wires":[["6effdd91187887d5"]]},{"id":"6effdd91187887d5","type":"debug","z":"1bb568bc7ce1ed92","name":"debug 14","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1480,"y":440,"wires":[]},{"id":"c1eed9d5.7b0d28","type":"server","name":"Home Assistant","addon":true}]

Wir brauchen mindestens einen Auslöser. Bei mir gibt es gleich mehrere. Jeden Tag um Mitternacht soll der Pfad einmal ausgelöst werden. Spätestens wenn man die Startautomatik aktiviert, oder wenn man halt einmal Manuell die Files updaten will. Es wird ein GET Request an die REST API in Octoprint gestellt. Dieser gibt jede Menge Infos zurück. Die Filtern wir auf die Daten die wir brauchen (Name, Origin, Path) und speichern die Namen in einen input_select.

Falls ihr eure G-codes nur per Weboberfläche in den Octoprint schiebt, könnte man Origin auch hardcoden, aber sicher ist sicher.

Wir nehmen uns dann den Wert aus dem Input Select, suchen im Array nach dem Index und ziehen Path und Origin dieses Index raus. Bei mir waren Path und Name zwar immer identisch, aber in der Dokumentation wird ganz klar auf Path für den start des Druckers verwiesen. Also halte ich mich einfach mal dran.

Entwicklung des Flows

Damit die Noobs wie ich hier auch noch was mitnehmen können, gehe ich hier einmal durch, wie ich den Flow entwickelt hab.
Zunächst habe ich geschaut, wie der GET Request für Octoprint aussehen muss. Dazu braucht man einen HTTP Request node.

Bei der URL muss die IP im Heimnetzt eurer OctoPi instanz rein und mit /api/files ergänzt werden. Ausgelöst wird der request (Methode GET, da wir etwas erhalten wollen) durch einen injection Node.

Mit doppelklick könnt ihr die Nodes bearbeiten, links am Rand mittig könnt ihr die Palette ein und ausklappen.

Die Debug Nodes geben dann den Response wieder, der nun im msg.payload zu finden ist.

Wenn ich links auf das kleine Quadrat am Injection Node drücke, wird dieser manuell ausgelöst. Ihr könnt den auch auf einen Intervall wie 5 sekunden setzen zum Entwickeln (Achtung bei kostenpflichtigen externen API Calls). Im rechten Rand muss auf den Debug-Reiter gewechselt sein, sonst sehen wir nix. Bei mir kommt dann ein großer JSON String zurück. Diesen wandeln wir mit dem JSON-Node in ein echtes JSON Objekt um.

Rechts bei den Debug-Nodes kann man mit dem grünen Quadrat aktivieren / deaktivieren.

Struktur

Nachdem das JSON Objekt umgewandelt wurde, lässt sich auch ganz gut die Struktur erkennen. Die brauchen wir um im nächsten Node (einem Function Node) gescheit arbeiten zu können.

msg.headers = msg.originalHeaders;
var input = msg.payload;
var names = [];
var paths = [];
var origin = [];
for (var i = 0; i < msg.payload.files.length; i++) {
names.push(input.files[i].display);
paths.push(input.files[i].path);
origin.push(input.files[i].origin);
}
msg.payload.files = [];
msg.payload.names = names;
msg.payload.paths = paths;
msg.payload.origin = origin;
msg.topic = "output";
return msg;

Wir brauchen also ein paar Arrays für unsere Namen, Paths und Origins. Dann gehen wir alle Elemente des Objekts msg.payload.files (das wissen wir, weil wir die Struktur gelesen haben, sonst hätten wir vermutlich wie bescheuert mit msg.payload gearbeitet) durch, in dem wir eine For-Schleife für alle Indizes I mit Schrittweite 1 bis zum Ende von .files nutzen. Wenn ihr also 10 oder 23 Dateien auf der Karte habt, funktioniert es trotzdem.

Die Werte werden dann immer mit push an das Ende des Arrays gehängt. Nach der Schleife hängen wir unsere neuen Arrays an das msg.payload und geben die Nachricht an den nächsten Node weiter.

Wir sehen, nun gibt es neben msg.payload.files auch msg.payload.names etc.

Der nächste Funktionsnode wandelt dann alle Namen in eine List innerhalb eines String um. Hier Formuliere ich schon den JSON String, den wir im nächsten Node brauchen.

var names = msg.payload.names.join('","').toString();
msg.payload = '{"options": ["' +  names + '"]}';
return msg;

Der Grund, warum ich das auf zwei Funktions-Nodes aufgeteilt habe ist der, dass ich den großen payload nicht die ganze Zeit mitschleppen will, aber die Infos noch brauche um die Indizes später zu nutzen. Also ab hier ist der Payload nur noch der Strings mit der Liste der Dateinamen. Die restlichen Infos sind weg. Ich brauche aber ein Array mit der Anzahl der Elemente.

JSON

Die Liste wird wieder in ein JSON Objekt umgewandelt, mit dem JSON Node und kommt dann in einen Service-Call.

Hier wird die Liste der Dateien als Optionen in den input_select übertragen, in dem wir den set_options service nutzen. Hier gab es für mich einige Stolpersteine, da die Umwandlung von JSON Objekt zu JSON String für mich nicht offensichtlich genug war. Ich hatte immer alle Elemente der Liste als ein Objekt im input_select. Ganz eigenartig.

Parallel zur Liste (die sollte ja vom letzten Durchlauf um Mitternacht bekannt sein), starten wir den vergleich zum input_select.

msg.headers = msg.originalHeaders;
var input = msg.payload;
var name;
var path;
var origin;
var comp = global.get('homeassistant.homeAssistant.states["input_select.octoprint_files"].state');
for (var i = 0; i < input.names.length; i++) {
if (input.names[i] == comp ){
origin = input.origin[i];
path = input.paths[i];
name = input.names[i];
} 
}
// msg.payload.files = [];
msg.payload.name = name;
msg.payload.comp = comp;
msg.payload.path = path;
msg.payload.origin = origin;
msg.topic = "selection";
return msg;

Den msg.payload.comp lasse ich mir nur zu debuging-zwecken extra ausgeben, da es bei mir hier auch ein paar Schwierigkeiten gab.

Mit einem neuen service call speichern wir nun den gefundenen Pfad bzw. Origin in ein input_text. Hier sehen wir, dass nicht msg.payload sondern msg.payload.origin gespeichert wird.

Hier hab ich mich dazu entschieden, einfach den input_select (also Name der Datei) zu nehmen, anstatt extra nochmal den Path. Wenn ihr also hier Probleme bekommt, braucht ihr noch einen Input_text und müsst später den rest_command anpassen. Der rest sollte im Flow ja schon integriert sein.

Nun haben wir ein paar tolle Bausteine für unser Interface und können Daten aus Octoprint auslesen. Nun müssen wir wieder in den File-Editor und einen rest_command in der config.yaml erstellen.

rest_command:
octoprint_file_select:
url: 'http://192.168.178.39/api/files/{{states("input_text.octoprint_origin_of_selection")}}/{{states("input_select.octoprint_files")}}'
method: POST
headers:
Content-Type: application/json
#User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
X-Api-Key: ASDF-YOUR-API-KEY-ASDF
#Upgrade-Insecure-Requests: 1
Referer: 'http://192.168.178.68'
payload: '{"command": "select", "print": true }'

Achtung – ihr müsst euren Key natürlich einfügen und eure IP anpassen. Wenn ihr die Helfer anders benannt habt, die natürlich auch! (Ich glaube ich bin hier im Tutorial auch nicht ganz konsistent.) Die auskommentierten Zeilen könnt ihr auch löschen, die braucht man nicht unbedingt.

Nun speichern wir die config und müssen HomeAssistant einmal neu starten.

Als nächstes können wir die Automation bauen:

Automation

Wenn HomeAssistant wieder da ist, gehen wir auf Einstellungen-Automatisierungen und legen eine neue automatisierung an. Die sieht bei mir am Ende so aus:

Ihr könnt die auch einfach copy&pasten wenn ihr in den yaml modus geht wie im screenshot.

alias: "Drucker Start-Automatik "
description: >-
Man kann ein Datum und eine Zeit einstellen und zu diesem Zeitpunkt soll ein
3d druck starten.
trigger:
- platform: time
at: input_datetime.drucker_start_automatik
condition:
- condition: state
entity_id: input_boolean.drucker_start_automatik
state: "on"
action:
- if:
- condition: state
entity_id: input_select.printer_select
state: Kudo Bean
then:
- if:
- condition: state
entity_id: switch.kudo_bean_switch
state: "off"
then:
- service: switch.turn_on
data: {}
target:
entity_id: switch.kudo_bean_switch
- wait_for_trigger:
- platform: state
entity_id:
- input_select.bean_status
to: bereit
timeout:
hours: 0
minutes: 5
seconds: 0
milliseconds: 0
- service: shell_command.nanodlp_remote_start
data: {}
- if:
- condition: state
entity_id: input_select.bean_status
state: druckt
then:
- service: notify.mobile_app_daniel_phone
data:
title: Druck gestartet!
message: >-
Der automatische Druckerstart von Kudo Bean war erfolgreich. Der
Druck {{ states("input_select.nanodlp_files")}} ist
vorraussichtlich um     {{
states("sensor.nanodlp_finishingtime")}} fertig!
- if:
- condition: state
entity_id: input_select.printer_select
state: Anycubic
then:
- service: switch.turn_on
data: {}
target:
entity_id: switch.anycubic_switch
- delay:
hours: 0
minutes: 0
seconds: 10
milliseconds: 0
- service: switch.turn_on
data: {}
target:
entity_id: switch.octoprint_connect_to_printer
- delay:
hours: 0
minutes: 0
seconds: 10
milliseconds: 0
- if:
- condition: state
entity_id: binary_sensor.octoprint_connected
state: "on"
then:
- delay:
hours: 0
minutes: 0
seconds: 5
milliseconds: 0
- service: rest_command.octoprint_file_select
data: {}
- service: switch.turn_on
data: {}
target:
entity_id: switch.anycubic_lampe_switch
- wait_for_trigger:
- platform: numeric_state
entity_id: sensor.octoprint_print_progress
above: 0.1
- service: notify.mobile_app_daniel_phone
data:
title: Druck gestartet!
message: >-
Der automatische Druckerstart von Anycubic war erfolgreich. Der
Druck {{ states("input_select.octoprint_files")}} ist
vorraussichtlich um     {{
as_timestamp(states("sensor.octoprint_estimated_finish_time"))  |
timestamp_custom(' %H:%M:%S ')}} fertig!
mode: single

Auslöser ist die aktuelle Zeit. Wenn diese gleich dem Wert des Helfers ist, wird die automatisierung gestartet. Dann wird geprüft ob der Schalter für die aktivität überhaupt auf on steht. Wenn nicht, wird nichts weiter unternommen. Wenn doch wird der input_select für den Drucker befragt, welcher drucker denn starten soll. Zu NanoDLP kommen wir später.

Wenn Octoprint (hier Anycubic) ausgewählt ist, wird zunächst die Smarte Steckdose des 3D Druckers angeschaltet. Dann wird die Serielle verbindung zum Drucker hergestellt. Hier wird das Plug-In in Octoprint HomeAssistant discovery dafür benutzt. Ihr könnt aber auch einen zweiten Rest-Command aufbauen. Da muss bloß die URL und der Payload angepasst werden. Bischen Eigenleistung muss hier ja auch mal kommen 😀

Wenn der Status auf Verbunden wechselt, wird die Lampe auch noch angeschaltet und der Rest Command zur Auswahl der Datei und zum starten des Drucks gefeuert. Cool! Ich lasse mir dann bei Fertigstellung des Drucks noch eine zweite Automatisierung feuern, die mir einen Screenshot der Kamera in einer Push Notification am Handy zeigt und danach die Beleuchtung und den Drucker ausschalten.

Dashboard

Die Steuerelemente könnt ihr nach eigenem Belieben in eurem Dashboar einbauen. Hier ist meine Bastel-Keller seite:

NanoDLP

Das ganze funktioniert auch in NanoDLP. Die Entwickler haben hier eine Schnittstelle implementiert, die nicht mal mit einem Key abgesichert ist. Wir nutzen dazu einfach den HomeAssistant Shell Command, da wir den ganzen Overhead von REST nicht brauchen.

shell_command:
nanodlp_remote_start: 'curl -X POST http://192.168.178.68/printer/start/{{states("input_number.nanodlp_plate_id")}}'

Ihr seht schon, hier brauchen wir wieder einen Helfer – diesmal eine input_number. Bei NanoDLP geht es ja um Plate-IDs. Wie dieser angelegt wird, bekommt ihr jetzt wohl selbst hin. Oder ihr lest nochmal hier nach.

NodeRED

Damit wir auch hier wieder schön mit einem Dropdown und den echten Dateinamen arbeiten können, brauchen wir wieder einen input_select. Das auslesen der Dateiliste machen wir wieder mit NodeRED.

[{"id":"385009df65622055","type":"tab","label":"NanoDLP Plate Scrape","disabled":false,"info":"","env":[]},{"id":"74173939f562c333","type":"inject","z":"385009df65622055","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"59 23 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":290,"y":320,"wires":[["f5ef0e2a3a148583"]]},{"id":"f5ef0e2a3a148583","type":"http request","z":"385009df65622055","name":"GET Files","method":"GET","ret":"txt","paytoqs":"ignore","url":"http://192.168.178.68/plates","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[{"keyType":"User-Agent","keyValue":"","valueType":"Mozilla/5.0","valueValue":""}],"x":464.8000183105469,"y":318.8000183105469,"wires":[["38111a4d79d89407"]]},{"id":"37657fc59ccc2139","type":"debug","z":"385009df65622055","name":"debug 16","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":840,"y":280,"wires":[]},{"id":"38111a4d79d89407","type":"html","z":"385009df65622055","name":"scrape plates","property":"payload","outproperty":"payload","tag":"table > tbody > tr","ret":"html","as":"single","x":650,"y":320,"wires":[["37657fc59ccc2139","b4466b54d5971a2b"]]},{"id":"b4466b54d5971a2b","type":"function","z":"385009df65622055","name":"function 1","func":"msg.headers = msg.originalHeaders;\n\nvar input = msg.payload;\nvar plateID = [];\nvar plateName = [];\nmsg.payload = {};\n\nfor (var i = 0; i < input.length-2; i++) {\n    plateID.push(Number(input[i].split(\">\")[1].split(\"<\")[0].replace(/\\n/g, \" \")));\n    plateName.push(input[i].split(\">\")[4].split(\"<\")[0].replace(/\\n/g, \" \"));\n}\nmsg.payload.plateID = plateID;\nmsg.payload.names = plateName;\nmsg.topic = \"processed\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":840,"y":320,"wires":[["88d6e8a53044a40c","3a0b1218100fa814","8e9e4a1e458b7f8a"]]},{"id":"88d6e8a53044a40c","type":"debug","z":"385009df65622055","name":"debug 17","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1040,"y":280,"wires":[]},{"id":"3a0b1218100fa814","type":"function","z":"385009df65622055","name":"convert to comma seperated list","func":"msg.headers = msg.originalHeaders;\nvar names = msg.payload.names.join('\",\"').toString();\n\nmsg.payload = '{\"options\": [\"' +  names + '\"]}';\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1110,"y":320,"wires":[["98a463f437b53a42","6e1740a41a81b34d"]]},{"id":"98a463f437b53a42","type":"debug","z":"385009df65622055","name":"debug 18","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1320,"y":300,"wires":[]},{"id":"8e9e4a1e458b7f8a","type":"function","z":"385009df65622055","name":"compare to selection","func":"msg.headers = msg.originalHeaders;\nvar input = msg.payload;\n\nvar name;\nvar plateID;\nvar comp = global.get('homeassistant.homeAssistant.states[\"input_select.nanodlp_files\"].state');\nfor (var i = 0; i < input.names.length; i++) {\n    if (input.names[i] == comp ){\n        plateID = input.plateID[i];\n        name = input.names[i];\n    } \n}\n\nmsg.payload.name = name;\nmsg.payload.comp = comp;\nmsg.payload.plateID = plateID;\nmsg.topic = \"selection\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1080,"y":400,"wires":[["573f525896e3a0de","d1fa6e6d31a580cc"]]},{"id":"6e1740a41a81b34d","type":"json","z":"385009df65622055","name":"","property":"payload","action":"","pretty":false,"x":1330,"y":360,"wires":[["7856d3f06465a09f"]]},{"id":"573f525896e3a0de","type":"debug","z":"385009df65622055","name":"debug 19","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1340,"y":400,"wires":[]},{"id":"d1fa6e6d31a580cc","type":"api-call-service","z":"385009df65622055","name":"Store to Input Number","server":"c1eed9d5.7b0d28","version":5,"debugenabled":false,"domain":"input_number","service":"set_value","areaId":[],"deviceId":[],"entityId":["input_number.nanodlp_plate_id"],"data":"{\"value\":msg.payload.plateID}","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"all","x":1380,"y":440,"wires":[["5e0d8039b5cac438"]]},{"id":"7856d3f06465a09f","type":"api-call-service","z":"385009df65622055","name":"Store to Input Select","server":"c1eed9d5.7b0d28","version":5,"debugenabled":false,"domain":"input_select","service":"set_options","areaId":[],"deviceId":[],"entityId":["input_select.nanodlp_files"],"data":"payload","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"all","x":1520,"y":360,"wires":[["097aec8512c34713"]]},{"id":"5e0d8039b5cac438","type":"debug","z":"385009df65622055","name":"debug 20","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1560,"y":440,"wires":[]},{"id":"097aec8512c34713","type":"debug","z":"385009df65622055","name":"debug 21","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1760,"y":360,"wires":[]},{"id":"34304268f4705084","type":"poll-state","z":"385009df65622055","name":"Manual File Update","server":"c1eed9d5.7b0d28","version":2,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"updateinterval":"60","updateIntervalType":"num","updateIntervalUnits":"seconds","outputinitially":false,"outputonchanged":true,"entity_id":"input_boolean.nanodlp_update_files","state_type":"habool","halt_if":"true","halt_if_type":"bool","halt_if_compare":"is","outputs":2,"x":290,"y":180,"wires":[["e7740e8091cc61be"],["e7740e8091cc61be"]]},{"id":"e7740e8091cc61be","type":"trigger","z":"385009df65622055","name":"","op1":"1","op2":"0","op1type":"str","op2type":"str","duration":"0","extend":false,"overrideDelay":false,"units":"ms","reset":"false","bytopic":"all","topic":"topic","outputs":1,"x":500,"y":180,"wires":[["f5ef0e2a3a148583"]]},{"id":"67c7831412bce85d","type":"poll-state","z":"385009df65622055","name":"Print Automatic On","server":"c1eed9d5.7b0d28","version":2,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"updateinterval":"60","updateIntervalType":"num","updateIntervalUnits":"seconds","outputinitially":false,"outputonchanged":true,"entity_id":"input_boolean.drucker_start_automatik","state_type":"habool","halt_if":"true","halt_if_type":"bool","halt_if_compare":"is","outputs":2,"x":270,"y":420,"wires":[["d8686a115c5079d0"],["d8686a115c5079d0"]]},{"id":"d8686a115c5079d0","type":"trigger","z":"385009df65622055","name":"","op1":"1","op2":"0","op1type":"str","op2type":"str","duration":"0","extend":false,"overrideDelay":false,"units":"ms","reset":"false","bytopic":"all","topic":"topic","outputs":1,"x":500,"y":420,"wires":[["f5ef0e2a3a148583"]]},{"id":"c1eed9d5.7b0d28","type":"server","name":"Home Assistant","addon":true}]

Flow

Auch hier will ich wieder kurz auf die Entwicklung des Flow eingehen.

Wir haben wieder drei Modi zum Auslösen. Der Injection-Node mit dem Intervall täglich um Mitternacht, dann die aktivierung der Startautomatik und manuelle Aktualisierung durch einen input_boolean.

Wir holen uns dann einfach die gesamte Plate-Website von unserer NanoDLP instanz mit einem GET Request.

Mit dem HTML Bearbeiten Node filtern wir die Tabelle nach ihren Reihen in dem wir den folgenden Selektor benutzen:

table > tbody > tr

Das gibt uns ein Array zurück mit dem Code von jeder Zeile. Hier können wir mit einem Function-Node aufräumen. Wir wollen ja nur die ID und den Namen wissen. Man kann aber auch noch weitere Interessante Infos rausziehen. (Resinverbauch und Kosten, Letzter Druck, Laufzeit).

msg.headers = msg.originalHeaders;
var input = msg.payload;
var plateID = [];
var plateName = [];
msg.payload = {};
for (var i = 0; i < input.length-2; i++) {
plateID.push(Number(input[i].split(">")[1].split("<")[0].replace(/\n/g, " ")));
plateName.push(input[i].split(">")[4].split("<")[0].replace(/\n/g, " "));
}
msg.payload.plateID = plateID;
msg.payload.names = plateName;
msg.topic = "processed";
return msg;

Im Detail: Ich lege mir Variablen für den Namen und die ID an, leere dann den Payload in dem ich ein leeres Objekt überschreibe. Dann kommt eine for Schleife. Die Zählt die Elemente aus payload (jetzt in input gespeichert) durch und zerstückelt den String an bestimmten Stellen. Ich habe hier die für die typischen html Tags genutzten eckigen Klammern als Seperator genommen. Der Index in den Eckigen klammern sagt an, welches Stück vom getrennten String weitergenutzt werden soll. Das ganze wird dann an den Array angehängt.

Der Rest ist 1:1 wie im Flow von Octoprint nur mit ausgetauschten Helfern. Wir machen eine Liste, pushen die in den input_select. Dann können wir mit der Auswahl im Dropdown vergleichen, die ID rausziehen und in den Startbefehl pushen. Easy 🙂

Automatisierung

In der Automatisierung oben am Ende von Octoprint ist der Kram für NanoDLP ja schon drin. Auch hier wird eine Lampe gesteuert. Wenn die Startautomatik erfolgreich war, wird ebenfalls eine Pushnotification an mein Smartphone geschickt.

Ich habe noch zwei Automatisierungen, die mir einen Screenshot der jeweiligen Kamera in einer Pushnotification durchgeben, sobald der Druck fertig ist.

Happy Printing!

2 Kommentare

Schreibe einen Kommentar

Geb mir einen aus :)

Wenn du das Zeug hier magst, denk doch über eine Spende nach um Server und Domain zu finanzieren.

$ Die mit einem $ gekennzeichneten Links, sind Affiliate Links. Wenn du über diese in den Shop gelangst und etwas kaufst, bekomme ich eine kleine Provision.

Suche & Filter