Ich habe mir mit dem Seeed Studio XIAO 7,5" ePaper Panel und dem Home-Assistant-Addon Puppet eine eigene Anzeige für verschiedene Darstellungen gebaut.
Der große Vorteil von Puppet: Man erstellt einfach ein Dashboard mit allem, was angezeigt werden soll, schnell, unkompliziert und ohne großes Gefrickel und dieses wird an das Display übergeben.
Das Display aktualisiert sich alle 5 Minuten. Da es keine Akkuanzeige gibt, lasse ich oben rechts Datum und Uhrzeit anzeigen. So sehe ich auf einen Blick, ob das Display noch läuft oder ob es Zeit zum Aufladen ist.
Den Rahmen habe ich mit dem 3D-Drucker hergestellt, weil mir das optisch deutlich besser gefällt als der Originalrahmen.
Nice ! Meines wartet immer noch im Karton integriert zu werden
Bisheriges Szenario was ich abbilden möchte, ist der Fenster und Türzustand der Hütte beim Verlassen. Hatte ich nicht mal von Problemen mit puppet und dem abbilden von screenshots gelesen ?! Ist das gefixt worden ?
Du meinst das Problem, bei dem sich das Display im Sekundentakt per USB mit dem PC verbindet und wieder trennt?
In diesem Fall müssen sowohl das Display als auch der Akku vom MCU getrennt werden. Das USB-Kabel zunächst noch nicht anschließen.
Anschließend die Boot-Taste am MCU gedrückt halten und währenddessen das USB-Kabel mit dem PC verbinden. Die Taste weiterhin gedrückt halten und erst nach dem erfolgreichen Anschluss loslassen.
Danach kann die Firmware wie gewohnt aufgespielt werden.
Danke. Ist ja auch alles fast dasselbe wie bei mir außer dass du auf das BMP Format setzt. Hat das ein Grund ? Bei mir wird die Seite nicht aufgebaut wegen einem Timeout Fehler.
Ich habe heute mein Display-Setup fertiggestellt. Mein Ziel war es, eine möglichst geringe Latenz bei Statusänderungen (z. B. Türverriegelung) mit maximaler Akku- und Display-Schonung zu kombinieren. Da das System erst seit heute läuft, liegen noch keine Langzeiterfahrungen zur Akkulaufzeit vor, aber das Konzept ist auf minimale “On-Time” optimiert.
Die Grundidee basiert auf einer dreistufigen Logik:
Status-Monitoring (Trigger): Eine Automatisierung überwacht permanent alle relevanten Entitäten. Sobald sich ein Status ändert, wird ein boolescher Helfer (needs_render) auf true gesetzt.
Prädiktives Rendering: Kurz bevor das Display aufwacht (berechnet anhand der letzten Rückmeldung), prüft eine zweite Automatisierung diesen Helfer. Nur bei Bedarf wird das Bild serverseitig gerendert. Das spart Rechenleistung und verkürzt die Wachzeit des ESP32 massiv, da dieser nur noch das fertige Image abrufen muss. Nach dem Rendering wird ein zweiter Helfer (update_available) gesetzt.
Synchronisierter Deep Sleep: Beim Wake-up prüft das Display das update_available-Flag. Nur bei positivem Status erfolgt ein Refresh. Parallel werden Telemetriedaten (Batteriestand, RSSI) an Home Assistant übertragen.
Besonderes Feature – Adaptive Sleep Cycles: Das Display liest beim Check-in aktiv aus einem HA-Helfer aus, wie lange die nächste Schlafphase dauern soll, und gibt den exakten Zeitpunkt des nächsten Wake-ups an HA zurück. Dadurch kann ich das Polling-Intervall dynamisch steuern (z. B. 5-Minuten-Takt tagsüber, 60-Minuten-Takt in der Nacht).
Hier ist der Code des Displays:
esphome:
name: trmnl
friendly_name: TRMNL
on_boot:
priority: 600
then:
- output.turn_on: bsp_battery_enable
- delay: 200ms
- component.update: battery_voltage
- component.update: battery_level
#- output.turn_off: bsp_battery_enable # Wieder aus, um Strom zu sparen TODO Prüfen
esp32:
board: esp32-s3-devkitc-1
framework:
type: arduino
psram:
mode: octal
speed: 80MHz
# Enable logging
logger:
level: WARN # schnelleres Compilieren, weniger Informationen bei Fehlersuche
# level: INFO # Alle Infos bei Fehlersuche
# Enable Home Assistant API
api:
encryption:
key: "key"
ota:
- platform: esphome
password: "password"
globals:
- id: wifi_status
type: int
restore_value: no
initial_value: "0"
- id: recorded_display_refresh
type: int
restore_value: yes
initial_value: '0'
binary_sensor:
- platform: homeassistant
id: display0_picture_update_available
entity_id: input_boolean.display0_picture_update_available
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
on_connect:
then:
- lambda: |-
id(wifi_status) = 1;
# WICHTIG: Warte, bis die API-Verbindung zu Home Assistant steht
- wait_until:
api.connected:
- logger.log: "API verbunden!"
# Jetzt ein kleiner Puffer, damit die Sensoren ihre Werte von HA empfangen
- delay: 1s
- if:
condition:
binary_sensor.is_on: display0_picture_update_available
then:
- logger.log: "Bildupdate verfügbar - starte Download."
- component.update: dashboard_image
else:
# Aufwachzeit an HA melden
- homeassistant.service:
service: input_datetime.set_datetime
data:
entity_id: input_datetime.display0_expected_wakeup
timestamp: !lambda |-
float duration = id(next_sleep_duration).state;
if (isnan(duration) || duration <= 0) { duration = 15.0; }
return id(homeassistant_time).now().timestamp + (duration * 60);
# Jetzt erst schlafen
- logger.log: "Aufwachzeit gesendet. Gehe schlafen."
- deep_sleep.enter:
id: deep_sleep_1
sleep_duration: !lambda |-
float duration = id(next_sleep_duration).state;
if (isnan(duration) || duration <= 0) {
return 15.0 * 60000; // 15 Min Default in Millisekunden
}
return duration * 60000;
on_disconnect:
then:
- lambda: |-
id(wifi_status) = 0;
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "TRMNL Fallback Hotspot"
password: "xxxxxxxx"
captive_portal:
deep_sleep:
id: deep_sleep_1
run_duration: 1min # Device wake up and run 60s in case Homeassistant is not reachable
sleep_duration: 30min # deep sleep for 30m in case Homeassistant is not reachable
http_request:
verify_ssl: false
timeout: 10s
watchdog_timeout: 15s
# https://esphome.io/components/online_image/
online_image:
- id: dashboard_image
format: PNG
#type: GRAYSCALE
type: BINARY
buffer_size: 50000
url: http://192.168.10.54:8123/local/Display-0bw.png
update_interval: never # avoid updateing bevore wifi is connected
on_download_finished:
- delay: 10ms
# 1. Bild auf das E-Ink übertragen
- component.update: main_display
# 2. Warten, bis das Display physisch fertig gezeichnet hat
- wait_until:
lambda: |-
return id(main_display).is_ready();
- delay: 200ms
# 3. Den Schalter picture_update in Home Assistant auf AUS setzen
- homeassistant.service:
service: input_boolean.turn_off
data:
entity_id: input_boolean.display0_picture_update_available
# 4. Aufwachzeit an Home Assistant melden
- homeassistant.service:
service: input_datetime.set_datetime
data:
entity_id: input_datetime.display0_expected_wakeup
timestamp: !lambda |-
float duration = id(next_sleep_duration).state;
if (isnan(duration) || duration <= 0) { duration = 15.0; }
return id(homeassistant_time).now().timestamp + (duration * 60);
# 5. Jetzt erst schlafen
- logger.log: "Aufwachzeit gesendet. Gehe schlafen."
- deep_sleep.enter:
id: deep_sleep_1
sleep_duration: !lambda |-
float duration = id(next_sleep_duration).state;
if (isnan(duration) || duration <= 0) {
return 15.0 * 60000; // 15 Min Default in Millisekunden
}
return duration * 60000;
spi:
clk_pin: GPIO7
mosi_pin: GPIO9
display:
- platform: waveshare_epaper
id: main_display
model: 7.50inv2
cs_pin: GPIO44
dc_pin: GPIO10
reset_pin: GPIO38
busy_pin:
number: GPIO4
inverted: true # just the busy pin logic
update_interval: never # wird durch Wifi-Verbindung aufgerufen
lambda: |-
if (id(dashboard_image).is_ready()) {
it.image(0, 0, id(dashboard_image));
}
time:
- platform: homeassistant
id: homeassistant_time
sensor:
- platform: adc
pin: GPIO1
name: "Voltage"
id: battery_voltage
device_class: voltage
unit_of_measurement: "V"
state_class: measurement
entity_category: "diagnostic"
update_interval: 60s
attenuation: 12db
filters:
- multiply: 2.0
- platform: template
name: "Battery"
id: battery_level
device_class: battery
unit_of_measurement: "%"
state_class: measurement
entity_category: "diagnostic"
lambda: 'return id(battery_voltage).state;'
update_interval: 60s
filters:
- calibrate_linear:
- 4.15 -> 100.0
- 3.96 -> 90.0
- 3.91 -> 80.0
- 3.85 -> 70.0
- 3.80 -> 60.0
- 3.75 -> 50.0
- 3.68 -> 40.0
- 3.58 -> 30.0
- 3.49 -> 20.0
- 3.41 -> 10.0
- 3.30 -> 5.0
- 3.27 -> 0.0
- clamp:
min_value: 0
max_value: 100
- platform: wifi_signal # Reports the WiFi signal strength/RSSI in dB
name: "WiFi Signal dB"
id: wifi_signal_db
update_interval: 120s
entity_category: "diagnostic"
- platform: copy # Reports the WiFi signal strength in %
source_id: wifi_signal_db
name: "WiFi Signal Percent"
id: wifi_signal_percent
filters:
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
unit_of_measurement: "%"
entity_category: "diagnostic"
- platform: uptime
name: Uptime
- platform: internal_temperature
name: "Internal Temperature"
- platform: template
name: "Display Last Update"
device_class: timestamp
entity_category: "diagnostic"
id: display_last_update
lambda: 'return id(homeassistant_time).now().timestamp;'
- platform: template
name: "Display Refresh Counts"
accuracy_decimals: 0
unit_of_measurement: "Refreshes"
state_class: "total_increasing"
entity_category: "diagnostic"
lambda: 'return id(recorded_display_refresh) += 1;'
- platform: homeassistant
id: next_sleep_duration
entity_id: input_number.display0_next_sleep_duration
# Wichtig: Falls HA nicht erreichbar ist, nutzen wir 30 Min als Standard
on_value:
then:
- logger.log: "Neue Schlafdauer empfangen"
output:
- platform: gpio
pin: GPIO6
id: bsp_battery_enable
Für die Generierung nutze ich trmnl Home Assistant.
Die Einbindung erfolgt über einen flexiblen shell_command und eine zustandsgesteuerte Automatisierung.
Konfiguration des Shell-Commands
Um den Rendering-Prozess universell zu halten, habe ich in der configuration.yaml einen Befehl definiert, der Dashboard-Pfad, Auflösung und Konfiguration als Variablen entgegennimmt:
Die Steuerungs-Logik (Automation)
Die Automatisierung triggert prädiktiv 45 Sekunden vor dem berechneten Wake-up des Displays. Die condition-Logik stellt sicher, dass nur gerendert wird, wenn entweder ein manueller Render-Bedarf (needs_render: on) besteht oder das letzte Bild älter als 4 Stunden ist. Das Rendern dauert bei mir ca. 26 Sekunden auf einem Odroid C4
Obwohl das System grundsätzlich stabil läuft, beobachte ich derzeit zwei Phänomene:
Rendering-Artefakte: Gelegentlich wird lediglich ein dunkles Bild mit einem zentralen Punkt generiert. Da die wait=3500ms bereits großzügig bemessen sind, konnte ich die Ursache im Headless-Browser-Prozess noch nicht zweifelsfrei identifizieren.
Graustufen unter ESPHome: Während die originale TRMNL-Firmware Graustufen unterstützt, führt die Darstellung von Grayscales unter ESPHome bei mir aktuell zu einer rein schwarzen Anzeige. Falls jemand eine funktionierende Konfiguration für ESPHome nutzt, die über reines B/W hinausgeht, wäre ich für einen Tipp dankbar