NanoDLP in Home Assistant einbinden mit Scraper / REST

Ihr kennt alle OctoPrint und habt es vermutlich auch im HomeAssistant eingebunden. Das gleiche wollte ich für meinen Resindrucker auch machen. Hier habe ich von der OriginalFW auf NanoDLP – genauer gesagt auf NanoBean geflasht. Hier gibt es (meines Wissens nach) keine Integration – also habe ich mich mal mit der Scrape-Integration des HomeAssistant beschäftigt und versucht das Umzusetzen.

Eigentlich bin ich zuerst über den Scraper gestolpert und habe dann gedacht, dass der genau das richtige Tool ist, um endlich mal nanoDLP einzubinden.

NanoDLP ist eine openSource Firmware für Resindrucker. Vll habt ihr ja einen <$> Elegoo Mars </$> oder einen <$> Anycubic Photon </$>. Mit denen ist das auch Kompatibel. Aber man verliert die Gewährleistung, wenn man am Drucker rumpfuscht.

Scraper Integration

Ich hab nicht so viel Ahnung – also muss ich mich durch Tutorials und Dokus wälzen. Es gibt zwei Wege, einen Scrape-Sensor einzurichten:

File Editor

Ich hab also erstmal einen ganz normalen Sensor mit Plattform Scrape gebastelt, so wie in zahlreichen Blogs im Netz. Da kam dann direkt eine Warnung:

Also wurde wohl die Syntax angepasst, wie kürzlich bei MQTT auch. Dann schauen wir mal lieber in die Offizielle Dokumentation. Beim Scraper gibt es coole Beispielconfigs, von der ich direkt eine mit Copy&Paste in meine configuration.yaml gepackt hab. Und es hat direkt geklappt. Cool! Ich hab jetzt die URL zum HomeAssitant Podcast. Ich wusste nicht mal dass die sowas machen.

Falls ihr ganz neu dabei seid: Um die config.yaml bearbeiten zu können, braucht ihr das HomeAssistant Add-On File Editor. Oben Links gibt es einen ordner, mit dem ihr die Files in einem Explorer durchsuchen könnt. Das sieht so aus:

GUI

Der andere sichere Weg ist es über die Integrationsseite den Scraper hinzuzufügen.

Aufbau

Doch wie gliedert sich der YAML-Code?

scrape:
  - resource: https://hasspodcast.io/feed/podcast
    sensor:
      - name: Home Assistant Podcast
        select: "enclosure"
        index: 1
        attribute: url

Bei Ressource muss die URL zu der Website rein, die Ihr Scrapen wollt. In meinem Fall ist es einfach die IP vom Resin-Drucker im Heimnetzwerk.

scrape:
  - resource: http:192.168.178.68

Darunter können wir jetzt mehrere Sensoren definieren, wenn wir mehrere Infos von der gleichen Website sammeln wollen.

scrape:
  - resource: http:192.168.178.68
    sensor:
      - name: NanoDLP-Status
        select: "span.last_location"

      - name: NanoDLP-ETA
        select: "span.last_eta"

      - name: NanoDLP-Time
        select: "span.last_remaining"

Webdevelopment Basics

Doch wie kommen wir an die Intel, die wir brauchen, um den Scraper gescheit laufen zu lassen. Das wird jetzt Verrückt: Mit den WebDev Tools im Browser:

Ein Tutorial hat gesagt, man kann einfach den CSS-Selector kopieren Wenn wir also auf das gewünschte Element mit der rechten Maustaste klicken und dann den Punkt „untersuchen“ auswählen, öffnen sich die Entwicklerwerkzeuge. Rechts im BOM (Browser Object Model) sind alle Elemente der Website aufgelistet. Es gibt span und p und div und und und. Wenn wir auf unser gesuchtes Element wieder rechtsklicken, können wir den CSS Selector kopieren.

Das bedeutet, der ganze Pfad zu gewünschten Element ist dann in der Zwischenablage. Diesen können wir dann in den select-Bereich im HomeAssistant YAML einfügen. Man kann den Pfad ggf. auch auf das letzte Element kürzen. Probiert einfach aus was Funktioniert.

scrape:
  - resource: https://hasspodcast.io/feed/podcast
    sensor:
      - name: Home Assistant Podcast
        select: "enclosure"
        index: 1
        attribute: url
        
  - resource: http://192.168.178.68
    sensor:
      - name: NanoDLP-Status
        select: "span.last_location"
        #index: 0
        #attribute: url
      - name: NanoDLP-ETA
        select: "span.last_eta"
        #index: 0
        #attribute: url
      - name: NanoDLP-Time
        select: "span.last_remaining"
        #index: 0
        #attribute: url

