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

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