Klipper in HomeAssistant einbinden
Man kann Klipper ganz einfach auch in sein Smart Home einbinden. Mit HomeAssistant und einem RestSensor können wir die integrierte Web API benutzen um Informationen zu sammeln. Außerdem können wir mit ein paar Requests auch Klipper steuern.
TL;DR
Wenn ihr nicht wissen wollt, wie man hier zum Ziel kommt, sondern nur den Code braucht – Here you go:
Mit strg + f könnt ihr im Home Assistant File Editor auch suchen und ersetzen und dann „ultimaker“ durch euren Druckernamen ersetzen.
input_boolean:
ultimaker_pause:
name: Ultimaker Pause
camera:
- platform: generic
name: "Ultimaker Thumbnail"
still_image_url: http://192.168.178.120:7125/server/files/gcodes/{{ states("sensor.ultimaker_object_thumbnails") }}
verify_ssl: false
#infos aktueller druck
rest:
scan_interval: 5
resource_template: "http://192.168.178.120:7125/server/files/metadata?filename={{ states(('sensor.ultimaker_current_print')) }}"
sensor:
- name: ultimaker_file_metadata
json_attributes_path: "$.result"
json_attributes:
- layer_height
- object_height
- thumbnails
value_template: "OK"
#allgemeine printer info
sensor:
- platform: rest
name: ultimaker
resource: "http://192.168.178.120:7125/printer/objects/query?extruder&extruder1&print_stats&toolhead&display_status&virtual_sdcard&heaters&mcu"
json_attributes_path: "$.result.status"
json_attributes:
- heaters
- mcu
- extruder
- extruder1
- print_stats
- toolhead
- display_status
- virtual_sdcard
value_template: >-
{{ 'OK' if ('result' in value_json) else None }}
- platform: rest
name: ultimaker_info
scan_interval: 1
resource_template: "http://192.168.178.120:7125/printer/info"
json_attributes_path: "$.result"
json_attributes:
- state_message
- state
value_template: "{{ 'OK' if ('result' in value_json) else None }}"
- platform: rest
scan_interval: 15
name: ultimaker_preview_path
resource_template: "http://192.168.178.120
:7125/server/files/metadata?filename={{ states(('sensor.ultimaker_current_print')) }}"
json_attributes_path: "$.result.thumbnails.[1]"
json_attributes:
- relative_path
- width
- height
- size
value_template: "{{ 'OK' if ('result' in value_json) else None }}"
template:
sensor:
- name: ultimaker_print_preview
unique_id: "ultimaker-print-preview"
state: >
{{ ['http://192.168.178.120/server/files/gcodes/.thumbs/', (states.sensor.ultimaker.attributes['print_stats'].filename | replace('.gcode', '-400x300.png') | replace(' ', '%20') | string)] | join if is_state('sensor.ultimaker', 'OK') else None }}"
- name: ultimaker_hotend0_actual
unique_id: "ultimaker-hotend0-actual"
device_class: temperature
unit_of_measurement: '°C'
state: "{{ states.sensor.ultimaker.attributes['extruder'].temperature | float | round(1) if is_state('sensor.ultimaker', 'OK') else None }}"
icon: mdi:thermometer
- name: ultimaker_hotend0_target
unique_id: "ultimaker-hotend0-target"
device_class: temperature
unit_of_measurement: '°C'
state: "{{ states.sensor.ultimaker.attributes['extruder'].target | float | round(1) if is_state('sensor.ultimaker', 'OK') else None }}"
icon: mdi:thermometer
- name: ultimaker_hotend1_actual
unique_id: "ultimaker-hotend1-actual"
device_class: temperature
unit_of_measurement: '°C'
state: "{{ states.sensor.ultimaker.attributes['extruder1'].temperature | float | round(1) if is_state('sensor.ultimaker', 'OK') else None }}"
icon: mdi:thermometer
- name: ultimaker_hotend1_target
unique_id: "ultimaker-hotend1-target"
device_class: temperature
unit_of_measurement: '°C'
state: "{{ states.sensor.ultimaker.attributes['extruder1'].target | float | round(1) if is_state('sensor.ultimaker', 'OK') else None }}"
icon: mdi:thermometer
- name: ultimaker_state
state: "{{ states.sensor.ultimaker.attributes['print_stats'].state if is_state('sensor.ultimaker', 'OK') else 'Offline' }}"
icon: >
{% set val = states.sensor.ultimaker.attributes["print_stats"]["state"] %}
{% if val == 'standby' %}
mdi:sleep
{% elif val == 'error' %}
mdi:alert-circle
{% elif val == 'printing' %}
mdi:printer-3d-nozzle
{% elif val == 'paused' %}
mdi:pause-circle
{% elif val == 'complete' %}
mdi:printer-3d
{% else %}
mdi:help-circle
{% endif %}
- name: ultimaker_current_print
state: "{{ states.sensor.ultimaker.attributes['print_stats'].filename if is_state('sensor.ultimaker', 'OK') else None }}"
icon: mdi:file
- name: ultimaker_current_progress
unit_of_measurement: '%'
icon: mdi:file-percent
state: "{{ (states.sensor.ultimaker.attributes['display_status'].progress * 100) | round(1) if is_state('sensor.ultimaker', 'OK') else None }}"
- name: ultimaker_print_time
state: "{{ states.sensor.ultimaker.attributes['print_stats'].print_duration | timestamp_custom('%H:%M:%S', 0) if is_state('sensor.ultimaker', 'OK') else None }}"
icon: mdi:progress-clock
- name: ultimaker_time_remaining
icon: mdi:clock-end
state: "{{ (((states.sensor.ultimaker.attributes['print_stats'].print_duration / states.sensor.ultimaker.attributes['display_status'].progress - states.sensor.ultimaker.attributes['print_stats'].print_duration) if states.sensor.ultimaker.attributes['display_status'].progress > 0 else 0) | timestamp_custom('%H:%M:%S', 0)) if is_state('sensor.ultimaker', 'OK') else None }}"
- name: ultimaker_eta
icon: mdi:clock-outline
state: "{{ (as_timestamp(now()) + 2 * 60 * 60 + ((states.sensor.ultimaker.attributes['print_stats'].print_duration / states.sensor.ultimaker.attributes['display_status'].progress - states.sensor.ultimaker.attributes['print_stats'].print_duration) if states.sensor.ultimaker.attributes['display_status'].progress > 0 else 0)) | timestamp_custom('%H:%M:%S', 0) if is_state('sensor.ultimaker', 'OK') else None }}"
- name: ultimaker_nozzletemp0
icon: mdi:thermometer
state: "{{ [(states.sensor.ultimaker.attributes['extruder'].temperature | float | round(1) | string), ' / ', (states.sensor.ultimaker.attributes['extruder'].target | float | round(1) | string)] | join if is_state('sensor.ultimaker', 'OK') else None }}"
- name: ultimaker_nozzletemp1
icon: mdi:thermometer
state: "{{ [(states.sensor.ultimaker.attributes['extruder1'].temperature | float | round(1) | string), ' / ', (states.sensor.ultimaker.attributes['extruder1'].target | float | round(1) | string)] | join if is_state('sensor.ultimaker', 'OK') else None }}"
- name: ultimaker_message
unique_id: "ultimaker_message"
state: '{{ states.sensor.ultimaker.attributes["display_status"]["message"] if is_state("sensor.ultimaker_info", "OK") else None }}'
icon: mdi:message-cog
- name: ultimaker_layer_height
unique_id: "ultimaker_layerheight"
state: '{{ states.sensor.ultimaker_file_metadata.attributes["layer_height"] | float(0) if is_state("sensor.ultimaker_file_metadata", "OK") else None }}'
unit_of_measurement: "mm"
icon: mdi:arrow-collapse-down
- name: ultimaker_object_height
unique_id: "<moonraker-ip-address>6d6d9dc0-9a02-4ce4-a797-c84b42e011a6"
state: '{{ (states.sensor.ultimaker_file_metadata.attributes["object_height"] | float(0)) - (states.sensor.ultimaker_file_metadata.attributes["layer_height"] | float(0)) if is_state("sensor.ultimaker_file_metadata", "OK") else None }}'
unit_of_measurement: "mm"
icon: mdi:arrow-expand-vertical
- name: ultimaker_current_height
unique_id: "ultimaker_current_height"
state: '{{ states.sensor.ultimaker.attributes["gcode_move"]["gcode_position"][2] | float(0) | round(2) if is_state("sensor.ultimaker_file_metadata", "OK") else None }}'
unit_of_measurement: "mm"
icon: mdi:arrow-collapse-down
- name: ultimaker_current_layer
unique_id: "ultimaker_current_layer"
state: '{{ (states("sensor.ultimaker_current_height")|float(0) / states("sensor.ultimaker_layer_height")|float(0))|round(0) if is_state("sensor.ultimaker_file_metadata", "OK") else None }}'
icon: mdi:counter
- name: ultimaker_total_layers
unique_id: "ultimaker_total_layers"
state: '{{ (states("sensor.ultimaker_object_height")|float(0) / states("sensor.ultimaker_layer_height")|float(0))|round(0) if is_state("sensor.ultimaker_file_metadata", "OK") else None }}'
icon: mdi:counter
- name: ultimaker_object_thumbnails
unique_id: "ultimaker_object_thumbnails"
state: '{{ states.sensor.ultimaker_file_metadata.attributes["thumbnails"][1]["relative_path"] if is_state("sensor.ultimaker_file_metadata", "OK") else None }}'
unit_of_measurement: "mm"
icon: mdi:image
- name: ultimaker_state_message
unique_id: "ultimaker_state_message"
state: '{{ states.sensor.ultimaker_info.attributes["state_message"] if is_state("sensor.ultimaker_info", "OK") else None }}'
icon: mdi:message-cog
rest_command:
ultimaker_emergency_stop:
url: "http://192.168.178.120:7125/printer/emergency_stop"
method: post
ultimaker_firmware_restart:
url: "http://192.168.178.120:7125/printer/firmware_restart"
method: post
ultimaker_cancel:
url: "http://192.168.178.120:7125/printer/print/cancel"
method: post
ultimaker_pause:
url: "http://192.168.178.120:7125/printer/print/pause"
method: post
ultimaker_resume:
url: "http://192.168.178.120:7125/printer/print/resume"
method: post
switch:
- platform: template
switches:
ultimaker_pause:
value_template: "{{ states('input_boolean.ultimaker_pause') }}"
unique_id: switch.ultimaker_pause
turn_on:
- service: rest_command.ultimaker_pause
- service: input_boolean.toggle
target:
entity_id: input_boolean.ultimaker_pause
turn_off:
- service: rest_command.ultimaker_resume
- service: input_boolean.toggle
target:
entity_id: input_boolean.ultimaker_pause
Klipper Web API
In der Doku von Klipper gibt es eine Seite zur Web API. Hier Suchen wir den Abschnitt für die verfügbaren Objekte:

