Amazon Wishlist Price Alert mit HomeAssistant und NodeRED for noobs

NodeRED kommt später, hier erstmal wie es begann: Ich hab mal wieder etwas mit dem Scrape rumgespielt. Ich hab mir meine Amazon Wunschliste (die ja öffentlich ist) geschnappt, und mir den folgenden Scraper gebaut:

scrape:
  - resource: https://www.amazon.de/hz/wishlist/ls/226CCLKX9Q3A8?ref_=wl_share
    headers:
        User-Agent: Mozilla/5.0
    sensor:
      - name: Amazon-Price-Whole
        select: ".a-price-whole"
      - name: Amazon-Price-fraction
        select: ".a-price-fraction"
      - name: Amazon-Title
        select: "h2.a-size-base"

Das funktioniert auch soweit:

Problem ist jetzt aber, dass ich so für jedes Element einen eigenen Sensor basteln müsste – und wenn die Länge der Liste sich ändert, muss ich ständig anpassen. Den Scraper kann ich leider nicht in eine Loop einbauen.

Aber: NodeRED kann das. Ich hab vorher noch nie NodeRED benutzt, und das hier zum laufen zu bekommen hat mich einen ganzen Sonntag gekostet. Daher will ich hier sehr ausführlich auf NodeRED eingehen, damit ihr etwas Zeit sparen könnt beim Trobule Shooting.

TL;DR

Wer einfach nur meinen Flow haben will: Hier ist er:

