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. Ohne Handy, ohne App, ohne nerviges Aufleuchten mitten in der Nacht. Einfach draufschauen.
Hier zeige ich dir zwei Wege, wie du das TRMNL mit Home Assistant verbindest. Der einfache: Du baust ein Dashboard in HA und lässt es per Screenshot aufs Display schicken. Der fortgeschrittene: Du schickst die Daten direkt per Webhook und baust dir ein eigenes Plugin mit dem TRMNL Framework. Alle YAML-Configs sind zum Kopieren dabei.
Was du brauchst
- Ein TRMNL E-Ink Display (ab 123 Dollar, mit Code allautomatic 10 Dollar Rabatt)
- Eine laufende Home Assistant Installation
- Optional: Das Clarity Kit mit Ständer und größerem Akku (2.500 mAh, hält 2-6 Monate)
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 genau das, was ich daran so schätze. Es ist wahnsinnig entschleunigend, wenn du ein Gerät hast, das dir Infos zeigt und dich nicht gleichzeitig einlädt, daran rumzuspielen.
Die Software ist zu 100 Prozent Open Source. Es gibt eine wachsende Community, die laufend neue Plugins und Blueprints beisteuert. Kalender, Spotify, E-Mails, YouTube, das meiste davon ist bereits als One-Click-Setup verfügbar.
Schritt 1: TRMNL mit Home Assistant verbinden
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 deinen API-Schlüssel ein (findest du auf der TRMNL-Kontoseite unter Settings → API), klickst auf okay, fertig.
Die Integration bringt sieben Entitäten mit: Ruhezustand, Batteriespannung, WLAN-Signalstärke, Linkspeed und ein paar Diagnosewerte. Die meisten davon sind standardmäßig deaktiviert. Ich lasse das so. 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 nicht. Dafür gibt es zwei Wege.
Variante 1: Home Assistant Screenshot (einfach)
Die schnellste Methode: Du baust dir ein Dashboard in Home Assistant und lässt es automatisch als Screenshot aufs TRMNL schicken. Kein eigenes Plugin nötig, keine Webhook-Konfiguration.
Schritt 2: HA Screenshot Plugin aktivieren
- Gehe auf usetrmnl.com → Plugins
- Suche nach Home Assistant Screenshot und klicke auf Add
- Klicke auf Save, dann bekommst du eine Webhook URL angezeigt
- Kopiere diese Webhook URL, die brauchst du gleich
Schritt 3: TRMNL HA App installieren
In Home Assistant brauchst du die TRMNL HA App als Add-on. Die findest du unter Einstellungen → Add-ons → Add-on Store. Nach der Installation:
- Gehe auf Konfiguration der App
- Erstelle einen Long-Lived Access Token in HA: Klicke auf deinen Benutzernamen → Sicherheit → Token erstellen. Gib dem Token einen Namen (z.B. "TRMNL") und kopiere ihn
- Füge den Token in der App-Konfiguration ein
- Gib deine Home Assistant URL ein (normalerweise
http://homeassistant.local:8123) - Klicke auf Speichern und Starten
- Aktiviere Start beim Systemstart und Watchdog, damit die App immer läuft
Öffne dann die Benutzeroberfläche der App. Dort legst du einen neuen Schedule an:
- Gib die URL deines TRMNL-Dashboards ein (das bauen wir im nächsten Schritt)
- Füge die Webhook URL aus Schritt 2 ein
- Setze den Cron-Zeitplan auf
*/10 * * * *(alle 10 Minuten). Falls dir die Cron-Notation nichts sagt, hilft Crontab Guru - Klicke auf Save und teste mit Send Now
Schritt 4: Dashboard für das TRMNL bauen
Jetzt brauchst du ein eigenes HA-Dashboard, das für das E-Ink Display optimiert ist. Erstelle unter Einstellungen → Dashboards ein neues Dashboard (z.B. "TRMNL"). In diesem Dashboard legst du eine einzelne Markdown-Karte an.
Der Trick: In einer Markdown-Karte kannst du HTML und HA-Templates kombinieren. Die Sensoren werden direkt aus Home Assistant gelesen. Hier ist ein einfaches Beispiel-Dashboard, das Temperatur, Energie, Türen/Fenster und Wetter anzeigt:
1type: markdown
2content: >-
3 <div style="font-family:sans-serif;color:#000;padding:16px">
4 <h2 style="margin:0 0 12px 0;font-size:20px">Smart Home</h2>
5 <div style="display:flex;justify-content:space-between;margin-bottom:8px">
6 <span>Innen</span>
7 <strong>{{ states('sensor.temperatur_wohnzimmer') | round(1) }}°C</strong>
8 </div>
9 <div style="display:flex;justify-content:space-between;margin-bottom:8px">
10 <span>Außen</span>
11 <strong>{{ states('sensor.temperatur_aussen') | round(1) }}°C</strong>
12 </div>
13 <hr style="border:none;border-top:1px solid #ccc;margin:12px 0">
14 <div style="display:flex;justify-content:space-between;margin-bottom:8px">
15 <span>Verbrauch</span>
16 <strong>{{ states('sensor.stromverbrauch') | round(0) }} W</strong>
17 </div>
18 <div style="display:flex;justify-content:space-between;margin-bottom:8px">
19 <span>Solar</span>
20 <strong>{{ states('sensor.solar_produktion') | round(1) }} kW</strong>
21 </div>
22 <div style="display:flex;justify-content:space-between;margin-bottom:8px">
23 <span>Erzeugt heute</span>
24 <strong>{{ states('sensor.solar_energie_heute') | round(1) }} kWh</strong>
25 </div>
26 <hr style="border:none;border-top:1px solid #ccc;margin:12px 0">
27 <div style="display:flex;justify-content:space-between;margin-bottom:8px">
28 <span>Türen/Fenster</span>
29 <strong>
30 {% set offen = states.binary_sensor
31 | selectattr('attributes.device_class', 'eq', 'door')
32 | selectattr('state', 'eq', 'on') | list | count %}
33 {% if offen == 0 %}Alles zu{% else %}{{ offen }} offen{% endif %}
34 </strong>
35 </div>
36 <hr style="border:none;border-top:1px solid #ccc;margin:12px 0">
37 <div style="display:flex;justify-content:space-between;margin-bottom:4px">
38 <span>{{ states('weather.zuhause') }}</span>
39 <strong>{{ state_attr('weather.zuhause', 'temperature') | round(0) }}°C</strong>
40 </div>
41 <div style="font-size:14px;color:#666">
42 {{ now().strftime('%A, %d. %B %Y – %H:%M') }}
43 </div>
44 </div>Passe die Entity-IDs an dein Setup an. Die findest du unter Entwicklerwerkzeuge → Zustände. Du kannst dieses Dashboard beliebig erweitern, z.B. mit Müllabfuhr, Kalender oder Batteriestatus.
In der TRMNL HA App gibst du dann die URL dieses Dashboards ein, z.B. http://homeassistant.local:8123/lovelace-trmnl/0. Die App macht davon alle 10 Minuten einen Screenshot und schickt ihn an dein TRMNL Display. Fertig.
Tipp: In der App kannst du Breite und Höhe des Screenshots einstellen. Das TRMNL OG hat 800x480 Pixel. Mit den X/Y-Koordinaten kannst du auch nur einen Ausschnitt deines Dashboards anzeigen lassen. Wenn du mehrere Dashboards hast, leg einfach mehrere Schedules an. Die landen dann als Slides in deiner TRMNL-Playlist und rotieren automatisch durch.
Variante 2: Eigenes Plugin per Webhook (fortgeschritten)
Wenn du mehr Kontrolle willst, kannst du dir ein eigenes TRMNL-Plugin bauen. Home Assistant schickt die Daten direkt als JSON per Webhook an TRMNL, und das Layout baust du mit dem TRMNL Framework und Liquid-Templates. Kein Screenshot, keine HA-App. TRMNL rendert daraus ein Bild und schickt es ans Display.
Plugin anlegen
- Gehe auf usetrmnl.com → Plugins → Private Plugin
- Wähle als Strategy Webhook aus
- Gib dem Plugin einen Namen (z.B. "Home Dashboard")
- Notiere dir die Webhook-UUID aus der URL. Die sieht so aus:
https://usetrmnl.com/api/custom_plugins/abc123-deine-uuid. Du brauchst den Teil nach/custom_plugins/ - Klicke auf Markup und öffne den Markup-Editor
Dashboard-Markup einfügen
Im Markup-Editor baust du das Layout zusammen. Das Template nutzt das TRMNL Framework v2 mit Liquid-Syntax. Variablen kommen als {{ variable_name }} rein und werden 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önliches (unten rechts). Kopiere das folgende Markup komplett in den Editor:
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>Das Markup nutzt sechs eigene CSS-Regeln für das 2x2-Grid-Layout, weil das TRMNL Framework kein natives Vier-Quadranten-Layout mitbringt. Alles andere (Progress-Bars, Grids, Labels) kommt direkt aus dem Framework v2.
Wichtig: Wenn du nicht alle vier Quadranten brauchst, kannst du einzelne <div class="qc"> Blöcke einfach rauslöschen und das Grid auf grid-template-columns: 1fr anpassen.
REST Command anlegen
In Home Assistant brauchst du einen rest_command, der die Daten per POST an TRMNL schickt. Leg dafür eine neue Datei an unter /config/packages/trmnl_dashboard.yaml. Falls du noch keinen packages-Ordner hast, erstelle ihn und füge in deiner configuration.yaml folgendes ein:
homeassistant:
packages: !include_dir_named packagesDann kommt der REST Command in die Package-Datei:
1rest_command:
2 trmnl_dashboard:
3 url: "https://usetrmnl.com/api/custom_plugins/DEINE_WEBHOOK_UUID"
4 method: POST
5 content_type: "application/json"
6 headers:
7 Authorization: "Bearer DEIN_TRMNL_API_KEY"
8 payload: >-
9 {"merge_variables": {
10 "power_current": "{{ power_current }}",
11 "solar_production": "{{ solar_production }}",
12 "energy_today": "{{ energy_today }}",
13 "grid_label": "{{ grid_label }}",
14 "grid_value": "{{ grid_value }}",
15 "energy_balance": "{{ energy_balance }}",
16 "energy_balance_positive": "{{ energy_balance_positive }}",
17 "battery_level": "{{ battery_level }}",
18 "power_price": "{{ power_price }}",
19 "temp_indoor": "{{ temp_indoor }}",
20 "temp_outdoor": "{{ temp_outdoor }}",
21 "aa_subs": "{{ aa_subs }}",
22 "aa_views": "{{ aa_views }}",
23 "aa_videos": "{{ aa_videos }}",
24 "kk_subs": "{{ kk_subs }}",
25 "kk_views": "{{ kk_views }}",
26 "kk_videos": "{{ kk_videos }}",
27 "cf_pg_h": "{{ cf_pg_h }}",
28 "cf_pg_g": "{{ cf_pg_g }}",
29 "cf_pg_m": "{{ cf_pg_m }}",
30 "cf_db_h": "{{ cf_db_h }}",
31 "cf_db_g": "{{ cf_db_g }}",
32 "cf_db_m": "{{ cf_db_m }}",
33 "cf_bs_h": "{{ cf_bs_h }}",
34 "cf_bs_g": "{{ cf_bs_g }}",
35 "cf_bs_m": "{{ cf_bs_m }}",
36 "spark_pg": "{{ spark_pg }}",
37 "spark_db": "{{ spark_db }}",
38 "spark_bs": "{{ spark_bs }}",
39 "weather_temp": "{{ weather_temp }}",
40 "weather_desc": "{{ weather_desc }}",
41 "weather_max": "{{ weather_max }}",
42 "weather_min": "{{ weather_min }}",
43 "sunrise": "{{ sunrise }}",
44 "sunset": "{{ sunset }}",
45 "next_event_time": "{{ next_event_time }}",
46 "next_event": "{{ next_event }}",
47 "skate_km": "{{ skate_km }}",
48 "skate_goal": "{{ skate_goal }}",
49 "skate_pct": "{{ skate_pct }}",
50 "skate_remaining": "{{ skate_remaining }}",
51 "skate_sessions": "{{ skate_sessions }}",
52 "next_birthday_date": "{{ next_birthday_date }}",
53 "next_birthday": "{{ next_birthday }}",
54 "greeting": "{{ greeting }}",
55 "updated_at": "{{ updated_at }}"
56 }}Ersetze DEINE_WEBHOOK_UUID durch die UUID aus dem Abschnitt "Plugin anlegen" und DEIN_TRMNL_API_KEY durch deinen API-Key von der TRMNL-Kontoseite (Settings → API).
Wichtig: Nach dem Anlegen der Datei musst du Home Assistant einmal neu starten, damit das Package geladen wird. Das geht unter Entwicklerwerkzeuge → YAML → Konfiguration prüfen und dann Neu starten. Bei späteren Änderungen an der Automation reicht ein Reload unter Automationen neu laden.
Die Automation
Die Automation ruft den REST Command alle 15 Minuten auf und befüllt dabei alle Variablen mit echten Werten aus Home Assistant. Hier ist die komplette Automation, die du in die gleiche Package-Datei packst:
1automation:
2 - alias: "TRMNL: Dashboard Update"
3 description: "Sendet alle 15 Minuten Dashboard-Daten an das TRMNL Display"
4 triggers:
5 - trigger: time_pattern
6 minutes: "/15"
7 actions:
8 - action: rest_command.trmnl_dashboard
9 data:
10 # --- Smart Home (passe die Entity-IDs an dein Setup an) ---
11 power_current: "{{ states('sensor.stromverbrauch') | round(0) }}"
12 solar_production: "{{ states('sensor.solar_produktion') | round(1) }}"
13 energy_today: "{{ states('sensor.solar_energie_heute') | round(1) }}"
14 grid_label: >-
15 {% if states('sensor.netz_leistung') | float > 0 %}Bezug{% else %}Einspeisung{% endif %}
16 grid_value: "{{ states('sensor.netz_leistung') | float | abs | round(0) }}"
17 energy_balance: "{{ (states('sensor.einspeisung_heute') | float - states('sensor.netzbezug_heute') | float) | abs | round(1) }}"
18 energy_balance_positive: "{{ (states('sensor.einspeisung_heute') | float - states('sensor.netzbezug_heute') | float) > 0 }}"
19 battery_level: "{{ states('sensor.batterie_ladezustand') | round(0) }}"
20 power_price: "{{ states('sensor.strompreis') | round(2) }}"
21 temp_indoor: "{{ states('sensor.temperatur_wohnzimmer') | round(1) }}"
22 temp_outdoor: "{{ states('sensor.temperatur_aussen') | round(1) }}"
23
24 # --- YouTube (brauchst REST-Sensoren, siehe unten) ---
25 aa_subs: "{{ states('sensor.youtube_kanal_1_abonnenten') }}"
26 aa_views: "{{ states('sensor.youtube_kanal_1_aufrufe') }}"
27 aa_videos: "{{ states('sensor.youtube_kanal_1_videos') }}"
28 kk_subs: "{{ states('sensor.youtube_kanal_2_abonnenten') }}"
29 kk_views: "{{ states('sensor.youtube_kanal_2_aufrufe') }}"
30 kk_videos: "{{ states('sensor.youtube_kanal_2_videos') }}"
31
32 # --- Webseiten (brauchst REST-Sensoren, siehe unten) ---
33 cf_pg_h: "{{ states('sensor.cloudflare_seite_1_heute') }}"
34 cf_pg_g: "{{ states('sensor.cloudflare_seite_1_gestern') }}"
35 cf_pg_m: "{{ states('sensor.cloudflare_seite_1_monat') }}"
36 cf_db_h: "{{ states('sensor.cloudflare_seite_2_heute') }}"
37 cf_db_g: "{{ states('sensor.cloudflare_seite_2_gestern') }}"
38 cf_db_m: "{{ states('sensor.cloudflare_seite_2_monat') }}"
39 cf_bs_h: "{{ states('sensor.cloudflare_seite_3_heute') }}"
40 cf_bs_g: "{{ states('sensor.cloudflare_seite_3_gestern') }}"
41 cf_bs_m: "{{ states('sensor.cloudflare_seite_3_monat') }}"
42 spark_pg: "{{ states('sensor.sparkline_seite_1') }}"
43 spark_db: "{{ states('sensor.sparkline_seite_2') }}"
44 spark_bs: "{{ states('sensor.sparkline_seite_3') }}"
45
46 # --- Wetter ---
47 weather_temp: "{{ state_attr('weather.zuhause', 'temperature') | round(0) }}"
48 weather_desc: "{{ states('weather.zuhause') }}"
49 weather_max: "{{ state_attr('weather.zuhause', 'forecast')[0].temperature | default(0) | round(0) }}"
50 weather_min: "{{ state_attr('weather.zuhause', 'forecast')[0].templow | default(0) | round(0) }}"
51 sunrise: "{{ as_timestamp(state_attr('sun.sun', 'next_rising')) | timestamp_custom('%H:%M') }}"
52 sunset: "{{ as_timestamp(state_attr('sun.sun', 'next_setting')) | timestamp_custom('%H:%M') }}"
53 # Hinweis: Das forecast-Attribut wurde in neueren HA-Versionen (ab 2024.3) durch
54 # die Aktion weather.get_forecasts ersetzt. Wenn weather_max/weather_min bei dir
55 # "unknown" zeigen, nutze stattdessen einen Template-Sensor, der die Forecast-Aktion aufruft.
56
57 # --- Kalender ---
58 next_event_time: >-
59 {% set e = state_attr('calendar.privat', 'start_time') %}
60 {% if e %}{{ as_timestamp(e) | timestamp_custom('%H:%M') }}{% else %}--:--{% endif %}
61 next_event: "{{ state_attr('calendar.privat', 'message') | default('Kein Termin') | truncate(30) }}"
62
63 # --- Persönlich (passe an oder lösche, was du nicht brauchst) ---
64 skate_km: "{{ states('input_number.skating_km_jahr') | round(0) }}"
65 skate_goal: "{{ states('input_number.skating_jahresziel') | round(0) }}"
66 skate_pct: "{{ ((states('input_number.skating_km_jahr') | float / states('input_number.skating_jahresziel') | float) * 100) | round(0) }}"
67 skate_remaining: "{{ (states('input_number.skating_jahresziel') | float - states('input_number.skating_km_jahr') | float) | round(0) }}"
68 skate_sessions: "{{ states('sensor.skating_einheiten_jahr') | default(0) }}"
69 next_birthday_date: "{{ states('sensor.naechster_geburtstag_datum') | default('') }}"
70 next_birthday: "{{ states('sensor.naechster_geburtstag_name') | default('') }}"
71
72 # --- Greeting & Timestamp ---
73 greeting: >-
74 {% set h = now().hour %}
75 {% if h < 12 %}Guten Morgen{% elif h < 18 %}Guten Tag{% else %}Guten Abend{% endif %}
76 updated_at: "{{ now().strftime('%d.%m. %H:%M') }}"
77 mode: singleDas sieht nach viel aus. Ist es auch. Aber die meisten Zeilen sind einfach nur states('sensor.xyz'), also das Auslesen eines Sensorwerts. Du musst nur die Entity-IDs an dein Setup anpassen. Alles was du nicht brauchst (YouTube, Webseiten, Skating), kannst du einfach rauslöschen und den entsprechenden Quadranten im Markup anpassen.
Tipp: Gehe in Home Assistant unter Entwicklerwerkzeuge → Zustände und suche dort nach deinen Sensoren. Die Entity-ID steht in der linken Spalte.
REST-Sensoren für YouTube und Cloudflare
Die Smart-Home-Werte hast du wahrscheinlich schon in Home Assistant. Für YouTube und Cloudflare brauchst du aber eigene REST-Sensoren. Die sind optional. Wenn du die YouTube- oder Webseiten-Quadranten nicht brauchst, überspringe diesen Schritt.
Die folgenden REST-Sensoren packst du ebenfalls in die gleiche Package-Datei (/config/packages/trmnl_dashboard.yaml).
YouTube braucht einen YouTube Data API v3 Key aus der Google Cloud Console. Cloudflare braucht einen API-Token mit Zone.Analytics Berechtigung. Beide kommen unter einen gemeinsamen rest: Key, weil YAML keine doppelten Schlüssel erlaubt:
1rest:
2 # --- YouTube ---
3 - resource: "https://www.googleapis.com/youtube/v3/channels?part=statistics&id=DEINE_CHANNEL_ID&key=DEIN_YOUTUBE_API_KEY"
4 scan_interval: 3600
5 sensor:
6 - name: "YouTube Kanal 1 Abonnenten"
7 value_template: "{{ value_json.items[0].statistics.subscriberCount }}"
8 - name: "YouTube Kanal 1 Aufrufe"
9 value_template: "{{ value_json.items[0].statistics.viewCount }}"
10 - name: "YouTube Kanal 1 Videos"
11 value_template: "{{ value_json.items[0].statistics.videoCount }}"
12
13 # --- Cloudflare ---
14 - resource: "https://api.cloudflare.com/client/v4/zones/DEINE_ZONE_ID/analytics/dashboard?since=-43200&continuous=true"
15 headers:
16 Authorization: "Bearer DEIN_CLOUDFLARE_TOKEN"
17 scan_interval: 1800
18 sensor:
19 - name: "Cloudflare Seite 1 Heute"
20 value_template: "{{ value_json.result.totals.requests.all }}"
21 - name: "Cloudflare Seite 1 Gestern"
22 value_template: "{{ value_json.result.timeseries[-2].requests.all | default(0) }}"
23 - name: "Cloudflare Seite 1 Monat"
24 value_template: "{{ value_json.result.totals.pageviews.all }}"Die DEINE_CHANNEL_ID findest du auf deiner YouTube-Kanalseite in der URL. Für einen zweiten Kanal fügst du einfach einen weiteren Eintrag unter rest: hinzu.
Template-Sensoren für Sparklines
Die kleinen Balkendiagramme in der Webseiten-Ansicht brauchen einen Template-Sensor, der die letzten sieben Tageswerte als kommaseparierte Liste liefert, z.B. 120,150,180,95,210,175,160. Das JavaScript im Markup rendert daraus die Sparklines.
1template:
2 - sensor:
3 - name: "Sparkline Seite 1"
4 state: >-
5 {% set ns = namespace(vals=[]) %}
6 {% for day in range(6, -1, -1) %}
7 {% set ts = (now() - timedelta(days=day)).strftime('%Y-%m-%d') %}
8 {% set val = state_attr('sensor.cloudflare_seite_1_historie', ts) | default(0) %}
9 {% set ns.vals = ns.vals + [val | string] %}
10 {% endfor %}
11 {{ ns.vals | join(',') }}Wie genau du die historischen Werte bekommst, hängt davon ab, wie dein Cloudflare-Sensor die Daten liefert. Eine Alternative: Du speicherst den Tageswert jeden Abend per Automation in einen input_text Helfer und baust den String dort zusammen.
Webhook testen
Teste den Webhook einmal manuell, bevor du auf die Automation wartest:
- Gehe in Home Assistant zu Entwicklerwerkzeuge → Aktionen
- Wähle
rest_command.trmnl_dashboardaus - Fülle die Variablen mit Testwerten (oder kopiere die Template-Werte aus der Automation)
- Klicke auf Aktion ausführen
- Gehe auf usetrmnl.com → dein Plugin → Preview. Dort siehst du sofort, ob die Daten ankommen und das Layout stimmt
Troubleshooting
Egal welche Variante du nutzt, hier die häufigsten Probleme:
- Display bleibt leer: Prüfe in der TRMNL Plugin-Vorschau, ob Daten ankommen (Webhook) bzw. ob der Screenshot korrekt aussieht (HA Screenshot). Wenn nichts ankommt, stimmt die Webhook-URL oder der Access Token nicht.
- Screenshot ist leer oder zeigt Login: Der Long-Lived Access Token ist abgelaufen oder falsch. Erstelle einen neuen unter Benutzername → Sicherheit → Token erstellen.
- Progress-Bars fehlen (Webhook-Variante): Der
content-Wrapper um Label und Value ist Pflicht. Ohne den rendert TRMNL die Balken nicht. - Payload zu groß (Webhook-Variante): Der Webhook akzeptiert maximal 2 KB. Kürze Texte oder entferne Quadranten. Das TRMNL+ Modell hat ein höheres Limit von 5 KB.
- Werte zeigen "unavailable": Prüfe unter Entwicklerwerkzeuge → Zustände, ob der Sensor existiert und einen Wert hat. Entity-ID Tippfehler sind die häufigste Ursache.
Das TRMNL Framework v2 bringt fertige Komponenten mit: Fortschrittsbalken, Grids, Label-Value-Paare, Trennlinien. Wenn du dir ein eigenes Plugin bauen willst, arbeitest du mit HTML, Liquid-Templates und dem Framework-CSS. Du kannst das Plugin privat lassen oder als Blueprint für andere veröffentlichen.
Hast du das TRMNL schon im Einsatz oder überlegst du, dir eins zu holen? Schreib mir in die Kommentare, was du dir darauf anzeigen lässt.