Der gesuchte Befehl lautet also:
/printer/objects/list
Dies müssen wir einfach hinter die IP Adresse von unserem Klipper-Web-Interface setzen um einen vollständigen HTTP-Request zu formen. Also
http://192.168.178.120/printer/objects/list
Wobei ihr eure IP eingeben müsst. GGf funktioniert auch der vergebene Druckername mit dem Zusatz local. Bei mir:
ultimaker.local/printer/object/list
Nun bekommen wir im Browser ein unformatiertes JSON Objekt zurück, das wir einfach komplett markieren und kopieren. In diesem JSON-Objekt sind jetzt alle möglichen Infos zu unseren Drucker-Objekten. Das kann man jetzt natürlich schlecht lesen, daher wollen wir das zunächst mal formatieren. Daher das kopieren.

Update: Man kann oben jetzt auch einfach Formatiert anzeigen lassen in Chrome mit einem Häkchen bei Quelltextformatierung:

JSON Formatieren
Ich nutze dazu einfach Visual Studio Code, lege eine neue Datei an und füge den Code ein. Dann gehe ich einfach mit einem Rechtsklick ins Kontextmenü und klicke auf Format Document.

Nun klappt das Direkt oder es wird kurz gemeckert, dass kein Parser vorhanden. Dann wird aber direkt JSON Accent oder so ähnlich vorgeschlagen.

