Ich habe mir in letzter Zeit mal angeschaut, wie man ein E-Ink Display sinnvoll ins Smart Home einbinden kann, ohne dafür einen Raspberry Pi in die Wand zu schrauben oder irgendwelche fragwürdigen Display-Rahmen aus China zu bestellen. Die Antwort, die ich gefunden habe, heißt TRMNL. Und ich bin wirklich überrascht, wie gut das Ganze funktioniert.
Das TRMNL ist ein 7,5-Zoll-E-Ink-Display, das bei mir jetzt seit ein paar Wochen an der Wand hängt und mir auf einen Blick zeigt, was im Haus passiert. Temperatur, Solarertrag, Webseiten-Besucher, nächster Kalendereintrag, alles auf einen Blick, ohne Handy, ohne App, ohne nerviges Aufleuchten mitten in der Nacht. Einfach draufschauen.
Was das TRMNL eigentlich ist
Das Gerät ist im Grunde eine smarte Anzeige, mehr nicht. Hinten drauf ist ein An/Aus-Schalter und eine Ladebuchse. Keine Knöpfe, keine Touchfläche, nichts zum Drücken. Das klingt erstmal nach einer Einschränkung, ist aber tatsächlich genau das, was ich daran so schätze. Es ist einfach wahnsinnig entschleunigend, wenn du ein Gerät hast, das dir Infos zeigt und dich nicht gleichzeitig einlädt, daran rumzuspielen.
Das TRMNL OG kostet knapp 123 Dollar und ist in verschiedenen Farben verfügbar. Ich habe mir das Clarity Kit dazu geholt, das bringt einen praktischen Ständer und einen größeren Akku mit 2.500 mAh. Der Akku hält je nach Refresh-Intervall zwei bis sechs Monate, ohne aufgeladen werden zu müssen. Bei mir läuft es seit einer Woche mit 15-Minuten-Updates und steht noch bei über 70 Prozent. Wenn du bestellen möchtest, mit dem Code allautomatic bekommst du 10 Dollar Rabatt.
Die Software, die auf dem Gerät läuft, ist zu 100 Prozent Open Source. Das hat den großen Vorteil, dass es eine wachsende Community gibt, die laufend neue Plugins und Blueprints beisteuert. Kalender, Spotify, E-Mails, YouTube, das meiste davon ist bereits als One-Click-Setup verfügbar.
Die native Home Assistant Integration
Seit einem der letzten Updates gibt es eine offizielle TRMNL-Integration direkt in Home Assistant. Die findest du unter Einstellungen → Geräte und Dienste. Du gibst dort einfach deinen API-Schlüssel ein, den du auf der TRMNL-Kontoseite findest, klickst auf okay, und das war es eigentlich schon.
Die Integration bringt sieben Entitäten mit. Ruhezustand ein und aus, Batteriespannung, WLAN-Signalstärke, Linkspeed und ein paar Diagnosewerte. Die meisten davon sind standardmäßig deaktiviert. Ich lasse das so, weil ich mir nicht vorstelle, dass mich der Linkspeed des Displays täglich interessiert. Praktisch ist aber, dass du dir über die Batterie eine Benachrichtigung einrichten könntest, wenn der Ladestand zu weit sinkt.
Für das eigentliche Dashboard, also was auf dem Display angezeigt wird, brauche ich die native Integration allerdings nicht. Dafür gibt es einen anderen Weg.
Dein eigenes Dashboard per Webhook
Das Prinzip ist einfach: Home Assistant macht alle 15 Minuten einen POST-Request an die TRMNL Webhook-URL und schickt dabei alle Daten als JSON mit. TRMNL rendert daraus ein Bild und schickt es ans Display. Keine Screenshots, kein Browser, kein Puppeteer. Sauber und zuverlässig.
Für das Setup brauchst du das TRMNL Developer Add-on, das einmalig 20 Dollar kostet. Damit kannst du ein privates Plugin anlegen. Du wählst als Strategy Webhook aus, gibst dem Plugin einen Namen und notierst dir die Webhook-UUID. Die brauchst du gleich in Home Assistant.
Auf der TRMNL-Seite baust du dann das Layout im sogenannten Markup-Editor zusammen. Das Template nutzt das TRMNL Framework v2 mit Liquid-Syntax. Variablen kommen als {{ variable_name }} rein und werden zur Laufzeit durch die Werte ersetzt, die Home Assistant schickt.
Mein Dashboard ist in vier Quadranten aufgeteilt: Smart Home oben links, YouTube oben rechts, Webseiten unten links und persönliche Daten unten rechts. Das komplette Markup sieht so aus:
1<style>
2 .qg { display:grid; grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr; width:100%; height:100%; }
3 .qc { padding:6px 10px; overflow:hidden; display:flex; flex-direction:column; gap:3px; }
4 .qc--r { border-left:1.5px solid #000; }
5 .qc--b { border-top:1.5px solid #000; gap:5px; }
6 .row { display:flex; justify-content:space-between; align-items:baseline; }
7 .spark { display:flex; align-items:flex-end; gap:1px; height:12px; flex-shrink:0; }
8 .qc .label--small { font-size:18px; }
9</style>
10
11<div class="view view--full">
12 <div class="layout layout--col layout--stretch">
13 <div class="qg">
14
15 <!-- SMART HOME -->
16 <div class="qc">
17 <span class="label label--small label--underline">SMART HOME</span>
18 <div class="row">
19 <span class="label label--small">Verbrauch</span>
20 <span class="value value--xsmall value--tnums">{{ power_current }} W</span>
21 </div>
22 <div class="row">
23 <span class="label label--small">Produktion</span>
24 <span class="value value--xxsmall value--tnums">{{ solar_production }} kW</span>
25 </div>
26 <div class="row">
27 <span class="label label--small">Erzeugt</span>
28 <span class="value value--xxsmall value--tnums">{{ energy_today }} kWh</span>
29 </div>
30 <div class="row">
31 <span class="label label--small">{{ grid_label }}</span>
32 <span class="value value--xxsmall value--tnums">{{ grid_value }} W</span>
33 </div>
34 <span class="label label--small">
35 {% if energy_balance_positive == "true" %}+{{ energy_balance }} kWh eingespeist
36 {% else %}-{{ energy_balance }} kWh bezogen{% endif %}
37 </span>
38 <div class="progress-bar progress-bar--xsmall">
39 <div class="content">
40 <span class="label label--small">Batterie</span>
41 <span class="value value--xxsmall">{{ battery_level }}%</span>
42 </div>
43 <div class="track"><div class="fill" style="width:{{ battery_level }}%"></div></div>
44 </div>
45 <div class="row">
46 <span class="label label--small">{{ power_price }} €/kWh</span>
47 <span class="label label--small">{{ temp_indoor }}° / {{ temp_outdoor }}°</span>
48 </div>
49 </div>
50
51 <!-- YOUTUBE -->
52 <div class="qc qc--r">
53 <span class="label label--small label--underline">YOUTUBE</span>
54 <span class="label label--small label--gray">ALLES AUTOMATISCH</span>
55 <div class="grid grid--cols-3">
56 <div class="item"><div class="content">
57 <span class="value value--xxsmall value--tnums">{{ aa_subs }}</span>
58 <span class="label label--small">Abos</span>
59 </div></div>
60 <div class="item"><div class="content">
61 <span class="value value--xxsmall value--tnums">{{ aa_views }}</span>
62 <span class="label label--small">Aufrufe</span>
63 </div></div>
64 <div class="item"><div class="content">
65 <span class="value value--xxsmall value--tnums">{{ aa_videos }}</span>
66 <span class="label label--small">Videos</span>
67 </div></div>
68 </div>
69 <div class="w-full border--h-1"></div>
70 <span class="label label--small label--gray">KAMERAKRAM</span>
71 <div class="grid grid--cols-3">
72 <div class="item"><div class="content">
73 <span class="value value--xxsmall value--tnums">{{ kk_subs }}</span>
74 <span class="label label--small">Abos</span>
75 </div></div>
76 <div class="item"><div class="content">
77 <span class="value value--xxsmall value--tnums">{{ kk_views }}</span>
78 <span class="label label--small">Aufrufe</span>
79 </div></div>
80 <div class="item"><div class="content">
81 <span class="value value--xxsmall value--tnums">{{ kk_videos }}</span>
82 <span class="label label--small">Videos</span>
83 </div></div>
84 </div>
85 </div>
86
87 <!-- WEBSITES -->
88 <div class="qc qc--b">
89 <span class="label label--small label--underline">WEBSITES</span>
90 <div class="row">
91 <span class="title title--small">pixelgranaten</span>
92 <span class="value value--xxsmall value--tnums">{{ cf_pg_h }}</span>
93 </div>
94 <div style="display:flex;align-items:center;gap:6px">
95 <div class="spark" id="spark-pg"></div>
96 <span class="label label--small">{{ cf_pg_g }} gest. / {{ cf_pg_m }} 30d</span>
97 </div>
98 <div class="row">
99 <span class="title title--small">danielboberg</span>
100 <span class="value value--xxsmall value--tnums">{{ cf_db_h }}</span>
101 </div>
102 <div style="display:flex;align-items:center;gap:6px">
103 <div class="spark" id="spark-db"></div>
104 <span class="label label--small">{{ cf_db_g }} gest. / {{ cf_db_m }} 30d</span>
105 </div>
106 <div class="row">
107 <span class="title title--small">brick-storage</span>
108 <span class="value value--xxsmall value--tnums">{{ cf_bs_h }}</span>
109 </div>
110 <div style="display:flex;align-items:center;gap:6px">
111 <div class="spark" id="spark-bs"></div>
112 <span class="label label--small">{{ cf_bs_g }} gest. / {{ cf_bs_m }} 30d</span>
113 </div>
114 </div>
115
116 <!-- PERSÖNLICH -->
117 <div class="qc qc--r qc--b">
118 <span class="label label--small label--underline">PERSÖNLICH</span>
119 <div class="row">
120 <span class="value value--xsmall value--tnums">{{ weather_temp }}° {{ weather_desc }}</span>
121 </div>
122 <span class="label label--small">{{ weather_max }}°/{{ weather_min }}° · {{ sunrise }}/{{ sunset }}</span>
123 <div class="w-full border--h-1"></div>
124 <span class="title title--small">{{ next_event_time }} {{ next_event }}</span>
125 <div class="w-full border--h-1"></div>
126 <div class="progress-bar progress-bar--xsmall">
127 <div class="content">
128 <span class="label label--small">Skaten {{ skate_km }}/{{ skate_goal }} km</span>
129 <span class="value value--xxsmall">{{ skate_pct }}%</span>
130 </div>
131 <div class="track"><div class="fill" style="width:{{ skate_pct }}%"></div></div>
132 </div>
133 <span class="label label--small">{{ skate_remaining }} km, {{ skate_sessions }} Einheiten</span>
134 {% if next_birthday != blank %}
135 <div class="w-full border--h-1"></div>
136 <span class="label label--small">{{ next_birthday_date }} {{ next_birthday }}</span>
137 {% endif %}
138 </div>
139
140 </div>
141 </div>
142
143 <div class="title_bar">
144 <span class="title">{{ greeting }}</span>
145 <span class="instance">{{ updated_at }}</span>
146 </div>
147</div>
148
149<script>
150document.addEventListener("DOMContentLoaded", function() {
151 function renderSparkline(id, dataStr) {
152 var el = document.getElementById(id);
153 if (!el || !dataStr) return;
154 var vals = dataStr.split(",").map(function(v) { return parseInt(v) || 0; });
155 var max = Math.max.apply(null, vals) || 1;
156 for (var i = 0; i < vals.length; i++) {
157 var bar = document.createElement("div");
158 bar.style.flex = "1";
159 bar.style.background = "#000";
160 bar.style.minWidth = "2px";
161 bar.style.height = Math.max(Math.round((vals[i] / max) * 12), 1) + "px";
162 el.appendChild(bar);
163 }
164 }
165 renderSparkline("spark-pg", "{{ spark_pg }}");
166 renderSparkline("spark-db", "{{ spark_db }}");
167 renderSparkline("spark-bs", "{{ spark_bs }}");
168});
169</script>Home Assistant: Webhook und Automation
In Home Assistant brauchst du einen rest_command, der die Daten an TRMNL schickt, und eine Automation, die das alle 15 Minuten anstößt. Ich habe das als Package in /config/packages/trmnl_dashboard.yaml abgelegt.
Der REST Command sieht so aus:
1rest_command:
2 trmnl_webhook:
3 url: "https://trmnl.com/api/custom_plugins/DEINE_WEBHOOK_UUID"
4 method: POST
5 content_type: "application/json"
6 payload: '{"merge_variables": {{ merge_variables }} }'Die DEINE_WEBHOOK_UUID ersetzt du natürlich durch die UUID aus deinem TRMNL-Plugin. Und die Automation, die das regelmäßig auslöst:
1automation:
2 - alias: "TRMNL Dashboard Update"
3 trigger:
4 - platform: time_pattern
5 minutes: "/15"
6 action:
7 - service: rest_command.trmnl_webhook
8 data:
9 merge_variables: >
10 {
11 "power_current": "{{ states('sensor.solaredge_stromverbrauch') }}",
12 ... alle weiteren Variablen ...
13 }Die vollständige Liste der Variablen ist ziemlich lang. Ich schicke da Stromverbrauch, Solarproduktion, Tagesertrag, Netzbezug, Batteriestand, Strompreis, Innen- und Außentemperatur, YouTube-Statistiken für zwei Kanäle, Webseiten-Besucher aus der Cloudflare API, Wetterdaten von Open-Meteo, den nächsten Kalendertermin, meinen Skating-Fortschritt und den nächsten Geburtstag. Das klingt nach viel, aber die meisten Werte kommen direkt aus vorhandenen Home-Assistant-Sensoren. YouTube und Cloudflare brauchen je einen REST-Sensor mit API-Key, Open-Meteo ist kostenlos ohne Key.
Für die Sparkline-Charts der Webseiten-Besucher brauchst du Template-Sensoren, die die letzten sieben Tageswerte als kommaseparierte Liste zusammenbauen, zum Beispiel 120,150,180,95,210,175,160. Das JavaScript im Template rendert daraus dann die kleinen Balkendiagramme.
Was bei der TRMNL Webhook-Variante zu beachten ist
Ein paar Dinge, die ich beim Einrichten gelernt habe. Der Payload darf nicht größer als 2 KB sein. Wenn du sehr viele Variablen schickst oder lange Texte verwendest, kann das eng werden. Das TRMNL+ Modell hat ein höheres Limit von 5 KB, falls das bei dir ein Thema wird.
Bei den Progress-Bars ist ein häufiger Fehler, den content-Wrapper wegzulassen. Ohne den rendert TRMNL die Fortschrittsbalken nicht. Das ist in der Dokumentation ehrlich gesagt nicht super offensichtlich, deswegen erwähne ich das hier.
Wenn das Display leer bleibt, lohnt es sich als erstes, den Webhook manuell aus den Home-Assistant-Entwicklerwerkzeugen heraus aufzurufen und dann in der TRMNL-Plugin-Vorschau zu prüfen, ob die Daten ankommen. Das schließt die meisten Fehlerquellen direkt aus.
Für den Cronjob-Zeitplan in der TRMNL-App oder beim Screenshot-Plugin gibt es übrigens Crontab Guru, das ist ein wahnsinnig praktisches kleines Tool, wenn du die Notation nicht auswendig kannst. Einfach klicken und den generierten String übernehmen.
Das Framework und eigene Plugins
Das TRMNL Framework v2 bringt eine ganze Reihe fertiger Komponenten mit: Fortschrittsbalken, Grids, Label-Value-Paare, Trennlinien. Das meiste davon benutze ich in meinem Dashboard und brauche dafür kaum eigenes CSS. Die sechs Custom-CSS-Regeln, die ich dazu geschrieben habe, sind nur für das 2x2-Grid-Layout, weil das Framework kein natives Vier-Quadranten-Layout für den Full View mitbringt.
Wenn du dir ein eigenes Plugin bauen willst, arbeitest du mit Basic HTML, Liquid-Templates und dem Framework-CSS. Wer schon mal eine einfache Webseite gebaut hat, wird sich da schnell zurechtfinden. Du kannst das Plugin privat lassen oder es als Blueprint für andere veröffentlichen, ganz wie du möchtest.
Ich habe neben diesem Dashboard noch ein zweites, komplett eigenes Plugin in Arbeit, das Daten aus mehreren verschiedenen Quellen zusammenzieht. Das ist ein bisschen komplizierter und kommt in einem eigenen Artikel. Aber allein mit dem, was ich dir hier gezeigt habe, kannst du dir auf jeden Fall schon mal ein richtig nützliches Display einrichten.
Hast du das TRMNL schon im Einsatz oder überlegst du, dir eins zu holen? Was würdest du dir darauf anzeigen lassen?
