IFTTT im Selbstbau
Ich kann mich an wenige Webdienste erinnern, die so verheißungsvoll gestartet sind und am Ende so enttäuschend waren, wie IFTTT1 (If This Than That). Die Idee war brilliant. Mit einfachsten "Wenn" => "Dann" Verknüpfungen kann man unterschiedlichste Geräte und Dienste miteinander Verknüpfen. Und das funktioniert auch heute noch wunderbar. Allerdings ist die Webseite irgendwie in die Jahre gekommen und der ganze Workflow fühlt sich an vielen Stellen etwas hakelig an. Und mit den Jahren sind auch irgendwie die Ideen gewachsen, was man mit einem solchen Tool alles machen könnte. Leider ist IFTTT da aber nie so richtig mitgewachsen. Ja, es sind etwas komplexere Möglichkeiten der Verknüpfung hinzugekommen, aber das konnte dennoch nicht so wirklich mit den Ideen mithalten. Die Anzahl der verfügbaren Hardware- und Webserviceintegration ist zwar über die Jahre immer weiter gewachsen, aber vieles davon gibt es nur auf dem US-Markt und/oder orientiert sich sehr an kommerziellen Produkten.
Das kostenlose Paket wurde mit der Zeit immer weiter eingedampft, bis man irgendwann kaum noch etwas sinnvolles damit tun konnte, dafür wurden die Preise für die kostenpflichtigen Pakete erhöht. Und auch wenn diese unterm Strich noch in Ordnung sind, habe ich mich irgendwann nach Alternativen umgeschaut und bin auf Huginn
2 gestoßen.
Vom Userinterface her mag Huginn zunächst wie ein Rückschritt wirken, aber es ist auch ungleich mächtiger. Die Dokumentation könnte an einigen Stellen etwas besser sein, aber mit ein wenig herumprobieren, kommt man schnell zu brauchbaren Ergebnissen. Ich nutze Huginn hauptsächlich um RSS Feeds zu verabeiten, bzw. um Webseiten die keinen RSS-Feed anbieten auszulesen und mir daraus einen RSS-Feed selbst zu erzeugen. Man kann aber über MQTT auch mit seinen SmartHome-Geräten interagieren.
Die Bereitstellung von Huginn kann schnell und einfach mittels Docker erfolgen:
version: '2'
services:
mysqldata:
image: mysql:5.7
command: /bin/true
mysql:
image: mysql:5.7
restart: always
env_file:
- ../mysql.env
volumes_from:
- mysqldata
web:
image: ghcr.io/huginn/huginn-single-process
restart: always
ports:
- "3000:3000"
env_file:
- ../mysql.env
- ../secrets.env
depends_on:
- mysql
threaded:
image: ghcr.io/huginn/huginn-single-process
command: /scripts/init bin/threaded.rb
restart: always
env_file:
- ../mysql.env
- ../secrets.env
depends_on:
- mysql
- web
Vor dem Start erstellt man noch die Dateien mysql.env
und secrets.env
:
MYSQL_PORT_3306_TCP_ADDR=mysql
MYSQL_ROOT_PASSWORD=64zeichenrandom
HUGINN_DATABASE_PASSWORD=64zeichenrandom
HUGINN_DATABASE_USERNAME=root
HUGINN_DATABASE_NAME=huginn
APP_SECRET_TOKEN=128zeichenrandom
Nach einem docker-compose up
sollte uns die Huginn Weboberfläche unter http://localhost:3000
zur Verfügung stehen (der Port sollte ggf. angepasst werden, falls wir auf demselben Server Grafana betreiben möchten). Der erste Start dauert eine Weile, die zahlreichen Fehlermeldungen während der Startvorgangs kann man ignorieren.
Wie schon erwähnt, kann man mit Huginn prima RSS-Feeds für Seiten erzeugen, die keinen eigenen Feed anbieten. Wer RSS3 nicht kennt, damit kann man Webseiten quasi als Newsticker nutzen. Sobald ein neuer Artikel veröffentlicht wird, werden dessen Daten (oft nur als Zusammenfassung, manchmal aber auch der ganze Artikel) auch im RSS-Feed veröffentlicht. Ein spezieller RSS-Reader4 fragt diesen Feed in regelmäßigen Abständen ab und sagt Bescheid, wenn es etwas neues gibt. Es ist mir sehr unerklärlich, warum RSS ein wenig aus der mode gekommen ist. aber zum Glück bieten viele seiten es immer noch an. Ich konsumiere fast alle meine Nachrichten darüber. aber zurück zum Thema.
Ein paar seiten bieten eben leider keine RSS-Feeds an. Aber mit Huginn kann man über den sog. Website Agent
sehr einfach Webseiten laden und in ihre Bestandteile zerlegen. Die Konfiguration eines solchen Agents sieht dann z.B. so aus:
{
"url": "https://what-if.xkcd.com",
"mode": "on_change",
"expected_update_period_in_days": "365",
"extract": {
"question": {
"css": "#question",
"value": "string(.)"
},
"attribute": {
"css": "#attribute",
"value": "string(.)"
},
"answer": {
"xpath": "//*[@id=\"entry\"]/p[3]",
"value": "string(.)"
},
"img": {
"xpath": "//*[@id=\"entry\"]/img[1]",
"value": "@src"
}
}
}
Im ersten Moment mag das sehr kryptisch aussehen, aber schauen wir uns das einfach mal im Einzelnen an:
- Die
url
gibt die Seite an, die abgefragt werden soll. in diesem fall alsohttps://what-if.xkcd.com/
- Der
mode
gibt an, wann der Agent tätig werden soll. Hieron_change
, also immer dann, wenn er eine Veränderung der seite feststellt - Die
expected_update_period_in_days
gibt an, wie oft wir mind. mit einer Aktivität des Agents rechnen (in diesem Fall also einer Änderung der überwachten Seite), löst der Agent länger keinen Event aus, dann wird er in der Übersicht von Huginn als defekt markiert - Im
extract
Abschnitt lesen wir die Teile der seite aus, die uns Interessieren. das geht ziemlich einfach, entweder über die ID5 eines HTML-Elements, die CSS-Klasse bzw. den CSS-Selector6 oder über den XPath7 unter 8 findet sich ein gutes Tutorial bezüglich XPath.
Der XPath sieht im ersten Moment manchmal etwas wild aus, aber man kann ihn meist einfach aus der Developerkonsole seines Browsers herauskopieren. Über die Developerkonsole gelangt man auch leicht an des CSS-Selector der gewünschten Elemente. Am einfachsten ist es natürlich auf Elemente über ihre ID zuzugreifen, dafür sind diese schließlich da. Leider setzen nicht alle Seiten diese Sinnvoll ein.
Aber schauen wir uns das einmal ganz Praktisch an: Wir wollen zunächst das Element mit dem CSS-Selector question
finden und dessen value
in Form eines Strings auslesen. string(.)
heißt hier, dass man den Inhalt des HTML Tags mit dem CSS-Selector question
ausliest:
"question": {
"css": "#question",
"value": "string(.)"
},
Im zweiten Schritt lesen wir das Element mit dem CSS-Selektor attribute
aus. Ebenfalls ein String:
"attribute": {
"css": "#attribute",
"value": "string(.)"
},
Nun wollen wir einen Teil der Seite auslesen, den wir answer
nennen. Dieser hat keinen CSS-Selektor, daher greifen wir hier auf den XPath zurück.
"answer": {
"xpath": "//*[@id=\"entry\"]/p[3]",
"value": "string(.)"
},
//*[@id="entry"]/p[3]
sagt, dass es ein Element mit der ID entry
gibt, dieses enthält mehrere <p>
Tags, wir möchten den Inhalt des 3. <p>
innerhalb des Elements entry
auslesen, erneut ein string
.
Außerdem wollen wir ein Bild aus dem Artikel extrahieren. Auch dieses befindet sich im Element entry
und wir möchten diesmal das Erste img
. Da ein <img>
Ttag in html aber keinen Inhalt hat, müssen wir ein Attribut des Tags auslesen, nämlich src
, daher geben wir hier bei value
nicht string(.)
an, sondern @src
:
"img": {
"xpath": "//*[@id=\"entry\"]/img[1]",
"value": "@src"
}
Der Agent liest also bei jeder Veränderung der Seite die question
, den Fragesteller (attribute
), den Beginn der answer
und das Erste img
im Artikel aus. Diese Infos können wir jetzt in Huginn an den sog. DataOutputAgent
weiterreichen. Dieser sieht wie folgt aus:
{
"expected_receive_period_in_days": 365,
"template": {
"title": "What if?",
"description": "serious answers to absurd questions and absurd advice for common concerns from xkcd's Randall Munroe",
"item": {
"title": "{{question}}",
"description": "{{answer}}",
"link": "{{img}}"
}
},
"ns_media": "true",
"secrets": [
"whatif"
]
}
Relevant ist vor allem der Abschnitt template
. Hier geben wir zunächst den title
und die description
für unseren RSS-Feed an. Dieser bleibt immer gleich. Im Abschnitt item
werden dann die vom Website Agent
gesammelten Informationen übergeben.
Der etwas irreführend benannte Abschnitt secrets
gibt den Namen der RSS-Datei an. Die Adresse des Feeds wird in der Detailansicht des Data Output Agents
angezeigt und kann dann einfach an den gewünschten RSS-Reader verfüttert werden. Neben dem normalen Feed in Form einer XML Datei, exportiert Huginn auch gleich noch eine JSON Entsprechung.
Der fertige RSS-Feed sieht dann in etwa so aus:
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>What if?</title>
<description>serious answers to absurd questions and absurd advice for common concerns from xkcd's Randall Munroe</description>
<link>https://localhost:3000</link>
<lastBuildDate>Tue, 27 Sep 2022 14:30:25 -0700</lastBuildDate>
<pubDate>Tue, 27 Sep 2022 14:30:25 -0700</pubDate>
<ttl>60</ttl>
<item>
<title>My daughter recently received her driver's permit in the US, and aspires to visit mainland Europe someday. She has learned enough about the rules of the road to know never to drive into the ocean; however, she jokingly suggested that given a sufficient quantity of rental cars, she could eventually get to Europe by driving east repeatedly. The question is, how many vehicles would it take to build a car-bridge across the Atlantic?</title>
<description>After extensive research, I can conclusively state that this would be a violation of your rental car agreement.</description>
<link>https://what-if.xkcd.com/imgs/a/160/rental.png</link>
<guid isPermaLink="false">2414</guid>
<pubDate>Tue, 27 Sep 2022 14:30:10 -0700</pubDate>
</item>
</channel>
</rss>
Der title
ist so nicht ganz optimal, weil sehr lang, man könnte hier noch eine Anpassung am Data Output Agent
vornehmen und statt:
"template": {
"title": "What if?",
"description": "serious answers to absurd questions and absurd advice for common concerns from xkcd's Randall Munroe",
"item": {
"title": "{{question}}",
"description": "{{answer}}",
"link": "{{img}}"
}
},
folgende Angabe nutzen:
"template": {
"title": "What if?",
"description": "serious answers to absurd questions and absurd advice for common concerns from xkcd's Randall Munroe",
"item": {
"title": "{{question | truncate: 64}}",
"description": "{{question}} - {{answer}} - https://what-if.xkcd.com",
"link": "{{img}}"
}
},
Hier würde durch | truncate: 64
der Titel auf max. 64 Zeichen gekürzt. Damit wir die Frage aber dennoch vollständig lesen können, packen wir sie zusammen mit dem Beginn der Antwort in die description.
Außerdem fügen wir noch einen Link zur Webseite hinzu, denn der Link im Feed zeigt ja auf das erste extrahierte Bild. In der Praxis würde man hier eher den Link zur Seite selbst eintragen. ich wollte das hier nur im Beispiel nutzen, um noch einen Fall zu haben, bei dem ein Attribut eines HTML-Tags ausgelesen wird.