Zack, alles lesbar. Jetzt können wir überlegen, was wir alles in Home Assistant übertragen wollen.
API Request erstellen
In der Doku etwas weiter unten ist dann die eigentliche Abfrage der WebAPI die wir brauchen:
/printer/objects/query?gcode_move&toolhead&extruder=target,temperature

Und hier seht ihr, das hinter dem Anfang bis zu ? einfach die Objektnamen mit einem & verbunden sind. Also schreiben wir einfach alle Objekte die wir abfragen möchten so zusammen. Die genauen Bezeichnungen kopieren wir uns aus dem JSON Objekt mit den verfügbaren Objekten. Solltet ihr Leerzeichen haben, müssen diese mit %20 ersetzt werden.
Bei mir sieht das dann so aus:
http://192.168.178.120:7125/printer/objects/query?extruder&extruder1&print_stats&toolhead&display_status&virtual_sdcard&heaters&mcu
Das können wir im Browser auch mal testen:

Bzw. Formatiert:

Rest Sensor
Nun wollen wir diese Abfrage ja automatisiert durchführen. Dazu nutzen wir einen Rest-Sensor. In eurer configuration.yaml von Home Assistant muss also ein entsprechender Sensor angelegt werden. Entweder direkt in der configuration.yaml an der richtigen Stelle (sensor) oder aber, falls ihr wie ich Packages nutzt, empfehle ich eins hierfür anzulegen.
sensor:
- platform: rest
name: ultimaker
resource: "http://192.168.178.120:7125/printer/objects/query?extruder&extruder1&print_stats&toolhead&display_status&virtual_sdcard&heaters&mcu"
json_attributes_path: "$.result.status"
json_attributes:
- heaters
- mcu
- extruder
- extruder1
- print_stats
- toolhead
- display_status
- virtual_sdcard
value_template: >-
{{ 'OK' if ('result' in value_json) else None }}
Mehr zu Packages in meinem Beitrag hier.
Den Namen und name: könnt ihr frei wählen.
Bei resource: kommt unsere URL für die Objekte rein.
Die JSON Attributes sind noch mal die einzelnen Objekte aufgeführt, damit das korrekt verarbeitet wird.
Im value_template legen wir nur einen Status fest, welcher verarbeitet wird, falls wir keine response bekommen, z.B. wenn Klipper offline ist.
Template Sensoren
Als nächstes erstellen wir für jedes Objekt einen eigenen Template Sensor, damit wir leichter mit den Infos weiterarbeiten können.
Dazu also wieder an der richtigen Stelle die Zeilen einfügen:
- platform: template
sensors:
ultimaker_hotend0_actual:
friendly_name: 'Hotend'
device_class: temperature
unit_of_measurement: '°C'
value_template: "{{ states.sensor.ultimaker.attributes['extruder'].temperature | float | round(1) if is_state('sensor.ultimaker', 'OK') else None }}"
Das ist jetzt nur einer von vielen. Ihr könnt für jedes der Objekte einen Sensor erstellen, siehe CodeBlock unter TL;DR

