Zum Inhalt springen
Alles Automatisch

Home AssistantSmart Home & GadgetsDashboards & ThemesSnippets

TRMNL E-Ink Display mit Home Assistant einrichten

· Aktualisiert am

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

  1. Gehe auf usetrmnl.com → Plugins
  2. Suche nach Home Assistant Screenshot und klicke auf Add
  3. Klicke auf Save, dann bekommst du eine Webhook URL angezeigt
  4. Kopiere diese Webhook URL, die brauchst du gleich

Schritt 3: TRMNL HA App installieren

In Home Assistant brauchst du die TRMNL HA App (früher Add-on). Die findest du unter Einstellungen → Apps → App installieren. Nach der Installation:

  1. Gehe auf Konfiguration der App
  2. 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
  3. Füge den Token in der App-Konfiguration ein
  4. Gib deine Home Assistant URL ein, also die Adresse, unter der du HA im Browser öffnest. Bei einer Neuinstallation von Home Assistant OS ab 2026.8 ist das http://homeassistant.local, bei älteren Installationen meist http://homeassistant.local:8123
  5. Klicke auf Speichern und Starten
  6. 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:

  1. Gib die URL deines TRMNL-Dashboards ein (das bauen wir im nächsten Schritt)
  2. Füge die Webhook URL aus Schritt 2 ein
  3. Setze den Cron-Zeitplan auf */10 * * * * (alle 10 Minuten). Falls dir die Cron-Notation nichts sagt, hilft Crontab Guru
  4. 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:

yaml
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') | float(0) | 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') | float(0) | 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') | float(0) | 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') | float(0) | 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') | float(0) | 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') | float(0) | 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 Werkzeuge → Zustände (bis 2026.7 Entwicklerwerkzeuge). 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 (bei einer Neuinstallation ab 2026.8 ohne :8123). 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

  1. Gehe auf usetrmnl.com → Plugins → Private Plugin
  2. Wähle als Strategy Webhook aus
  3. Gib dem Plugin einen Namen (z.B. "Home Dashboard")
  4. 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/
  5. 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:

liquid
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:

yaml
homeassistant:
  packages: !include_dir_named packages

Dann kommt der REST Command in die Package-Datei:

yaml
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 Werkzeuge → 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:

