Seeed Studio XIAO 7,5" ePaper Panel und Puppet-Addon

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.

3 „Gefällt mir“

Nice ! Meines wartet immer noch im Karton integriert zu werden :upside_down_face:

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 ?

Bei mir läuft es stabil, allerdings erst, nachdem ich vom Raspberry Pi 5 mit 4 GB RAM auf einen Intel-PC mit Proxmox und 32 GB RAM gewechselt bin.

1 „Gefällt mir“

Mein HA ist in einer VM auf einer DS220+. Leistungsmangel kennt das System bisher nicht.

Und gibt es echt keine Möglichkeit den Batterie Status auslesen ?

Zumindest habe ich als Anfänger noch keine Möglichkeit gefunden.

aha geil. Ich bin in denselben Connect-Fehler gelaufen wie du in einem andren Fred geschrieben hast. Wie bist du denn da raus gekommen ?

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.

ja danke, würdest du mir deinen Code mal überlassen ? Es hackt an ein paar Stellen…

esphome:
  name: edisplayflur
  friendly_name: eDisplayFlur

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: esp-idf

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "HUk+retqHsrRe/WROKXghjzt/3wVBPMF1sx5642R2uMSp+gwU="

ota:
  - platform: esphome
    password: "b07ert45af14178retb43bdc67b9456rr010fc"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

http_request:
  verify_ssl: false
  timeout: 30s
  watchdog_timeout: 15s

online_image:
  - id: dashboard_image
    format: BMP
    type: BINARY
    buffer_size: 25000
    url: http://192.168.178.149:10000/strom-energie/seeedstudio-eink-display?viewport=800x480&eink=2&dithering=floyd-steinberg&format=bmp&invert
    update_interval: 120s
    on_download_finished:
      - delay: 0ms
      - component.update: main_display

spi:
  clk_pin: GPIO8
  mosi_pin: GPIO10

display:
  - platform: waveshare_epaper
    id: main_display
    cs_pin: GPIO3
    dc_pin: GPIO5
    busy_pin: 
      number: GPIO4
      inverted: true
    reset_pin: GPIO2
    model: 7.50inv2
    update_interval: never
    lambda: |-
      it.image(0, 0, id(dashboard_image));

1 „Gefällt mir“

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.

Mfg

Im PNG oder JPEG Format hat der Aufbau sehr lange gedauert. Kann aber auch andere Gründe gehabt haben. Bei mir läuft es so ganz stabil.

1 „Gefällt mir“

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:

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

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

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

…und dann kam er mit so einem Broken um die Ecke :astonished_face:

Wie generierst du denn das /Display-0bw.png ? Ebenfalls mit puppet ?

Für die Generierung nutze ich trmnl Home Assistant.
Die Einbindung erfolgt über einen flexiblen shell_command und eine zustandsgesteuerte Automatisierung.

  1. 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:
#Shell Command für TRMNL HA Bilderzeugung E-Ink Display
shell_command:
  trmnl_refresh_image: 'curl -L "http://homeassistant.local:10000/{{ dashboard }}?viewport={{ resolution }}&{{ config }}" -o "{{ filepath }}"'  

  1. 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
alias: E-Ink Display0 Bild aktualisieren
description: ""
triggers:
  - trigger: time
    at:
      entity_id: input_datetime.display0_expected_wakeup
      offset: "-00:00:45"
    id: Display wacht auf
conditions:
  - condition: or
    conditions:
      - condition: state
        entity_id: input_boolean.display0_needs_render
        state:
          - "on"
      - condition: and
        conditions:
          - condition: state
            entity_id: input_boolean.display0_needs_render
            state:
              - "off"
            for:
              hours: 4
              minutes: 0
              seconds: 0
          - condition: state
            entity_id: input_boolean.display0_picture_update_available
            state:
              - "off"
actions:
  - alias: Bild für E-Ink
    action: shell_command.trmnl_refresh_image
    data:
      dashboard: e-ink/0
      resolution: 480x800
      config: >-
        rotate=90&theme=Graphite%20E-ink%20Dark&dark_mode=true&wait=3500&dithering&dither_method=ordered&&zoom=1.2&palette=bw&compression_level=6
      filepath: /config/www/Display-0bw.png
  - action: input_boolean.turn_on
    metadata: {}
    target:
      entity_id: input_boolean.display0_picture_update_available
    data: {}
  - action: input_boolean.turn_off
    metadata: {}
    target:
      entity_id: input_boolean.display0_needs_render
    data: {}
mode: single

Obwohl das System grundsätzlich stabil läuft, beobachte ich derzeit zwei Phänomene:

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

  2. 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 :slightly_smiling_face:

1 „Gefällt mir“

Ich brauch mal eure Hilfe ich hab das e1002 Farbe display bei mir ist das gerät nach dem hochladen tot ist ein Fehler im code ?

esphome:
  name: reterminal-e1002
  friendly_name: reTerminal E1002

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "2EKtedVgz2XuYH/NkiIGY8r19w3m7JwnE6La4W6aQZU="

ota:
  - platform: esphome
    password: "04686a74c0ad1ad9a37195c4c74d5a63"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Reterminal-E1002"
    password: "qzHNwQ3NEwdK"

http_request:
  verify_ssl: false
  timeout: 30s
  watchdog_timeout: 15s

online_image:
  - id: dashboard_image
    format: BMP
    type: BINARY
    buffer_size: 25000
    url: http://192.168.178.52:10000/epaper-display/seeedstudio-eink-display?viewport=800x480&eink=2&dithering=floyd-steinberg&format=bmp&invert$0
    update_interval: 120s
    on_download_finished:
      - delay: 0ms
      - component.update: main_display

spi:
  clk_pin: GPIO7
  mosi_pin: GPIO9

display:
  - platform: epaper_spi
    id: main_display
    model: Seeed-reTerminal-E1002  # Ab ESPHome 2025.11
    # Falls das nicht geht, nimm: model: 7.3in-spectra-e6
    cs_pin: GPIO10
    dc_pin: GPIO11
    reset_pin: GPIO12
    busy_pin: GPIO13
    update_interval: never
    lambda: |-
      it.image(0, 0, id(dashboard_image));
   

Ich hab’s endlich hin bekommen mit Akku Anzeige usw über Home Assistant bzw ESP



Hi,

ist es möglich das sich das Panel nur durch eine Bedingung refresht? Also z.B. durch einen Bewegungsmelder?

Eine starre Zeit ist für meinen Anwendungsfall nicht nötig.