Dashboard
Und zu guter letzt, kann man die ganzen Sensoren dann in ein Dashboard packen. Das ist aber euch überlassen.
Bei mir schaut das so aus, ist aber noch WIP:

Bzw. als RAW:
views:
- title: Ultimaker
path: ultimaker
icon: mdi:clippy
cards:
- type: vertical-stack
cards:
- type: glance
entities:
- entity: sensor.ultimaker_hotend0_actual
- entity: sensor.ultimaker_hotend1_actual
- entity: sensor.ultimaker_current_progress
- type: horizontal-stack
cards:
- show_name: true
show_icon: true
type: button
tap_action:
action: toggle
entity: switch.ultimakerplug_switch
icon: mdi:power-plug
icon_height: 50px
name: Ultimaker
show_state: true
- show_name: true
show_icon: true
type: button
tap_action:
action: toggle
entity: switch.werkbank_switch
icon: mdi:desk-lamp
name: Licht
show_state: true
icon_height: 50px
- show_state: true
show_name: true
camera_view: auto
type: picture-entity
entity: camera.ultimaker_mjpg
name: Ultimaker
camera_image: camera.ultimaker_mjpg
- type: entities
entities:
- entity: sensor.ultimaker_eta
- entity: sensor.ultimaker_state
- entity: sensor.ultimaker_current_progress
- entity: sensor.ultimaker_print_time
- entity: sensor.ultimaker_time_remaining
- entity: sensor.ultimaker
- entity: sensor.ultimaker_current_print
- entity: sensor.ultimaker_hotend0_target
- entity: sensor.ultimaker_hotend1_target
- graph: line
type: sensor
entity: sensor.ultimaker_hotend0_actual
detail: 1
name: Düse 1
unit: °C
- graph: line
type: sensor
entity: sensor.ultimaker_hotend1_actual
detail: 1
name: Düse 2
unit: °C
Kamerafeed
Bei mir seht ihr auch einen Camera Feed, der aber aktuell nix anzeigt. Das kommt weil das Licht aus ist und man nix sieht.
Diesen kann man eigentlich auch mit YAML anlegen, da gab es aber Probleme kürzlich, daher hab ich das übers GUI gemacht. Wir nutzen hier eine MJPEG Kamera unter Integrationen:


Diese hab ich mit den zwei Links für MJPEG URL und Standbild festgelegt. Die zwei Felder Password und Benutzername hab ich leer gelassen.
http://192.168.178.120/webcam/?action=stream #Stream
http://192.168.178.120/webcam/?action=snapshot #Standbild
Drucker Fernsteuern
Jetzt kommt der Spannende Teil. Wir können auch aus HomeAssistant heraus den Drucker Fernsteuern. Dazu gibt es Steuerbefehle, die per WebAPI gesendet werden können. Ich zeig euch hier Pause, Resume, Cancel und Start. Die ersten Drei sind denkbar einfach: Einfach IP und request:
/printer/print/pause
/printer/print/resume
/printer/print/cancel
Bzw. angepasst dann so:
http://192.168.178.120/printer/print/pause
http://192.168.178.120/printer/print/resume
http://192.168.178.120/printer/print/cancel
Die packen wir z.B. in ein Script oder einen Command Line switch mit einem Curl Post Befehl, oder einfach als rest-command:
rest_command:
ultimaker_emergency_stop:
url: "http://192.168.178.120:7125/printer/emergency_stop"
method: post
ultimaker_firmware_restart:
url: "http://192.168.178.120:7125/printer/firmware_restart"
method: post
ultimaker_cancel:
url: "http://192.168.178.120:7125/printer/print/cancel"
method: post
ultimaker_pause:
url: "http://192.168.178.120:7125/printer/print/pause"
method: post
ultimaker_resume:
url: "http://192.168.178.120:7125/printer/print/resume"
method: post
switch:
- platform: template
switches:
ultimaker_pause:
value_template: "{{ states('input_boolean.ultimaker_pause') }}"
unique_id: switch.ultimaker_pause
turn_on:
- service: rest_command.ultimaker_pause
- service: input_boolean.toggle
target:
entity_id: input_boolean.ultimaker_pause
turn_off:
- service: rest_command.ultimaker_resume
- service: input_boolean.toggle
target:
entity_id: input_boolean.ultimaker_pause