yaml
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      # Die Tageswerte kommen über die Forecast-Aktion, nicht mehr über ein Attribut
9      - action: weather.get_forecasts
10        target:
11          entity_id: weather.zuhause
12        data:
13          type: daily
14        response_variable: wetter
15        # ohne Wetter-Entity sollen die übrigen Werte trotzdem ankommen
16        continue_on_error: true
17      - action: rest_command.trmnl_dashboard
18        data:
19          # --- Smart Home (passe die Entity-IDs an dein Setup an) ---
20          power_current: "{{ states('sensor.stromverbrauch') | float(0) | round(0) }}"
21          solar_production: "{{ states('sensor.solar_produktion') | float(0) | round(1) }}"
22          energy_today: "{{ states('sensor.solar_energie_heute') | float(0) | round(1) }}"
23          grid_label: >-
24            {% if states('sensor.netz_leistung') | float(0) > 0 %}Bezug{% else %}Einspeisung{% endif %}
25          grid_value: "{{ states('sensor.netz_leistung') | float(0) | abs | round(0) }}"
26          energy_balance: "{{ (states('sensor.einspeisung_heute') | float(0) - states('sensor.netzbezug_heute') | float(0)) | abs | round(1) }}"
27          energy_balance_positive: "{{ (states('sensor.einspeisung_heute') | float(0) - states('sensor.netzbezug_heute') | float(0)) > 0 }}"
28          battery_level: "{{ states('sensor.batterie_ladezustand') | float(0) | round(0) }}"
29          power_price: "{{ states('sensor.strompreis') | float(0) | round(2) }}"
30          temp_indoor: "{{ states('sensor.temperatur_wohnzimmer') | float(0) | round(1) }}"
31          temp_outdoor: "{{ states('sensor.temperatur_aussen') | float(0) | round(1) }}"
32
33          # --- YouTube (brauchst REST-Sensoren, siehe unten) ---
34          aa_subs: "{{ states('sensor.youtube_kanal_1_abonnenten') }}"
35          aa_views: "{{ states('sensor.youtube_kanal_1_aufrufe') }}"
36          aa_videos: "{{ states('sensor.youtube_kanal_1_videos') }}"
37          kk_subs: "{{ states('sensor.youtube_kanal_2_abonnenten') }}"
38          kk_views: "{{ states('sensor.youtube_kanal_2_aufrufe') }}"
39          kk_videos: "{{ states('sensor.youtube_kanal_2_videos') }}"
40
41          # --- Webseiten (brauchst REST-Sensoren, siehe unten) ---
42          cf_pg_h: "{{ states('sensor.cloudflare_seite_1_heute') }}"
43          cf_pg_g: "{{ states('sensor.cloudflare_seite_1_gestern') }}"
44          cf_pg_m: "{{ states('sensor.cloudflare_seite_1_monat') }}"
45          cf_db_h: "{{ states('sensor.cloudflare_seite_2_heute') }}"
46          cf_db_g: "{{ states('sensor.cloudflare_seite_2_gestern') }}"
47          cf_db_m: "{{ states('sensor.cloudflare_seite_2_monat') }}"
48          cf_bs_h: "{{ states('sensor.cloudflare_seite_3_heute') }}"
49          cf_bs_g: "{{ states('sensor.cloudflare_seite_3_gestern') }}"
50          cf_bs_m: "{{ states('sensor.cloudflare_seite_3_monat') }}"
51          spark_pg: "{{ states('sensor.sparkline_seite_1') }}"
52          spark_db: "{{ states('sensor.sparkline_seite_2') }}"
53          spark_bs: "{{ states('sensor.sparkline_seite_3') }}"
54
55          # --- Wetter ---
56          weather_temp: "{{ state_attr('weather.zuhause', 'temperature') | float(0) | round(0) }}"
57          weather_desc: "{{ states('weather.zuhause') }}"
58          weather_max: >-
59            {% set tage = (wetter | default({})).get('weather.zuhause', {}).get('forecast', []) %}
60            {{ (tage[0].get('temperature', 0) if tage else 0) | float(0) | round(0) }}
61          weather_min: >-
62            {% set tage = (wetter | default({})).get('weather.zuhause', {}).get('forecast', []) %}
63            {{ (tage[0].get('templow', 0) if tage else 0) | float(0) | round(0) }}
64          sunrise: "{{ as_timestamp(state_attr('sun.sun', 'next_rising')) | timestamp_custom('%H:%M') }}"
65          sunset: "{{ as_timestamp(state_attr('sun.sun', 'next_setting')) | timestamp_custom('%H:%M') }}"
66
67          # --- Kalender ---
68          next_event_time: >-
69            {% set e = state_attr('calendar.privat', 'start_time') %}
70            {% if e %}{{ as_timestamp(e) | timestamp_custom('%H:%M') }}{% else %}--:--{% endif %}
71          next_event: "{{ state_attr('calendar.privat', 'message') | default('Kein Termin') | truncate(30) }}"
72
73          # --- Persönlich (passe an oder lösche, was du nicht brauchst) ---
74          skate_km: "{{ states('input_number.skating_km_jahr') | float(0) | round(0) }}"
75          skate_goal: "{{ states('input_number.skating_jahresziel') | float(0) | round(0) }}"
76          skate_pct: >-
77            {% set ziel = states('input_number.skating_jahresziel') | float(0) %}
78            {{ ((states('input_number.skating_km_jahr') | float(0) / ziel * 100) | round(0)) if ziel > 0 else 0 }}
79          skate_remaining: "{{ (states('input_number.skating_jahresziel') | float(0) - states('input_number.skating_km_jahr') | float(0)) | round(0) }}"
80          skate_sessions: "{{ states('sensor.skating_einheiten_jahr') | default(0) }}"
81          next_birthday_date: "{{ states('sensor.naechster_geburtstag_datum') | default('') }}"
82          next_birthday: "{{ states('sensor.naechster_geburtstag_name') | default('') }}"
83
84          # --- Greeting & Timestamp ---
85          greeting: >-
86            {% set h = now().hour %}
87            {% if h < 12 %}Guten Morgen{% elif h < 18 %}Guten Tag{% else %}Guten Abend{% endif %}
88          updated_at: "{{ now().strftime('%d.%m. %H:%M') }}"
89    mode: single

Das 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 Werkzeuge → 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:

yaml
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.

yaml
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:

  1. Gehe in Home Assistant zu Werkzeuge → Aktionen
  2. Wähle rest_command.trmnl_dashboard aus
  3. Fülle die Variablen mit Testwerten (oder kopiere die Template-Werte aus der Automation)
  4. Klicke auf Aktion ausführen
  5. 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 Werkzeuge → 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.

Weiterführende Inhalte

Diskussion im Forum

Hast du Fragen oder Ideen zu diesem Thema?

Diskutiere im Forum

Video teilen

Community

Unterstütze den Kanal

Hat dir der Beitrag geholfen? Es gibt zwei Wege, etwas zurückzugeben.