Damit bekomme ich aber nur drei leere Entitäten. Unter Entwicklerwerkzeuge und Zustände kann man die Entitäten suchen und den Zustand direkt sehen. Wie wir sehen, sehen wir nichts.

Irgendwas stimmt also noch nicht…

Das kann daran liegen, dass der gewünschte Bereich erst nach dem ersten Laden der Seite durch ein JavaScript mit Informationen befüllt wird und daher der Scraper nur Leere vorfindet. Für diesen Verdacht spricht das etwa sekündliche Aufblinken des BOM-Elements – Es wird durch irgendein Script aktualisiert. Man kann also Versuchen, noch einen Header beim Scrapen mit durchzugeben.

  - resource: http://192.168.178.68
    headers:
        User-Agent: Mozilla/5.0
    sensor:
      - name: NanoDLP PrinterStatus
        select: ".last_location"

Hat bei mir aber auch nichts gebracht…

API-Request mit REST

Wenn wir also in den Entwicklertools auf den Reiter Network gehen, sehen wir eine Liste mit ziemlich vielen Einträgen.

Wenn ich den Blau markierten mit dem vielversprechenden Namen status einmal anklicke, sehen wir das was dahinter passiert.

Wenn wir nun wiederum auf Preview oder response wechseln, sehen wir die Antwort, die dieser Request bekommen hat. Das sieht wieder sehr vielversprechend aus.

Wir können also versuchen, diesen API request nach zu Ahmen und damit noch viel Umfangreichere Printer-Infos abzugreifen, als Ursprünglich mal angedacht. Wie cool ist das denn!

Leider kommt die Scrape-Integration hier an Grenzen – aber:

Für diesen Fall gibt es eine gute Alternative: REST

Wir können den folgenden Code unter den Block mit unseren Sensoren parken:

#sensor:
#--- nanoDLP via REST ---
  - platform: rest
    name: nanoDLP-PrinterStatus-REST
    value_template: "{{ value_json['Printing'] }}"
    #unit_of_measurement: "%"
    scan_interval: 3600
    resource: http://192.168.178.68/status
    headers:
      Accept: application/json, text/javascript, */*; q=0.01
      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
      #Upgrade-Insecure-Requests: 1
      Referer: 'http://192.168.178.68'

JSON

Aus den Entwicklertools weiß ich, dass der Status-Request den Folgenden JSON String zurück gibt:

{
"AutoShutdown":false,
"Build":"generic",
"Camera":0,"Cast":true,
"Covered":false,
"CurrentHeight":123200,
"ForceStop":false,
"Halted":false,
"LampHours":0,
"LayerID":1,
"LayerSinceStart":1440,
"LayerStartTime":0,
"LayerTime":18072628718,
"LayersCount":1440,
"PanicRow":23271,
"Panicked":false,
"Path":"_skull03-sup",
"Paused":false,
"PlateHeight":72,
"PlateID":5,
"PrevLayerTime":0,
"Printing":false,
"ResumeID":1,
"SlicingPlateID":0,
"StartAfterSlice":0,
"State":0,"Version":6025,
"Wifi":"",
"disk":"7%",
"mem":"4%",
"proc":"0%",
"proc_numb":"22",
"temp":"49.39°C",
"uptime":"26h"
}

Daher suche ich erstmal nur im JSON String nach dem Printing Wertepaar. Das tue ich mit dem valuetemplate. Dort könnten wir auch noch weitere Transformationen machen, z.b. bei uptime das h kürzen und in eine echte Integer umwandeln. Die restlichen Sachen imitieren den Zugriff durch einen Webbrowser, damit der Request beantwortet wird. Nun könnten wir für alle diese JSON Werte, die wir brauchen einen eigenen Sensor basteln. Oder wir speichern einfach den ganzen String in den Sensor und bearbeiten den später weiter, das geht natürlich auch.

Ich hab mir nur zum Probieren erstmal einfach noch Sensoren mit LayerID, LayersCount und LayerTime hier heraus gezogen.

Vergesst nicht, dass ihr nach allen Änderungen an der Config.yaml über Entwicklerwerkzeuge – YAML die Datei prüfen und dann Neu starten müsst.

  - platform: rest
    name: nanoDLP-PrinterLayer-REST
    value_template: "{{ value_json['LayerID'] }}"
    #unit_of_measurement: "%"
    scan_interval: 10
    resource: http://192.168.178.68/status
    headers:
      Accept: application/json, text/javascript, */*; q=0.01
      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
      #Upgrade-Insecure-Requests: 1
      Referer: 'http://192.168.178.68'
      
  - platform: rest
    name: nanoDLP-PrinterLayerCount-REST
    value_template: "{{ value_json['LayersCount'] }}"
    #unit_of_measurement: "%"
    scan_interval: 10
    resource: http://192.168.178.68/status
    headers:
      Accept: application/json, text/javascript, */*; q=0.01
      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
      #Upgrade-Insecure-Requests: 1
      Referer: 'http://192.168.178.68'

Im Screenshot sind noch die nichtfunktioniernden Scrape-Sensoren, die müssen wir später noch wieder aus der config.yaml oder per GUI löschen.

Wenn hier jemand noch einen Tipp hat, wie man die ggf. mit Scrape zum Laufen bringt, immer her damit.

Die Alternative ist, den ganzen JSON String zu speichern und später einfach die Wertepaare mit Templatesensoren auszulesen:

  - platform: rest
    name: nanoDLP-FullJSONString -REST
    scan_interval: 10
    value_template: "{{ value_json.message }}"
    resource: http://192.168.178.68/status
    headers:
      Accept: application/json, text/javascript, */*; q=0.01
      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
      #Upgrade-Insecure-Requests: 1
      Referer: 'http://192.168.178.68'
    json_attributes_path: "$.['.']"
    json_attributes:
      - AutoShutdown
      - Build
      - Camera
      - Covered
      - CurrentHeight
      - ForceStop
      - Halted
      - LampHours
      - LayerID
      - LayerSinceStart
      - LayerStartTime
      - LayerTime
      - LayersCount
      - PanicRow
      - Panicked
      - Path
      - Paused
      - PlateHeight
      - PlateID
      - PrevLayerTime
      - Printing
      - ResumeID
      - SlicingPlateID
      - StartAfterSlice
      - State
      - Wifi
      - disk
      - mem
      - proc
      - proc_numb
      - temp
      - uptime