Damit dies aber auch in Klipper funktionieren kann, müssen wir in der printer.cfg im Klipper den abschnitt pause_resume haben:
[pause_resume]
recover_velocity: 50
Druck starten
Um den Druck zu starten braucht man natürlich auch einen Dateinamen. Der Befehl lautet erstmal wie folgt:
/printer/print/start?filename=test_print.gcode
Also umgesetzt auf meinen Drucker z.B. so:
http://192.168.178.120/printer/print/start?filename=UMODE_3DBenchy.gcode
Integration in meine Start-Automatik
Das schreit ja geradezu nach einer Integration in meine Drucker Start Automatik.
Wir können das also wie folgt umsetzen. Zunächst müssen wir die G-Code Files in Klippers Dateisystem auslesen. Das geht laut Doku mit der folgenden Abfrage:
/server/files/list?root={root_folder}
Also angepasst wieder:
http://192.168.178.120/server/files/list
Den root Teil ignoriere ich, da der Default der Ordner gcode ist und das passt bei mir schon. Daher gehe ich mit diesem Befehl in meinen Node-Red flow und aktualisiere den Datenupdate-Part. Ich füge also einen Get-Node hinzu und füge die URL ein. Header Brauchen wir keine. Rechts im Debug-Window seht ihr auch schon den Antwort-JSON-String.

Diesen wandle ich in ein JSON Objekt um verarbeite das ganze in mehreren Funktions-Nodes um am Ende den Datei Pfad bzw. hier einfach den Namen in einen Input-Select zu schieben.