[{"id":"2cce914febb9d4d0","type":"tab","label":"Amazon Wishlist Scrape","disabled":false,"info":"","env":[]},{"id":"0bb9f88b34315ae8","type":"http request","z":"2cce914febb9d4d0","name":"GET Wishlist","method":"GET","ret":"txt","paytoqs":"ignore","url":"https://www.amazon.de/hz/wishlist/ls/226CCLKX9Q3A8?ref_=wl_share","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[{"keyType":"User-Agent","keyValue":"","valueType":"Mozilla/5.0","valueValue":""}],"x":310,"y":340,"wires":[["cb1ed921a561916e","e7234e054e4f0efa","6f71b8139f2c1d11","ea3d9c94b322293b"]]},{"id":"cb1ed921a561916e","type":"html","z":"2cce914febb9d4d0","name":"scrape titles","property":"payload","outproperty":"payload","tag":"h2.a-size-base","ret":"text","as":"single","x":510,"y":340,"wires":[["75b8396d08f33fe3"]]},{"id":"2139c8f7d42d4662","type":"inject","z":"2cce914febb9d4d0","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"00 12 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":130,"y":340,"wires":[["0bb9f88b34315ae8","2f12728bb4559955","a751f031420b6487","34ebd44844809d3d","a2427449f3afcf93"]]},{"id":"348c76a488e5a81d","type":"debug","z":"2cce914febb9d4d0","name":"show array","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":930,"y":220,"wires":[]},{"id":"e7234e054e4f0efa","type":"html","z":"2cce914febb9d4d0","name":"scrape whole","property":"payload","outproperty":"payload","tag":"span.a-price-whole","ret":"text","as":"single","x":510,"y":280,"wires":[["2e81f46f15407908"]]},{"id":"6f71b8139f2c1d11","type":"html","z":"2cce914febb9d4d0","name":"scrape fraction","property":"payload","outproperty":"payload","tag":"span.a-price-fraction","ret":"text","as":"single","x":520,"y":220,"wires":[["29c5b0402ba2e2d8"]]},{"id":"75b8396d08f33fe3","type":"function","z":"2cce914febb9d4d0","name":"clean array","func":"msg.headers = msg.originalHeaders;\nvar titles = msg.payload;\nvar newPayload = [];\nfor (var i = 0; i < titles.length ; i++) {\n    newPayload.push( String(titles[i].replace(/\\n/g, \"\").replace(/\\t/g, \"\").replace(/\\n/g, \"\") )\n);\n}\n\nmsg.payload = newPayload;\nmsg.topic = \"titles\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":730,"y":340,"wires":[["ea870eb63cdda1b5","a7d1fbfa9e47374e"]]},{"id":"2e81f46f15407908","type":"function","z":"2cce914febb9d4d0","name":"clean array","func":"msg.headers = msg.originalHeaders;\nvar whole = msg.payload;\nvar newPayload = [];\nfor (var i = 0; i < whole.length; i++) {\n    newPayload.push(Number(whole[i].replace(/\\n/g,\"\").replace(/,/g,\"\"))\n    );\n}\nmsg.payload = newPayload;\nmsg.topic =\"whole\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":730,"y":280,"wires":[["442c24aeeb77c6c3","a7d1fbfa9e47374e"]]},{"id":"29c5b0402ba2e2d8","type":"function","z":"2cce914febb9d4d0","name":"clean array","func":"msg.headers = msg.originalHeaders;\nvar fraction = msg.payload;\nvar newPayload = [];\nfor (var i = 0; i < fraction.length; i++) {\n    newPayload.push(\n        Number(fraction[i].replace(/\\n/g, \" \"))\n        );\n}\nmsg.payload = newPayload;\nmsg.topic = \"fraction\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":730,"y":220,"wires":[["348c76a488e5a81d","a7d1fbfa9e47374e"]]},{"id":"0b2b189ee61d8924","type":"debug","z":"2cce914febb9d4d0","name":"show join","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1100,"y":120,"wires":[]},{"id":"a1281f4630cae160","type":"api-call-service","z":"2cce914febb9d4d0","name":"","server":"c1eed9d5.7b0d28","version":5,"debugenabled":false,"domain":"input_text","service":"set_value","areaId":[],"deviceId":[],"entityId":["input_text.amazon_prices"],"data":"{\"value\":payload.prices}","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1520,"y":500,"wires":[[]]},{"id":"f9a5eebacdfbee32","type":"debug","z":"2cce914febb9d4d0","name":"show saved string","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1750,"y":560,"wires":[]},{"id":"b9e531455d2fc639","type":"api-call-service","z":"2cce914febb9d4d0","name":"","server":"c1eed9d5.7b0d28","version":5,"debugenabled":false,"domain":"input_text","service":"set_value","areaId":[],"deviceId":[],"entityId":["input_text.amazon_titles"],"data":"{\"value\":payload.titles}","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1540,"y":560,"wires":[["f9a5eebacdfbee32"]]},{"id":"3136544d4a68fd77","type":"function","z":"2cce914febb9d4d0","name":"check with treshold","func":"msg.headers = msg.originalHeaders;\nvar price = msg.payload.prices;\nvar titles = msg.payload.titles;\nvar urls = msg.payload.urls;\nvar minprice;\nvar index;\nvar title;\nvar url;\nvar tresprice = global.get('homeassistant.homeAssistant.states[\"input_number.amazon_treshold_price\"].state');;\nvar success = false;\n\n\nfor (var i = 0; i < price.length; i++) {\n\n    if (price[i] <= tresprice){\n    minprice = Number(price[i]);\n    index = i;\n    title = titles[i];\n    url = urls[i];\n    success = true;\n    msg.minprice = minprice;\n    msg.minindex = index;\n    msg.mintitle = title;  \n    msg.minurl = url;\n    msg.success = success;  \n    node.send(msg);     \n    }\n}\n\n\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1450,"y":160,"wires":[["02fd2694594f4b2a","5cb40e367086bc10"]]},{"id":"3c0b3bcd800a9209","type":"function","z":"2cce914febb9d4d0","name":"join as price","func":"msg.headers = msg.originalHeaders;\nvar fraction = msg.payload.fraction;\nvar whole = msg.payload.whole;\nvar titles = msg.payload.titles;\nvar urls = msg.payload.urls;\nvar newPayload = [];\n\nfor (var i = 0; i < fraction.length ; i++ ) {\n    var price = String(whole[i]+\".\"+fraction[i]);\n    //var string = String(String(titles[i])+\":\"+price);\n    newPayload.push(Number(price));\n}\nmsg.payload = {};\nmsg.payload.prices = newPayload;\nmsg.payload.titles = titles;\nmsg.payload.urls = urls;\nmsg.topic = \"price\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1110,"y":160,"wires":[["3136544d4a68fd77","9109491db10614d4","492cf2633193ba60","d6bbd3345b03d0b5"]]},{"id":"02fd2694594f4b2a","type":"debug","z":"2cce914febb9d4d0","name":"show price","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1670,"y":160,"wires":[]},{"id":"442c24aeeb77c6c3","type":"debug","z":"2cce914febb9d4d0","name":"show array","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":930,"y":280,"wires":[]},{"id":"ea870eb63cdda1b5","type":"debug","z":"2cce914febb9d4d0","name":"show array","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":930,"y":340,"wires":[]},{"id":"a7d1fbfa9e47374e","type":"join","z":"2cce914febb9d4d0","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"1","count":"4","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":910,"y":160,"wires":[["3c0b3bcd800a9209","0b2b189ee61d8924"]]},{"id":"001097acc5097868","type":"api-call-service","z":"2cce914febb9d4d0","name":"notify","server":"c1eed9d5.7b0d28","version":5,"debugenabled":false,"domain":"notify","service":"mobile_app_daniel_phone","areaId":[],"deviceId":[],"entityId":[],"data":"payload","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1450,"y":340,"wires":[[]]},{"id":"5cb40e367086bc10","type":"function","z":"2cce914febb9d4d0","name":"create Alert Message","func":"var minprice = msg.minprice;\nvar index = msg.minindex;\nvar title = msg.mintitle;\nvar url = msg.minurl;\nvar success = msg.success;\nvar message = {};\nif(success == true){\n    message = String(\"Das Buch\" + title + \"ist für \" + minprice + \" € erhältlich\");\n\n}else{message = String(\"Leider kein Rabatt heute\");}\n\nvar notification = {\n    data: {\n        \"message\": message,\n        \"title\": \"Price Alert\",\n        \"data\":\n        {\n            \"actions\":\n            [{\n                \"action\": \"URI\",\n                \"uri\": url,\n                \"title\": \"Direkt Bestellen\",\n            }],\n        }\n    }\n}\nmsg.payload = notification;\n//msg.payload.url = url;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1440,"y":220,"wires":[["6a54cf38b6951afe","528379fcab9c6a47"]]},{"id":"6a54cf38b6951afe","type":"debug","z":"2cce914febb9d4d0","name":"show message","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1680,"y":220,"wires":[]},{"id":"9109491db10614d4","type":"debug","z":"2cce914febb9d4d0","name":"show join","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1420,"y":120,"wires":[]},{"id":"492cf2633193ba60","type":"join","z":"2cce914febb9d4d0","name":"","mode":"custom","build":"string","property":"payload.prices","propertyType":"msg","key":"topic","joiner":",","joinerType":"str","accumulate":false,"timeout":"1","count":"1","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":1330,"y":500,"wires":[["a1281f4630cae160"]]},{"id":"d6bbd3345b03d0b5","type":"join","z":"2cce914febb9d4d0","name":"","mode":"custom","build":"string","property":"payload.titles","propertyType":"msg","key":"topic","joiner":",","joinerType":"str","accumulate":false,"timeout":"1","count":"1","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":1330,"y":560,"wires":[["b9e531455d2fc639"]]},{"id":"528379fcab9c6a47","type":"switch","z":"2cce914febb9d4d0","name":"","property":"success","propertyType":"msg","rules":[{"t":"true"},{"t":"false"}],"checkall":"true","repair":false,"outputs":2,"x":1450,"y":280,"wires":[["001097acc5097868"],[]]},{"id":"ea3d9c94b322293b","type":"html","z":"2cce914febb9d4d0","name":"scrape URLs","property":"payload","outproperty":"payload","tag":"h2.a-size-base > a","ret":"attr","as":"single","x":510,"y":400,"wires":[["42e2294cd4c3c7f6","dd4c07a59762ef3e"]]},{"id":"42e2294cd4c3c7f6","type":"function","z":"2cce914febb9d4d0","name":"clean array","func":"msg.headers = msg.originalHeaders;\nvar urls = msg.payload;\nvar newPayload = [];\nfor (var i = 0; i < urls.length ; i++) {\n    newPayload.push( \"https://www.amazon.de\" + urls[i].href);\n}\n\nmsg.payload = newPayload;\nmsg.topic = \"urls\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":730,"y":400,"wires":[["e961c1b976df37ac","a7d1fbfa9e47374e"]]},{"id":"dd4c07a59762ef3e","type":"debug","z":"2cce914febb9d4d0","name":"show array","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":730,"y":460,"wires":[]},{"id":"e961c1b976df37ac","type":"debug","z":"2cce914febb9d4d0","name":"show array","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":930,"y":400,"wires":[]},{"id":"7d6d8c385c98c645","type":"trigger","z":"2cce914febb9d4d0","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":120,"y":280,"wires":[["0bb9f88b34315ae8","2f12728bb4559955","a751f031420b6487","34ebd44844809d3d","a2427449f3afcf93"]]},{"id":"58f079f659bd6593","type":"debug","z":"2cce914febb9d4d0","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":300,"y":160,"wires":[]},{"id":"f340c28869150aa5","type":"poll-state","z":"2cce914febb9d4d0","name":"Manual Start","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.amazon_scrape_switch","state_type":"habool","halt_if":"true","halt_if_type":"bool","halt_if_compare":"is","outputs":2,"x":110,"y":180,"wires":[["58f079f659bd6593","7d6d8c385c98c645"],["58f079f659bd6593","7d6d8c385c98c645"]]},{"id":"2f12728bb4559955","type":"http request","z":"2cce914febb9d4d0","name":"GET Wishlist","method":"GET","ret":"txt","paytoqs":"ignore","url":"https://www.amazon.de/hz/wishlist/slv/items?filter=unpurchased&paginationToken=eyJGcm9tVVVJRCI6ImFjNDFlYTQ2LTc4MWEtNDgxZS1iZTU2LTczYjQ5MjI1MWU0MCIsIlRvVVVJRCI6IjdiYWIxZjBmLTBjODEtNDE2Ny1hNzg0LTdjY2RkYTZjOTJhZCIsIkVkZ2VSYW5rIjoxODgyMDJ9&itemsLayout=LIST&sort=default&type=wishlist&lid=226CCLKX9Q3A8&ajax=true","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[{"keyType":"User-Agent","keyValue":"","valueType":"Mozilla/5.0","valueValue":""}],"x":310,"y":420,"wires":[["6f71b8139f2c1d11","e7234e054e4f0efa","cb1ed921a561916e","ea3d9c94b322293b"]]},{"id":"a751f031420b6487","type":"http request","z":"2cce914febb9d4d0","name":"GET Wishlist","method":"GET","ret":"txt","paytoqs":"ignore","url":"https://www.amazon.de/hz/wishlist/slv/items?filter=unpurchased&paginationToken=eyJGcm9tVVVJRCI6ImFjNDFlYTQ2LTc4MWEtNDgxZS1iZTU2LTczYjQ5MjI1MWU0MCIsIlRvVVVJRCI6IjFkNjRlYjljLWYzNWItNDU5Yy1iZGUzLTVjZDM4YWRlZGU1NyIsIkVkZ2VSYW5rIjoxNTMwMDV9&itemsLayout=LIST&sort=default&type=wishlist&lid=226CCLKX9Q3A8&ajax=true","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[{"keyType":"User-Agent","keyValue":"","valueType":"Mozilla/5.0","valueValue":""}],"x":310,"y":480,"wires":[["ea3d9c94b322293b","cb1ed921a561916e","e7234e054e4f0efa","6f71b8139f2c1d11"]]},{"id":"34ebd44844809d3d","type":"http request","z":"2cce914febb9d4d0","name":"GET Wishlist","method":"GET","ret":"txt","paytoqs":"ignore","url":"https://www.amazon.de/hz/wishlist/slv/items?filter=unpurchased&paginationToken=eyJGcm9tVVVJRCI6ImFjNDFlYTQ2LTc4MWEtNDgxZS1iZTU2LTczYjQ5MjI1MWU0MCIsIlRvVVVJRCI6IjJhYmZiZmZlLWM0NmUtNDY1Ni1iNzkxLTNhYzc5Y2YyNjg5MCIsIkVkZ2VSYW5rIjo5OTI3MX0&itemsLayout=LIST&sort=default&type=wishlist&lid=226CCLKX9Q3A8&ajax=true","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[{"keyType":"User-Agent","keyValue":"","valueType":"Mozilla/5.0","valueValue":""}],"x":310,"y":540,"wires":[["6f71b8139f2c1d11","e7234e054e4f0efa","cb1ed921a561916e","ea3d9c94b322293b"]]},{"id":"a2427449f3afcf93","type":"http request","z":"2cce914febb9d4d0","name":"GET Wishlist","method":"GET","ret":"txt","paytoqs":"ignore","url":"https://www.amazon.de/hz/wishlist/slv/items?filter=unpurchased&paginationToken=eyJGcm9tVVVJRCI6ImFjNDFlYTQ2LTc4MWEtNDgxZS1iZTU2LTczYjQ5MjI1MWU0MCIsIlRvVVVJRCI6IjU3MDc2ZmQyLTcyZDItNDllYS1hYzdiLWZiNWUzOGE3NDY0ZSIsIkVkZ2VSYW5rIjo1MDA1OH0&itemsLayout=LIST&sort=default&type=wishlist&lid=226CCLKX9Q3A8&ajax=true","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[{"keyType":"User-Agent","keyValue":"","valueType":"Mozilla/5.0","valueValue":""}],"x":310,"y":600,"wires":[["ea3d9c94b322293b","cb1ed921a561916e","e7234e054e4f0efa","6f71b8139f2c1d11"]]},{"id":"c1eed9d5.7b0d28","type":"server","name":"Home Assistant","addon":true}]

Und so sieht das Baby aus:

Und hier sind die notwendigen Deklarationen:

input_text:
amazon_prices:
name: Amazon Preise
initial: 0
amazon_titles:
name: Amazon Titel
initial: None
input_number:
amazon_treshold_price:
name: Amazon Preisgrenze
initial: 5
min: 0
max: 20
step: 1
input_boolean:
amazon_scrape_switch:
name: Manual Start
switch:
- platform: template
switches:
scraper:
value_template: "{{ states('input_boolean.amazon_scrape_switch') }}"
unique_id: switch.amazon_scraper
turn_on:
- service: input_boolean.toggle
target:
entity_id: input_boolean.amazon_scrape_switch
- service: input_boolean.toggle
target:
entity_id: input_boolean.amazon_scrape_switch
turn_off:
- service: sinput_boolean.turn_off
target:
entity_id: input_boolean.amazon_scrape_switch

Wenn ihr den switch betätigt, geht dieser kurz an, löst den Node-Red Flow aus und sofort wieder auf aus. Fand ich schöner als direkt den input_boolean ins Dashboard zu nehmen. Die Input Number kann man schön als Slider ins Dashboard einbauen.

NodeRed für NOOBs

Also – NodeRED ist ein Tool mit dem man komplizierte Abläufe mit wenig echtem Code umsetzen kann. Ähnlich wie z.B. MIT App Inventor oder Blocklify lässt sich hier mit Blöcken – sogenannten Nodes arbeiten. Diese kann man munter miteinander verlinken und so sein Programm aufbauen. Am besten lest ihr auch die offizielle Dokumentation, die ist nicht soo trocken.

NodeRED basiert darauf, dass eine (oder mehrere) Nachricht(en) (ein JSON Objekt) von einem Node zum nächsten gereicht wird.

NodeRED einrichten

Hier gab es bei mir die ersten Schwierigkeiten, da bei mir die Erweiterung immer gestoppt hat. Ein kurzer Blick in die Logs hat mir aber verraten, das ich die Konfiguration einfach nicht gemacht hatte.

Wir holen uns also erstmal das Add-On im Home Assistant Addon Store (nicht HACS, sondern der Offizielle). Dazu gehen wir auf Einstellungen – Add-Ons und gehen auf die Kachel unten Rechts:

Da wird man mit optionen Überladen, daher suchen wir einfach nach node:

Nun können wir das Add-On hinzufügen und wechseln dann auf die Benutzeroberfläche.

Wir gehen auf den Reiter Konfiguration. Hier müssen wir uns ein paar Usernames und Passwörter überlegen. Am leichtesten, macht das einfach in der YAML Anischt. Überall wo User steht, muss ein Nutzername rein, überall wo ein Kennwort gebraucht wird, tragt ihr eines ein. Denkt euch was aus 🙂
Danach wird NodeRED neu gestartet.

Der erste Start dauert immer relativ lange – im Reiter Protokoll kann man das gut sehen. Erst wenn NGINX läuft, ist alles bereit.

Ich lasse mir gern links in der Seitenleiste einen Link zeigen und lass es auch automatisch Starten.

Nun sind wir Ready to GO – auf zur Benutzeroberfläche. So sieht ein frisches NodeRED aus:

NodeRED Interface

Links haben wir die ganzen Bausteine, rechts gibt es infos, oben ein paar Reiter, man sieht meinen Fertigen Scraper-Flow als Reiter. Relativ übersichtlich.

Inject Node

Als erstes brauchen wir einen sogenannten Injector. Das Teil kommt den Triggern in der HomeAssistant automation recht nahe. Der Injector sorgt dafür, dass eine Nachricht zu einem Intervall in Auftrag gegeben wird. Bei mir ist es täglich um Zwölf Uhr.

Wir können Links in der Nodeauswahl also nach Inject suchen und diesen Node mit Drag&Drop auf die Arbeitsfläche ziehen. Dieser ändert dann seinen Titel zu timestamp. Das hatte mich als erstes verwirrt, denn ich wollte direkt loslegen, hatte timestamp eingegeben und nichts gefunden. Also Inject – nicht Timestamp!

Mit doppelklick öffnen sich die Eigenschaften des Nodes.

Unten können wir nun eine Wiederholung auswählen. Danach ändert sich die Ansicht entsprechend den Auswahlmöglichkeiten.

Jetzt dürft ihr dreimal raten, warum mein Flow jeden Tag um 12 auslöst.

Wenn wir hier fertig sind, gibt es oben rechts einen Fertig-Knopf den wir sogleich betätigen. Das Icon auf dem Node ändert sich nun ein wenig, scheint also geklappt zu haben.

HTTP Request Node

Als nächstes brauchen wir einen HTTP Request, um unsere Website abzurufen. In meinem Fall ist es meine Amazon Wunschliste. Wir suchen also wieder links, diesmal nach HTTP und nehmen den Node „HTTP Request“ und ziehen diesen mit Drag&Drop auf die Arbeitsfläche.

Auch hier können wir wieder mit Doppelklick in die Attribute wechseln.

Die Methode belassen wir bei GET. Bei URL kommt unser Wunschlink rein. Wenn ihr Nutzernamen eingeben müsst, könnt ihr sogar das. Ich füge noch eine Kopfzeile hinzu, damit wir uns für einen Browser ausgeben können. Der Button dazu ist ganz unten, relativ klein (+Hinzufügen). Außerdem können wir noch einen Namen vergeben.

HTML Node

Der nächste Baustein ist ein HTML Element. Das können wir wieder Links bei den Nodes finden, wenn wir danach suchen. Wieder auf die Arbeitsfläche damit!

CSS Selector

Wie schon im vorherigen Post erwähnt, brauchen wir einen Selector. Diese bekommen wir, wenn wir auf der Website die Entwicklertools benutzen. Sucht einfach auf der Seite, dass Element dass ihr Scrapen wollt und geht mit rechtsklick auf Untersuchen. Es öffnet sich der HTML Code und das Element wird schon blau markiert. Für die Überschrift können wir also das h2 Element mit der Klasse a-size-base genutzen.

h2.a-size-base

Außerdem möchte ich nur den Textinhalt, da dass der wirkliche Titel ist. Das können wir im Dropdown bei Ausgang festlegen.

Debug Node

Der nächste Block ist der Debug Block. Der kann uns Nachrichten ausgeben was gerade so passiert. Man kann sich den Payload der Nachricht oder auch das ganze Nachricht Objekt ausgeben lassen.

Connections

Wenn ich jetzt also diese vier Nodes verbinde, kann ich die einzelnen Schritte testen und gut nachvollziehen was passiert.

Wir probieren also erstmal nur den Inject und den Debug Node aus. Dazu ziehen wir uns die beiden ein Stück nach oben (Drag&Drop) und verbinden den Ausgang des Inject-Nodes mit dem Eingang des Debug-Nodes (auch Drag&Drop auf die kleinen Punkte).

Jetzt müssen wir oben rechts auf den großen roten Knopf drücken, um unseren Node-Tree an den NodeRED Server zu schicken und können danach auf das kleine blaue Quadrat links am Injector klicken um einen Nachrichtendurchlauf manuell zu starten. Damit wir die Debug-Nachrichten sehen, müssen wir rechts im Infobereich auf den Reiter Debug wechseln. Das ist der kleine Käfer.

Es kommt ein Pop-Up und in der Debug-Konsole sehen wir den Timestamp.

Falls Ihr jetzt nichts seht, kann es sein, dass euer Debug Node deaktiviert ist. Das ist das kleine grüne Quadrat, rechts am Node, welches die Aktivität schnell Umschalten kann. Schauen wir uns doch mal das ganze Nachrichtenobjekt an. Wir können dazu einmal auf den Debugnode doppelklicken und uns das ganze Nachrichtenobjekt ausgeben lassen.

Man sieht, die gesamte Nachricht ist auch ein JSON Object, mit verschiedenen verschachtelten Objekten. Wir können also beliebig Dinge hinzufügen.

Dann packen wir doch mal den HTTP Request dazu. Wir können diesen einfach auf die bestehende Verbindung zwischen den Nodes ziehen. Diese wird dann gestrichelt.

Bestätigen mit Fertig – nochmal Deployen und dann wieder aufs blaue Quadrat.

Jetzt sehen wir, dass im Payload der HTML Code der Website ist. Ziemlich Cool 🙂 Wir brauchen aber ja nur ein paar kleine Elemente, da kommt der HTML Node ins spiel. wir können den wieder zwischen Debug und vorherigem Node platzieren. Dann klicken wir wieder auf Deploy und starten erneut einen Nachrichtendurchlauf (blaues Quadrat am Inject-Node)

Nun haben wir ein Array aus allen Titeln auf der Wunschliste. Sehr cool! Genau was der Scrape Sensor nicht so leicht konnte.

Function Node

Hier sieht man jetzt jede Menge Sonderzeichen, die wir nicht brauchen, also geht es mit dem nächsten Node weiter. Diesmal nutzen wir echtes JavaScript um mit einer for Schleife alle Elemente im Array einzeln zu bearbeiten und unerwünschte Zeichen zu entfernen.

Das ist der Code, den ich hier ausführe:

msg.headers = msg.originalHeaders;
var titles = msg.payload;
var newPayload = [];
for (var i = 0; i < titles.length ; i++) {
newPayload.push( String(titles[i].replace(/\n/g, "").replace(/\t/g, "").replace(/\n/g, "") )
);
}
msg.payload = newPayload;
msg.topic = "titles";
return msg;

Wir wollen die alten header mit durchschleifen, daher kann man Zeile eins benutzen.
var titles ist meine Variable, um den eben erzeugten Array zu speichern. Wir übergeben den Payload aus der Nachricht an die Variable.

var newPayload = []; erzeugt ein leeren Array, in dem wir später die aufgeräumten Werte aus dem Array speichern.

Dann kommt die Vorschleife. In den Bedingungen wird geprüft, ob der Zähler i kleiner als die Anzahl der Elemente im Array (mit .length abgefragt) ist und so lang mit einem 1er (i++) Schritt hochgezählt, bis die Bedingung nicht mehr erfüllt ist.

Mit .push fügen wir in jedem Durchgang das Aktuelle titles Element [i] hinten an den Array an. Während wir das tun, nutzen wir die .replace() funktion, um die Zeilenumbrüche und Tabs zu löschen.

Danach übergeben wir unsere Aufgeräumten Daten wieder an das MSG-Objekt, genauer gesagt, an dessen Payload Objekt.

Außerdem legen wir ein topic fest. Das wird später noch wichtig.

Mit return msg wird die Nachricht an den nächsten Node weitergegeben.

Und so sieht der Debug aus:

Trouble

Hier war bei mir tatsächlich ein großer Stolperstein. Den zu fixen hat lange gedauert.

Und zwar hatte ich die Schleife ursprünglich aus dem Beispiel von noderedguide nachempfunden. Er pusht jedes Wertepaar direkt als JSON Objekt (sieht man an den geschweiften Klammern) in den Array.

Da mein erster Plan war, nur das Scrapen in HA auszuführen und den rest über ein Template im HomeAssistant zu machen, wollte ich die Strings speichern. Das hat aber partou nicht geklappt, da ich es nicht korrekt formatiert bekommen habe. Daher habe ich später einfach nur noch den jeweiligen Wert in den Array geschoben, ohne direkt ein JSON Objekt zu basteln.

Preis

Da das nun soweit gut funktioniert, können wir die letzten drei Nodes (also html, function und debug) markieren und mit strg + c, strg + v duplizieren. Zwei mal brauchen wir den, da Amazon den Preis auf dieser Seite in zwei Elementen speichert, die wir später zusammen fügen.

Wir haben ein Element für die Zahl vor dem Komma, und eines für die Zahl nach dem Komma. Die sind whole und fraction genannt worden, daher halte ich mich einfach mal an diese Bezeichnung.

whole

Wir tauschen im HTML Element einfach den CSS Selektor aus. Diesmal brauchen wir:

span.a-price-whole

Im Funktions-Node passen wir den Code ein wenig an:

msg.headers = msg.originalHeaders;
var whole = msg.payload;
var newPayload = [];
for (var i = 0; i < whole.length; i++) {
newPayload.push(Number(whole[i].replace(/\n/g,"").replace(/,/g,""))
);
}
msg.payload = newPayload;
msg.topic ="whole";
return msg;

fraction

Und das gleiche in Grün für das nächste Element:

CSS-Selektor:

span.a-price-fraction

Angepasster Function Node

msg.headers = msg.originalHeaders;
var fraction = msg.payload;
var newPayload = [];
for (var i = 0; i < fraction.length; i++) {
newPayload.push(
Number(fraction[i].replace(/\n/g, " "))
);
}
msg.payload = newPayload;
msg.topic = "fraction";
return msg;

Und so sieht der zwischenstand bei mir aktuell aus:

Das ganze können wir wieder einmal testen um sicher zu gehen, dass auch alles läuft. Wir prüfen auch, ob die Topics korrekt gesetzt werden. Ich stelle die Debug-Nodes danach dann auch wieder zurück auf den reinen Nachrichteninhalt.

Der nächste Schritt ist das Kombinieren der Preis-Arrays zu einem ganzen Preis. Das bringt einen kleinen Kniff mit sich. Ich hab zunächst einen weiteren Function-Node mit einer For Schleife aufgebaut, um für jedes Array Element die beiden Werte zusammen zu fügen, aber habe immer die Fehlermeldung bekommen, dass das Array Leer sei.

Trouble again

NodeRED basiert ja darauf, dass Nachrichten durch das System geschickt werden. Wenn wir nun aus den beiden Preisfragmenten den echten Preis bauen wollen, müssen wir beide Nachrichten gleichzeitig in einem Node bearbeiten. Wenn wir also eine Entsprechende Funktion als function node haben, wird es nicht funktionieren, weil die beiden Arrays nicht zum selben Zeitpunkt am Node ankommen. Eins der beiden Arrays ist also immer leer. Da hilft der Join Node.

Join-Node

Jetzt wird es interessant, denn der Join Node ist eines der zickigsten Elemente hier. Wir setzen zunächst den Modus auf Manuell und stellen den Join Node so ein dass er den Payload jeder Nachricht die ankommt zu einem neuen Array zusammenbaut. Dazu braucht der Join die Topics, die wir in den clean Array Funktionen eingebaut haben.

Der Join-Node verlangt außerdem einen Parameter, wann er selbst beginnt, Messages zu senden. Also muss hier bei „Nach einer Anzahl von Nachrichtenteilen“ eine 3 Stehen. (Die 4 kommt später, wenn wir auch noch die Artikel-Urls scrapen). Außerdem setzen wir den Zeitablauf nach erster Nachricht auf 1. Ehrlichgesagt weiß ich nicht genau was das macht, aber wenn man die beiden Felder nicht definiert, Enden die Nachrichten hier und es geht nicht weiter. Musste ich auch auf die harte Tour lernen…

Vergesst nicht die Ausgänge der Funktionsnodes (alle drei) hinzuzufügen. Außerdem müssen wir wieder einen Debug-Node anhängen, der uns die neue Nachricht anzeigt. Die anderen Debug-Nodes habe ich hier deaktiviert (grünes Quadrat am Node rechts).

Wir sehen nun (ich gebe das ganze Message Objekt aus) – das im Payload drei Unter-Objekte mit den ehemaligen Topics vorhanden sind und das Message-Objekt nur noch ein Topic hat. In den Unterobjekten finden wir unsere Arrays mit den ausgelesenen Werten.

Funktionsnode

Der nächste Funktionsnode bastelt nun tatsächlich aus den zwei Preisfragmenten einen Preis als Float-Kommazahl. Der Node wird wieder hinter den letzten Join gehängt und der Code lautet bei mir wie folgt:

msg.headers = msg.originalHeaders;
var fraction = msg.payload.fraction;
var whole = msg.payload.whole;
var titles = msg.payload.titles;
var newPayload = [];
for (var i = 0; i < fraction.length ; i++ ) {
var price = String(whole[i]+"."+fraction[i]);
//var string = String(String(titles[i])+":"+price);
newPayload.push(Number(price));
}
msg.payload = {};
msg.payload.prices = newPayload;
msg.payload.titles = titles;
msg.topic = "price";
return msg;

Mit msg.payload.fraction, msg.payload.titles und msg.payoad.whole kann ich auf die jeweiligen Objekte im MSG Objekt zugreifen. Diese speichere ich in var fraction etc.

Außerdem erzeugen wir ein leeres newPayload Array mit []

In einer Schleife gehen wir durch jedes Array-Objekt und basteln und stumpf einen String aus beiden Preisteilen, die wir mit einem Punkt trennen. Danach wandeln wir das zu einer Nummer um und hängen es jeweils an das newPayload Array an.

Der Rest der Funktion ist wieder einfach die neue Nachricht zu erstellen.

Um dies erneut zu testen, kommt wieder ein Debug-Node dahinter und wir können den Trigger aktivieren und das Message-Objekt durchlaufen lassen. Im Debugging Fenster rechts sehe ich nun folgendes:

Das Message Objekt hat jetzt zwei Unter-Objekte, ein Array mit Preisen und eines mit Titeln. Cool!

Funktions Node – Preis Vergleich

Als nächstes wollen wir den Array mit den Preisen untersuchen. Dazu brauchen wir natürlich einen Grenzwert. Diesen bekomme ich aus einer input_number im Homeassistant. Die Entität kann ich auf meinem Dashboard mit einem Slider einstellen. Um diese im NodeRED einzulesen, kann man einfach einen Befehl nutzen und braucht keinen Extra Node dazu.

global.get('homeassistant.homeAssistant.states["input_number.amazon_treshold_price"].state');

Die Funktion sieht also so aus:

msg.headers = msg.originalHeaders;
var price = msg.payload.prices;
var titles = msg.payload.titles;
var minprice;
var index;
var title;
var tresprice = global.get('homeassistant.homeAssistant.states["input_number.amazon_treshold_price"].state');
var success = false;
for (var i = 0; i < price.length; i++) {
if (price[i] <= tresprice){
minprice = Number(price[i]);
index = i;
title = titles[i];
success = true;
msg.minprice = minprice;
msg.minindex = index;
msg.mintitle = title; 
msg.success = success;  
node.send(msg);     
}
}

Wir ziehen oben wieder die Werte aus dem Message Objekt und aus dem Grenzwert-Slider. Hier könnt ihr aber auch einfach einen Wert HardCoden.

In der for Loop vergleichen wir jedes Element im Array mit dem Threshold-Preis. Wenn die If-Funktion true wird – also der Preis im Array tatsächlich kleiner gleich Threshold-Preis ist, speichern wir die Element-ID [i] in eine Variable minprice. Außerdem den Titel zu dieser ID. Ich speichere hier sogar i als INdex, bin mir aber gerade nicht sicher ob ich das später noch benutze. Außerdem habe ich einen boolean success angelegt, mit dem ich später das Ausgeben der Push-Notification steuere. Bei jedem positiven durchlauf der if-funktion, also immer wenn ein günstiger Artikel gefunden wurde, wird eine Nachricht an den nächsten Node weiter gegeben. Hier sind wir also bei mehreren Nachrichten im Strukturbaum, anstatt einen Array mit allen günstigen Büchern zu erstellen. Das fand ich schöner, da es dann auch mehrere Notifications gibt, anstatt alle in eine zu packen.

Notification

Jetzt fehlt natürlich noch die Formulierung der Nachricht. Ich will in der Nachricht ja den Preis und den Titel wissen. Dazu kann ich wieder einen Funktionsnode verwenden. Der Code sieht diesmal so aus:

var minprice = msg.minprice;
var index = msg.minindex;
var title = msg.mintitle;
var success = msg.success;
var message = {};
if(success == true){
message = String("Das Buch" + title + "ist für " + minprice + " € erhältlich");
}else{message = String("Leider kein Rabatt heute");}
var notification = {
data: {
"message": message,
"title": "Price Alert",        
}
}
msg.payload = notification;
return msg;

Nachdem ich wie gehabt wieder die Elemente aus dem Message Objekt kopiere, habe ich den Trigger für die eigentliche Notification und das finale Message Objekt das ans Handy geht. Das ist einfach ein String, der sich aus Text und Variablen mit dem Titel und dem Preis zusammensetzt. Das kann man natürlich auch direkt im vorherigen Funktionsnode umsetzen, so finde ich es aber erstmal verständlicher zum Nachmachen.

if(success == true){
message = String("Das Buch" + title + "ist für " + minprice + " € erhältlich");
}else{message = String("Leider kein Rabatt heute");}

Wenn success – also ein Element gefunden wurde, das unter dem Grenzpreis liegt, wird der String aufgbaut, wenn nicht, kommt „Leider kein Rabatt heute“ raus. Dieser Teil diente lediglich zu debug zwecken und kann auch entfernt werden.

JSON

Im Teil var notification…. ist die Formatierung sehr wichtig, da es sich hier um ein JSON Objekt handelt. Wenn das nicht passt, bekommt ihr eine Fehlermeldung.

Switch Node

Als nächstes brauchen wir einen Switch-Node, der je nach dem Status des boolean success das Message Objekt weiterleitet oder nicht. Wir setzen einen Ausgang mit is true und einen mit is false. An den True ausgang kommt der nächste Node. Bei is false könnt ihr auch was eigenes machen, da kommt bei mir aber nichts.

Call Service Node

Der fast letzte node ist nun der Call Service node. Dieser kann den notification Service im HomeAssistant aufrufen und die Daten übergeben, die wir erzeugt haben. Bei mir wird an mein Handy der Payload des Message-Objekt übergeben, den wir ja schon als JSON Objekt im Funktionsnode aufgebaut haben. Natürlich würde es auch gehen hier erst das JSON Objekt zusammen zu setzen.

Actionable Notification

Nach dem alles läuft habe ich noch weitere Features eingebaut. Und zwar Scrape ich nun auch die URL – schleife diese durch alle Nodes und lasse mir diese als Action-Button in der Notification ausgeben. Diese Version ist oben bereits im gesamt-Flow eingebaut.

Manual Trigger mit Poll State Node

Außerdem habe ich einen manuellen Trigger Button hinzugefügt. Dazu nutze ich den Poll-State Node. Dieser Überwacht den Status eines input_boolean aus Home Assitant. Den Input-Boolean steuere ich mit einem template switch. Dieser Poll State Node feuert dann einen Trigger-Node. Der Manuelle Trigger wird einfach parallel zum Inject Node eingereiht.

Wishlist-Pagination

Wahrscheinlich ist euch schon aufgefallen, dass nur die ersten 10 Produkte auf der WIshlist getrackt wurden. Das liegt daran, dass erst beim Scrollen weitere nachgeladen werden. Wenn ihr in den Entwicklertools die Seite beobachtet könnt ihr im Netzwerk reiter sehen, wie weitere Produkte nachgeladen werden. Den Link der hier abgefragt wird können wir einfach in einen neuen Get Request kopieren. Natürlich muss für jedes Nachladen ein Request-Node eingebaut werden und diese müssen mit allen HTML und Trigger Nodes verbunden werden. Das wird leider etwas wild:

Speichern der Arrays

Ich hab außerdem Versucht die Arrays mit Titel und Preis zu speichern. Da muss man aber wahrscheinlich auch eher für jedes Wertepaar einen eigenen input_text anlegen, da sonst die Speichergröße überschritten wird. Das wollte ich für ein Preis-Tracking machen, bin hier aber noch nicht weiter gekommen. Vll hat da jemand von euch einen schlauen Einfall.

Im Einsatz

So sieht das ganze dann am Smartphone aus:

Ich hoffe ich konnte euch so etwas die Konzepte von NodeRED näherbringen und zeigen wie das ganze mit HomeAssistant zusammenspielt. Wie immer: Ich bin hier kein Experte sondern hier gilt auch für mich – learning by doing. Ich versuche Fragen aber natürlich so gut es geht zu beantworten.

$ 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