template:
  sensor:
    - name: AutoShutDown-nanodlp
      unique_id: AutoShutDown-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'AutoShutdown') }}"
    - name: Build-nanodlp
      unique_id: Build-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'Build') }}"
    - name: Camera-nanodlp
      unique_id: Camera-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'Camera') }}"
    - name: Covered-nanodlp
      unique_id: Covered-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'Covered') }}"
    - name: CurrentHeight-nanodlp
      unique_id: CurrentHeight-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'CurrentHeight') }}"
    - name: ForceStop-nanodlp
      unique_id: ForceStop-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'ForceStop') }}"
    - name: Halted-nanodlp
      unique_id: Halted-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'Halted') }}"
    - name: LampHours-nanodlp
      unique_id: LampHours-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'LampHours') }}"
    - name: LayerID-nanodlp
      unique_id: LayerID-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'LayerID') }}"
    - name: LayerSinceStart-nanodlp
      unique_id: LayerSinceStart-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'LayerSinceStart') }}"
    - name: LayerStartTime-nanodlp
      unique_id: LayerStartTime-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'LayerStartTime') }}"
    - name: LayerTime-nanodlp
      unique_id: LayerTime-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'LayerTime') }}"
    - name: LayersCount-nanodlp
      unique_id: LayersCount-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'LayersCount') }}"
    - name: PanicRow-nanodlp
      unique_id: PanicRow-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'PanicRow') }}"
    - name: Panicked-nanodlp
      unique_id: Panicked-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'Panicked') }}"
    - name: Path-nanodlp
      unique_id: Path-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'Path') }}"
    - name: Paused-nanodlp
      unique_id: Paused-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'Paused') }}"
    - name: PlateHeight-nanodlp
      unique_id: PlateHeight-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'PlateHeight') }}"
    - name: PlateID-nanodlp
      unique_id: PlateID-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'PlateID') }}"
    - name: PrevLayerTime-nanodlp
      unique_id: PrevLayerTime-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'PrevLayerTime') }}"
    - name: Printing-nanodlp
      unique_id: Printing-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'Printing') }}"
    - name: ResumeID-nanodlp
      unique_id: ResumeID-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'ResumeID') }}"
    - name: SlicingPlateID-nanodlp
      unique_id: SlicingPlateID-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'SlicingPlateID') }}"
    - name: StartAfterSlice-nanodlp
      unique_id: StartAfterSlice-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'StartAfterSlice') }}"
    - name: State-nanodlp
      unique_id: State-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'State') }}"
    - name: Wifi-nanodlp
      unique_id: Wifi-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'Wifi') }}"
    - name: disk-nanodlp
      unique_id: disk-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'disk') }}"
    - name: mem-nanodlp
      unique_id: mem-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'mem') }}"
    - name: proc-nanodlp
      unique_id: proc-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'proc') }}"
    - name: proc_numb-nanodlp
      unique_id: proc_numb-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'proc_numb') }}"
    - name: temp-nanodlp
      unique_id: temp-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'temp') }}"
    - name: uptime-nanodlp
      unique_id: uptime-nanodlp
      state: "{{ state_attr('sensor.nanodlp_fulljsonstring_rest', 'uptime') }}"