[{"id":"1e9078822c9d1826","type":"tab","label":"Ultimaker Autostart","disabled":false,"info":"","env":[]},{"id":"e4cea3de5791f417","type":"inject","z":"1e9078822c9d1826","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"59 23 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":150,"y":300,"wires":[["b293ddfc24867237"]]},{"id":"b293ddfc24867237","type":"http request","z":"1e9078822c9d1826","name":"GET Files Ultimaker (Moonraker)","method":"GET","ret":"txt","paytoqs":"ignore","url":"http://192.168.178.120/server/files/list","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":440,"y":300,"wires":[["d6fd18619559538b"]]},{"id":"d6fd18619559538b","type":"json","z":"1e9078822c9d1826","name":"","property":"payload","action":"","pretty":false,"x":650,"y":300,"wires":[["b42fd86335f9d56e"]]},{"id":"9e1f547cac1a8509","type":"function","z":"1e9078822c9d1826","name":"convert to comma seperated list","func":"var names = msg.payload.paths.join('\",\"').toString();\n\nmsg.payload = '{\"options\": [\"' + names + '\"]}';\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":990,"y":300,"wires":[["ead9c0dcea454ef4"]]},{"id":"ead9c0dcea454ef4","type":"json","z":"1e9078822c9d1826","name":"","property":"payload","action":"","pretty":false,"x":1210,"y":300,"wires":[["0a634086e1ae0175"]]},{"id":"b42fd86335f9d56e","type":"function","z":"1e9078822c9d1826","name":"get paths","func":"msg.headers = msg.originalHeaders;\nvar input = msg.payload;\n\nvar names = [];\nvar paths = [];\nvar origin = [];\nfor (var i = 0; i < msg.payload.result.length; i++) {\n paths.push(input.result[i].path);\n}\nmsg.payload = {};\nmsg.payload.paths = paths;\nmsg.topic = \"output\";\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":780,"y":300,"wires":[["9e1f547cac1a8509"]]},{"id":"0a634086e1ae0175","type":"api-call-service","z":"1e9078822c9d1826","name":"Store to Input Select","server":"c1eed9d5.7b0d28","version":5,"debugenabled":false,"domain":"input_select","service":"set_options","areaId":[],"deviceId":[],"entityId":["input_select.ultimaker_files"],"data":"payload","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"all","x":1400,"y":300,"wires":[["012ad2434a03aa44"]]},{"id":"012ad2434a03aa44","type":"debug","z":"1e9078822c9d1826","name":"debug 22","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1600,"y":300,"wires":[]},{"id":"b42bf5092c1e904c","type":"poll-state","z":"1e9078822c9d1826","name":"Manual File Update","server":"c1eed9d5.7b0d28","version":3,"exposeAsEntityConfig":"","updateInterval":"60","updateIntervalType":"num","updateIntervalUnits":"seconds","outputInitially":false,"outputOnChanged":true,"entityId":"input_boolean.ultimaker_update_files","stateType":"habool","ifState":"true","ifStateType":"bool","ifStateOperator":"is","outputs":2,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":210,"y":420,"wires":[["b293ddfc24867237"],[]]},{"id":"1f75f250a8af2a43","type":"poll-state","z":"1e9078822c9d1826","name":"Print Automatic On","server":"c1eed9d5.7b0d28","version":3,"exposeAsEntityConfig":"","updateInterval":"60","updateIntervalType":"num","updateIntervalUnits":"seconds","outputInitially":false,"outputOnChanged":true,"entityId":"input_boolean.ultimaker_autostart","stateType":"habool","ifState":"true","ifStateType":"bool","ifStateOperator":"is","outputs":2,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":210,"y":500,"wires":[["b293ddfc24867237"],[]]},{"id":"c1eed9d5.7b0d28","type":"server","name":"Home Assistant","addon":true}]
Dann steuere ich das ganze über ein paar Helfer und eine Automation. Trigger ist die aktuelle Zeit. Wenn diese dem Wert aus der Start-Automatik entspricht und diese per Input-Boolean aktiviert ist, werden die Start befehle an Klipper geschickt. Also der Plug für den Drucker und das Licht geht an, und der Rest-Command mit dem Startbefehl wird ausgeführt. Dann gibts noch eine Nachricht aufs Handy.
Hier die Automation:
alias: Drucker Autostart Ultimaker
description: >-
Man kann ein Datum und eine Zeit einstellen und zu diesem Zeitpunkt soll ein
3d druck starten.
trigger:
- platform: time
at: input_datetime.ultimaker_autostart
condition:
- condition: state
entity_id: input_boolean.ultimaker_autostart
state: "on"
action:
- if:
- condition: state
state: "on"
entity_id: input_boolean.ultimaker_autostart
then:
- service: switch.turn_on
data: {}
target:
entity_id: switch.ultimakerplug_switch
- delay:
hours: 0
minutes: 0
seconds: 10
milliseconds: 0
- service: switch.turn_on
data: {}
target:
entity_id: switch.werkbank_switch
- service: rest_command.utlimaker_start_print
data: {}
- wait_for_trigger:
- platform: state
entity_id:
- sensor.ultimaker_state
to: printing
- service: notify.mobile_app_daniel_phone
data:
title: Druck gestartet!
message: >-
Der automatische Druckerstart von Ultimaker via Klipper war
erfolgreich. Der Druck {{ states("input_select.ultimaker_files")}}
ist vorraussichtlich um {{
as_timestamp(states("sensor.ultimaker_eta")) | timestamp_custom('
%H:%M:%S ')}} fertig!
image: http://http://192.168.178.120/webcam/?action=snapshot
mode: single
Und hier die erweiterte YAML.
input_boolean:
ultimaker_autostart:
name: Ultimaker Start Automatik
initial: off
ultimaker_update_files:
name: Ultimaker Update Files
initial: off
ultimaker_pause:
name: Ultimaker Pause
initial: off
input_select:
ultimaker_files:
name: Ultimaker Files
options:
- none
initial: none
input_datetime:
ultimaker_autostart:
name: Ultimaker Autostart
has_date: true
has_time: true
# camera:
# name: "Ultimaker Thumbnail"
# still_image_url: http://192.168.178.120:7125/server/files/gcodes/{{ states("sensor.ultimaker_object_thumbnails") }}
# verify_ssl: false
#infos aktueller druck
rest:
scan_interval: 5
resource_template: "http://192.168.178.120:7125/server/files/metadata?filename={{ states(('sensor.ultimaker_current_print')) }}"
sensor:
- name: ultimaker_file_metadata
json_attributes_path: "$.result"
json_attributes:
- layer_height
- object_height
- thumbnails
value_template: "OK"
#allgemeine printer info
sensor:
- platform: rest
name: ultimaker
resource: "http://192.168.178.120:7125/printer/objects/query?extruder&extruder1&print_stats&toolhead&display_status&virtual_sdcard&heaters&mcu"
json_attributes_path: "$.result.status"
json_attributes:
- heaters
- mcu
- extruder
- extruder1
- print_stats
- toolhead
- display_status
- virtual_sdcard
value_template: >-
{{ 'OK' if ('result' in value_json) else None }}
- platform: rest
name: ultimaker_info
scan_interval: 1
resource_template: "http://192.168.178.120:7125/printer/info"
json_attributes_path: "$.result"
json_attributes:
- state_message
- state
value_template: "{{ 'OK' if ('result' in value_json) else None }}"
- platform: rest
scan_interval: 15
name: ultimaker_preview_path
resource_template: "http://192.168.178.120
:7125/server/files/metadata?filename={{ states(('sensor.ultimaker_current_print')) }}"
json_attributes_path: "$.result.thumbnails.[1]"
json_attributes:
- relative_path
- width
- height
- size
value_template: "{{ 'OK' if ('result' in value_json) else None }}"
template:
sensor:
- name: ultimaker_print_preview
unique_id: "ultimaker-print-preview"
state: >
{{ ['http://192.168.178.120/server/files/gcodes/.thumbs/', (states.sensor.ultimaker.attributes['print_stats'].filename | replace('.gcode', '-400x300.png') | replace(' ', '%20') | string)] | join if is_state('sensor.ultimaker', 'OK') else None }}"
- name: ultimaker_hotend0_actual
unique_id: "ultimaker-hotend0-actual"
device_class: temperature
unit_of_measurement: '°C'
state: "{{ states.sensor.ultimaker.attributes['extruder'].temperature | float | round(1) if is_state('sensor.ultimaker', 'OK') else None }}"
icon: mdi:thermometer
- name: ultimaker_hotend0_target
unique_id: "ultimaker-hotend0-target"
device_class: temperature
unit_of_measurement: '°C'
state: "{{ states.sensor.ultimaker.attributes['extruder'].target | float | round(1) if is_state('sensor.ultimaker', 'OK') else None }}"
icon: mdi:thermometer
- name: ultimaker_hotend1_actual
unique_id: "ultimaker-hotend1-actual"
device_class: temperature
unit_of_measurement: '°C'
state: "{{ states.sensor.ultimaker.attributes['extruder1'].temperature | float | round(1) if is_state('sensor.ultimaker', 'OK') else None }}"
icon: mdi:thermometer
- name: ultimaker_hotend1_target
unique_id: "ultimaker-hotend1-target"
device_class: temperature
unit_of_measurement: '°C'
state: "{{ states.sensor.ultimaker.attributes['extruder1'].target | float | round(1) if is_state('sensor.ultimaker', 'OK') else None }}"
icon: mdi:thermometer
- name: ultimaker_state
state: "{{ states.sensor.ultimaker.attributes['print_stats'].state if is_state('sensor.ultimaker', 'OK') else 'Offline' }}"
icon: >
{% set val = states.sensor.ultimaker.attributes["print_stats"]["state"] %}
{% if val == 'standby' %}
mdi:sleep
{% elif val == 'error' %}
mdi:alert-circle
{% elif val == 'printing' %}
mdi:printer-3d-nozzle
{% elif val == 'paused' %}
mdi:pause-circle
{% elif val == 'complete' %}
mdi:printer-3d
{% else %}
mdi:help-circle
{% endif %}
- name: ultimaker_current_print
state: "{{ states.sensor.ultimaker.attributes['print_stats'].filename if is_state('sensor.ultimaker', 'OK') else None }}"
icon: mdi:file
- name: ultimaker_current_progress
unit_of_measurement: '%'
icon: mdi:file-percent
state: "{{ (states.sensor.ultimaker.attributes['display_status'].progress * 100) | round(1) if is_state('sensor.ultimaker', 'OK') else None }}"
- name: ultimaker_print_time
state: "{{ states.sensor.ultimaker.attributes['print_stats'].print_duration | timestamp_custom('%H:%M:%S', 0) if is_state('sensor.ultimaker', 'OK') else None }}"
icon: mdi:progress-clock
- name: ultimaker_time_remaining
icon: mdi:clock-end
state: "{{ (((states.sensor.ultimaker.attributes['print_stats'].print_duration / states.sensor.ultimaker.attributes['display_status'].progress - states.sensor.ultimaker.attributes['print_stats'].print_duration) if states.sensor.ultimaker.attributes['display_status'].progress > 0 else 0) | timestamp_custom('%H:%M:%S', 0)) if is_state('sensor.ultimaker', 'OK') else None }}"
- name: ultimaker_eta
icon: mdi:clock-outline
state: "{{ (as_timestamp(now()) + 2 * 60 * 60 + ((states.sensor.ultimaker.attributes['print_stats'].print_duration / states.sensor.ultimaker.attributes['display_status'].progress - states.sensor.ultimaker.attributes['print_stats'].print_duration) if states.sensor.ultimaker.attributes['display_status'].progress > 0 else 0)) | timestamp_custom('%H:%M:%S', 0) if is_state('sensor.ultimaker', 'OK') else None }}"
- name: ultimaker_nozzletemp0
icon: mdi:thermometer
state: "{{ [(states.sensor.ultimaker.attributes['extruder'].temperature | float | round(1) | string), ' / ', (states.sensor.ultimaker.attributes['extruder'].target | float | round(1) | string)] | join if is_state('sensor.ultimaker', 'OK') else None }}"
- name: ultimaker_nozzletemp1
icon: mdi:thermometer
state: "{{ [(states.sensor.ultimaker.attributes['extruder1'].temperature | float | round(1) | string), ' / ', (states.sensor.ultimaker.attributes['extruder1'].target | float | round(1) | string)] | join if is_state('sensor.ultimaker', 'OK') else None }}"
- name: ultimaker_message
unique_id: "ultimaker_message"
state: '{{ states.sensor.ultimaker.attributes["display_status"]["message"] if is_state("sensor.ultimaker_info", "OK") else None }}'
icon: mdi:message-cog
- name: ultimaker_layer_height
unique_id: "ultimaker_layerheight"
state: '{{ states.sensor.ultimaker_file_metadata.attributes["layer_height"] | float(0) if is_state("sensor.ultimaker_file_metadata", "OK") else None }}'
unit_of_measurement: "mm"
icon: mdi:arrow-collapse-down
- name: ultimaker_object_height
unique_id: "<moonraker-ip-address>6d6d9dc0-9a02-4ce4-a797-c84b42e011a6"
state: '{{ (states.sensor.ultimaker_file_metadata.attributes["object_height"] | float(0)) - (states.sensor.ultimaker_file_metadata.attributes["layer_height"] | float(0)) if is_state("sensor.ultimaker_file_metadata", "OK") else None }}'
unit_of_measurement: "mm"
icon: mdi:arrow-expand-vertical
- name: ultimaker_current_height
unique_id: "ultimaker_current_height"
state: '{{ states.sensor.ultimaker.attributes["gcode_move"]["gcode_position"][2] | float(0) | round(2) if is_state("sensor.ultimaker_file_metadata", "OK") else None }}'
unit_of_measurement: "mm"
icon: mdi:arrow-collapse-down
- name: ultimaker_current_layer
unique_id: "ultimaker_current_layer"
state: '{{ (states("sensor.ultimaker_current_height")|float(0) / states("sensor.ultimaker_layer_height")|float(0))|round(0) if is_state("sensor.ultimaker_file_metadata", "OK") else None }}'
icon: mdi:counter
- name: ultimaker_total_layers
unique_id: "ultimaker_total_layers"
state: '{{ (states("sensor.ultimaker_object_height")|float(0) / states("sensor.ultimaker_layer_height")|float(0))|round(0) if is_state("sensor.ultimaker_file_metadata", "OK") else None }}'
icon: mdi:counter
- name: ultimaker_object_thumbnails
unique_id: "ultimaker_object_thumbnails"
state: '{{ states.sensor.ultimaker_file_metadata.attributes["thumbnails"][1]["relative_path"] if is_state("sensor.ultimaker_file_metadata", "OK") else None }}'
unit_of_measurement: "mm"
icon: mdi:image
- name: ultimaker_state_message
unique_id: "ultimaker_state_message"
state: '{{ states.sensor.ultimaker_info.attributes["state_message"] if is_state("sensor.ultimaker_info", "OK") else None }}'
icon: mdi:message-cog
rest_command:
ultimaker_emergency_stop:
url: "http://192.168.178.120:7125/printer/emergency_stop"
method: post
ultimaker_firmware_restart:
url: "http://192.168.178.120:7125/printer/firmware_restart"
method: post
ultimaker_cancel:
url: "http://192.168.178.120:7125/printer/print/cancel"
method: post
ultimaker_pause:
url: "http://192.168.178.120:7125/printer/print/pause"
method: post
ultimaker_resume:
url: "http://192.168.178.120:7125/printer/print/resume"
method: post
utlimaker_start_print:
url: "http://192.168.178.120/printer/print/start?filename={{ states('input_select.ultimaker_files') }}"
method: post
switch:
- platform: template
switches:
ultimaker_pause:
value_template: "{{ states('input_boolean.ultimaker_pause') }}"
unique_id: switch.ultimaker_pause
turn_on:
- service: rest_command.ultimaker_pause
- service: input_boolean.toggle
target:
entity_id: input_boolean.ultimaker_pause
turn_off:
- service: rest_command.ultimaker_resume
- service: input_boolean.toggle
target:
entity_id: input_boolean.ultimaker_pause
- platform: template
switches:
ultimaker_update_files:
value_template: "{{ states('input_boolean.ultimaker_update_files') }}"
unique_id: switch.ultimaker_update_files
turn_on:
- service: input_boolean.toggle
target:
entity_id: input_boolean.ultimaker_update_files
- service: input_boolean.toggle
target:
entity_id: input_boolean.ultimaker_update_files
turn_off:
- service: sinput_boolean.turn_off
target:
entity_id: input_boolean.ultimaker_update_files
G-Code Thumbnails
Ich wollte eigentlich gern ThumbNails auch vom Klipper abfragen. Das geht auch mit dem folgenden Request:
/server/files/thumbnails?filename={filename}
Leider kann man aber die Generic Kamera nicht mehr rein im YAML anlegen. Ich überspringe diesen Teil daher. Auch haben leider nicht alle meine Slices ein Thumbnail.
So oder so ähnlich kann man Klipper in sein Smart Home Einbinden. Bei mir schaut das so aus:
3D-Drucker Timed Start

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