Neustart nicht vergessen
Sensor mit dem ganzen JSON String
Template-Sensoren mit den jeweiligen Zuständen

Nutzung der Daten

Ich hab auch noch etwas das JS untersucht und ein paar Hinweise zur Berechnung gefunden. So wird LayerTime mit einem Wert von 1000000000 dividiert.

Außerdem habe ich die Berechnung der Zeiten im JavaScript gefunden.+:

var remaining_time = Math.round((last_value('layers_count')-current_layer_id)*last_value('layer_time')/60);
		var total_time = Math.round(last_value('layers_count')*last_value('layer_time')/60);
		var est = new Date();
		est.setMinutes(est.getMinutes() + remaining_time);

Reverse Engineering

Mit den ausgelesenen Werten können wir jetzt zum Beispiel selbst die verstrichene und die erwartete Restzeit bestimmen. Dazu können wir wieder einen Template-Sensor benutzen. Diesen entwickeln wir mit der praktischen Funktion Template unter Entwicklerwerkzeuge, indem wir einfach die Berechnung nachbauen.

{% set LayersCount = states('sensor.layerscount_nanodlp')| int %}
{% set CurrentLayersID = states('sensor.layerid_nanodlp') | int %}
{% set LayerTime = states("sensor.layertime_nanodlp") | int / 1000000000 %}
{% set RemainingTime = (LayersCount - CurrentLayersID) * LayerTime %}
{% set ETA = (as_timestamp(now()) + RemainingTime) | timestamp_custom("%H:%M", 1)  %}

Remaining Time: {{ RemainingTime | timestamp_custom("%H:%M", 0) }} h
Finishing Time: {{ ETA }}

Wir ziehen uns die Werte aus den Sensoren einfach in Variablen und Wandeln diese in echte Integer um. Dann Wandeln wir noch die Formatierung der Zeitanzeige etwas um und schon haben wir die Restzeit und den voraussichtlichen Zeitpunkt, wenn der Druck abgeschlossen ist. Die verstrichene Zeit ist hier jetzt nicht drin, kommt aber gleich.

Daraus können wir uns dann die folgenden Template-Sensoren in der Config.yaml basteln, diesmal auch einen für die verstrichene Zeit:

    - name: nanodlp-remainingTime
      unique_id: nanodlp-remainingTime
      state: >
        {% set LayersCount = states('sensor.layerscount_nanodlp')| int %}
        {% set CurrentLayersID = states('sensor.layerid_nanodlp') | int %}
        {% set LayerTime = states("sensor.layertime_nanodlp") | int / 1000000000 %}
        {% set RemainingTime = (LayersCount - CurrentLayersID) * LayerTime %}
        {{ RemainingTime | timestamp_custom("%H:%M", 0) }}
    - name: nanodlp-finishingTime
      unique_id: nanodlp-finishingTime
      state: >
        {% set LayersCount = states('sensor.layerscount_nanodlp')| int %}
        {% set CurrentLayersID = states('sensor.layerid_nanodlp') | int %}
        {% set LayerTime = states("sensor.layertime_nanodlp") | int / 1000000000 %}
        {% set RemainingTime = (LayersCount - CurrentLayersID) * LayerTime %}
        {% set ETA = (as_timestamp(now()) + RemainingTime) | timestamp_custom("%H:%M", 1)  %}
        {{ ETA }}
        
    - name: nanodlp-printTime
      unique_id: nanodlp-printTime
      state: >
        {% set CurrentLayersID = states('sensor.layerid_nanodlp') | int %}
        {% set LayerTime = states("sensor.layertime_nanodlp") | int / 1000000000 %}
        {% set PrintTime = (CurrentLayersID * LayerTime) | timestamp_custom("%H:%M:%S", 0) %}
        {{PrintTime }}

Wie immer – neu Starten nicht vergessen (vorher Prüfen) (Entwicklerwerkzeuge – YAML) und dann sehen wir das folgende für die ETA unter Entwicklerwerkzeuge – Zustände:

So weit so gut. Als nächstes können wir die Sensoren, die uns interessieren ins Dashboard einbauen. Das beschreibe ich jetzt nicht weiter im Detail.

NanoDLP in HomeAssistant Dashboard

Als nächstes werde ich versuchen, auch Befehle an das nanoDLP Interface zu schicken. Wünscht mir Glück

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

1 Kommentar

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