TRMNL DIY E-Ink Dashboard

Handy little dashboard that has been living on the fridge since May 2026 and saves me looking at the Sigenergy app every now and then. Today’s section for Calendar shows 4 events, tomorrow shows 2, Sat/Sun each shows 2 events. Built on the TRMNL 7.5" OG DIY Kit. Shows weather and refreshes every 60 minutes via deep sleep.

Dashboard Screenshot

How It Works Link to heading

  1. ESP32 wakes from deep sleep (every 60 min)
  2. Connects to WiFi, fetches sensor/text states from Home Assistant
  3. Renders the layout to the e-ink framebuffer
  4. E-ink updates (~10 seconds)
  5. Enters deep sleep to save battery
┌──────────────────────────────────────────────────────────┐
│  Tuesday, 27 May                              [DevBat]  │
├──────────────────────┬───────────────────────────────────┤
│ Today                │         ☀️ (weather icon)         │
│ ─────────            │           22°C                    │
│ 14:30 Meeting        │         ▼12°  ▲24°                │
│ All-day Birthday     │                                   │
│                      │ ─────────────────────────────     │
│ Tomorrow             │         🔋 (battery icon)         │
│ ─────────            │           87%                     │
│ 09:00 Standup        │         Discharging               │
│                      │                                   │
│ Weekend              │                                   │
│ ─────────            │                                   │
│ Sat 31 May           │                                   │
│  All-day BBQ         │                                   │
│ Sun 1 Jun            │                                   │
│  No events           │                                   │
├──────────────────────┴───────────────────────────────────┤
│ Built with ESPHome                    Refreshed at 08:15 │
└──────────────────────────────────────────────────────────┘

Home Assistant Setup Link to heading

Integrations Link to heading

Ensure these are enabled in Home Assistant:

  • ESPHome
  • Google Calendar
  • A weather integration (Met.no is built-in; BoM, OpenWeatherMap, or Pirate Weather also work)
  • A battery/solar integration (e.g. Sigenergy ESS via HACS, Enphase Envoy, Tesla Powerwall, etc.)

Input Text Helpers Link to heading

Create four helpers (configuration.yaml or UI > Settings > Devices & Services > Helpers):

input_text:
  trmnl_calendar:
    name: "TRMNL Calendar"
    max: 255
  trmnl_calendar_tomorrow:
    name: "TRMNL Calendar Tomorrow"
    max: 255
  trmnl_calendar_sat:
    name: "TRMNL Calendar Sat"
    max: 255
  trmnl_calendar_sun:
    name: "TRMNL Calendar Sun"
    max: 255

Template Sensors Link to heading

Add to configuration.yaml. The dashboard’s ESPHome lambda matches weather condition by exact string. The map below translates Met.no’s raw values (partlycloudy) to the labels expected by the display (Partly Cloudy).

template:
  - sensor:
      - name: "TRMNL Weather Temperature"
        unique_id: trmnl_weather_temperature
        state: "{{ state_attr('weather.forecast_home', 'temperature') }}"
        unit_of_measurement: "°C"

      - name: "TRMNL Weather Condition"
        unique_id: trmnl_weather_condition
        state: >
          {% set cond = states('weather.forecast_home') %}
          {% set map = {
            'clear-night': 'Clear Night', 'cloudy': 'Cloudy', 'fog': 'Foggy',
            'hail': 'Hail', 'lightning': 'Lightning', 'lightning-rainy': 'Stormy',
            'partlycloudy': 'Partly Cloudy', 'pouring': 'Heavy Rain',
            'rainy': 'Rainy', 'snowy': 'Snowy', 'snowy-rainy': 'Sleet',
            'sunny': 'Sunny', 'windy': 'Windy', 'windy-variant': 'Windy',
            'exceptional': 'Exceptional' } %}
          {{ map.get(cond, cond | title) }}          

      - name: "TRMNL Weather Low"
        unique_id: trmnl_weather_low
        state: >
          {% set lo = states('sensor.your_temp_min_0') %}
          {% if lo | float(-999) != -999 and lo | float(0) > -50 %}
            {{ lo }}
          {% else %}
            {{ state_attr('weather.forecast_home', 'forecast')[0].templow | default(0) }}
          {% endif %}          
        unit_of_measurement: "°C"

      - name: "TRMNL Weather High"
        unique_id: trmnl_weather_high
        state: >
          {% set hi = states('sensor.your_temp_max_0') %}
          {% if hi | float(-999) != -999 and hi | float(0) > -50 %}
            {{ hi }}
          {% else %}
            {{ state_attr('weather.forecast_home', 'forecast')[0].temperature | default(0) }}
          {% endif %}          
        unit_of_measurement: "°C"

Calendar Automation Link to heading

Create this automation in Home Assistant (Settings > Automations > New Automation > Edit in YAML). It fetches 7 days of events in one call, formats them as "HH:MM Summary" or "All-day Summary", and writes to the four input_text helpers. Runs every 10 minutes and on HA startup.

alias: "TRMNL: Update Calendar Display"
description: >-
  Fetch today's, tomorrow's and weekend calendar events for the TRMNL e-ink screen  
triggers:
  - trigger: time_pattern
    minutes: "/10"
  - trigger: homeassistant
    event: start
actions:
  - action: calendar.get_events
    target:
      entity_id: calendar.your_calendar
    data:
      start_date_time: >-
        {{ now().replace(hour=0, minute=0, second=0, microsecond=0)
           .strftime('%Y-%m-%dT%H:%M:%S') }}        
      duration:
        days: 7
    response_variable: cal_result

  - variables:
      cal_key: calendar.your_calendar
      evts: >-
        {{ cal_result.get(cal_key, {}).get('events', []) if cal_result is mapping else [] }}        
      today_str: "{{ now().strftime('%Y-%m-%d') }}"
      tomorrow_str: "{{ (now() + timedelta(days=1)).strftime('%Y-%m-%d') }}"
      sat_str: >-
        {{ (now() + timedelta(days=(5 - now().weekday()) % 7)).strftime('%Y-%m-%d') }}        
      sun_str: >-
        {{ (now() + timedelta(days=(6 - now().weekday()) % 7)).strftime('%Y-%m-%d') }}        

  # Today (upcoming events only)
  - action: input_text.set_value
    target:
      entity_id: input_text.trmnl_calendar
    data:
      value: >
        {%- set ns = namespace(lines=[]) %}
        {%- for evt in evts %}
        {%-   set s = evt.start | string %}
        {%-   if s[:10] == today_str %}
        {%-     if 'T' in s %}
        {%-       if (s | as_datetime | as_local) > now() %}
        {%-         set ns.lines = ns.lines + [(s | as_datetime | as_local).strftime('%H:%M')
                   ~ ' ' ~ (evt.summary | default('(no title)') | truncate(28, true))] %}
        {%-       endif %}
        {%-     else %}
        {%-       set ns.lines = ns.lines + ['All-day '
                   ~ (evt.summary | default('(no title)') | truncate(28, true))] %}
        {%-     endif %}
        {%-   endif %}
        {%- endfor %}
        {{ ns.lines | join('\n') }}        

  # Tomorrow
  - action: input_text.set_value
    target:
      entity_id: input_text.trmnl_calendar_tomorrow
    data:
      value: >
        {%- set ns = namespace(lines=[]) %}
        {%- for evt in evts %}
        {%-   set s = evt.start | string %}
        {%-   if s[:10] == tomorrow_str %}
        {%-     if 'T' in s %}
        {%-       set ns.lines = ns.lines + [(s | as_datetime | as_local).strftime('%H:%M')
                   ~ ' ' ~ (evt.summary | default('(no title)') | truncate(28, true))] %}
        {%-     else %}
        {%-       set ns.lines = ns.lines + ['All-day '
                   ~ (evt.summary | default('(no title)') | truncate(28, true))] %}
        {%-     endif %}
        {%-   endif %}
        {%- endfor %}
        {{ ns.lines | join('\n') }}        

  # Saturday
  - action: input_text.set_value
    target:
      entity_id: input_text.trmnl_calendar_sat
    data:
      value: >
        {%- set ns = namespace(lines=[]) %}
        {%- for evt in evts %}
        {%-   set s = evt.start | string %}
        {%-   if s[:10] == sat_str %}
        {%-     if 'T' in s %}
        {%-       set ns.lines = ns.lines + [(s | as_datetime | as_local).strftime('%H:%M')
                   ~ ' ' ~ (evt.summary | default('(no title)') | truncate(28, true))] %}
        {%-     else %}
        {%-       set ns.lines = ns.lines + ['All-day '
                   ~ (evt.summary | default('(no title)') | truncate(28, true))] %}
        {%-     endif %}
        {%-   endif %}
        {%- endfor %}
        {{ ns.lines | join('\n') }}        

  # Sunday
  - action: input_text.set_value
    target:
      entity_id: input_text.trmnl_calendar_sun
    data:
      value: >
        {%- set ns = namespace(lines=[]) %}
        {%- for evt in evts %}
        {%-   set s = evt.start | string %}
        {%-   if s[:10] == sun_str %}
        {%-     if 'T' in s %}
        {%-       set ns.lines = ns.lines + [(s | as_datetime | as_local).strftime('%H:%M')
                   ~ ' ' ~ (evt.summary | default('(no title)') | truncate(28, true))] %}
        {%-     else %}
        {%-       set ns.lines = ns.lines + ['All-day '
                   ~ (evt.summary | default('(no title)') | truncate(28, true))] %}
        {%-     endif %}
        {%-   endif %}
        {%- endfor %}
        {{ ns.lines | join('\n') }}        
mode: single
max_exceeded: silent

ESPHome Config Link to heading

Fonts Link to heading

Place these in fonts/ inside your ESPHome config directory:

Secrets Link to heading

wifi_ssid: "YourWiFiName"
wifi_password: "YourWiFiPassword"
api_encryption_key: "your-base64-32-byte-key"
ota_password: "your-ota-password"
ap_password: "your-fallback-ap-password"

Generate an API key: openssl rand -base64 32

Customize trmnl.yaml Link to heading

Copy trmnl.yaml to your ESPHome config. Adjust these entity references:

Full trmnl.yaml
esphome:
  name: "trmnl"
  friendly_name: "trmnl"
  on_boot:
    priority: 600
    then:
      - output.turn_on: bsp_battery_enable
      - delay: 45s
      - component.update: eink
esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: arduino
logger:
  level: DEBUG
api:
  encryption:
    key: !secret api_encryption_key
ota:
  - platform: esphome
    password: !secret ota_password
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: "Terminal Fallback Hotspot"
    password: !secret ap_password
captive_portal:
deep_sleep:
  run_duration: 180s
  sleep_duration: 60min
output:
  - platform: gpio
    pin: GPIO6
    id: bsp_battery_enable
spi:
  clk_pin: GPIO7
  mosi_pin: GPIO9
time:
  - platform: homeassistant
    id: ha_time
    timezone: Australia/Sydney
sensor:
  - platform: adc
    pin: GPIO1
    id: device_battery_voltage
    name: "TRMNL Battery Voltage"
    attenuation: 12db
    update_interval: 30s
    filters:
      - multiply: 3.0
  - platform: template
    id: device_battery_pct
    name: "TRMNL Battery Level"
    unit_of_measurement: "%"
    lambda: "return id(device_battery_voltage).state;"
    update_interval: 30s
    filters:
      - calibrate_linear:
          - 4.20 -> 100
          - 4.10 -> 90
          - 4.00 -> 80
          - 3.90 -> 70
          - 3.80 -> 60
          - 3.70 -> 50
          - 3.60 -> 40
          - 3.50 -> 30
          - 3.40 -> 20
          - 3.30 -> 10
          - 2.50 -> 5
          - 2.00 -> 0
      - clamp:
          min_value: 0
          max_value: 100
  - platform: homeassistant
    id: battery_soc
    entity_id: sensor.your_battery_soc
  - platform: homeassistant
    id: weather_temp
    entity_id: sensor.trmnl_weather_temperature
  - platform: homeassistant
    id: weather_lo
    entity_id: sensor.trmnl_weather_low
  - platform: homeassistant
    id: weather_hi
    entity_id: sensor.trmnl_weather_high
text_sensor:
  - platform: homeassistant
    id: calendar_today
    entity_id: input_text.trmnl_calendar
  - platform: homeassistant
    id: calendar_tomorrow
    entity_id: input_text.trmnl_calendar_tomorrow
  - platform: homeassistant
    id: calendar_sat
    entity_id: input_text.trmnl_calendar_sat
  - platform: homeassistant
    id: calendar_sun
    entity_id: input_text.trmnl_calendar_sun
  - platform: homeassistant
    id: weather_condition
    entity_id: sensor.trmnl_weather_condition
binary_sensor:
  - platform: homeassistant
    id: bat_charging
    entity_id: binary_sensor.your_battery_charging
  - platform: homeassistant
    id: bat_discharging
    entity_id: binary_sensor.your_battery_discharging
font:
  - file: "fonts/Roboto-Regular.ttf"
    id: font_title
    size: 36
  - file: "fonts/Roboto-Bold.ttf"
    id: font_bold
    size: 22
  - file: "fonts/Roboto-Regular.ttf"
    id: font_header
    size: 22
  - file: "fonts/Roboto-Regular.ttf"
    id: font_body
    size: 18
  - file: "fonts/Roboto-Regular.ttf"
    id: font_body_small
    size: 16
  - file: "fonts/Roboto-Regular.ttf"
    id: font_pct
    size: 36
  - file: "fonts/Roboto-Regular.ttf"
    id: font_pct_small
    size: 28
  - file: "fonts/Roboto-Regular.ttf"
    id: font_footnote
    size: 14
  - file: "fonts/materialdesignicons-webfont.ttf"
    id: font_mdi_large
    size: 80
    glyphs:
      - "\U000F0599"
      - "\U000F0595"
      - "\U000F0F2F"
      - "\U000F0597"
      - "\U000F0598"
      - "\U000F059B"
      - "\U000F0594"
      - "\U000F0596"
      - "\U000F1902"
  - file: "fonts/materialdesignicons-webfont.ttf"
    id: font_mdi_small
    size: 22
    glyphs:
      - "\U000F0E02"
      - "\U000F0E03"
color:
  - id: black
    red: 0%
    green: 0%
    blue: 0%
  - id: white
    red: 100%
    green: 100%
    blue: 100%
display:
  - platform: waveshare_epaper
    id: eink
    model: 7.50inv2
    cs_pin: GPIO44
    dc_pin: GPIO10
    reset_pin: GPIO38
    busy_pin:
      number: GPIO4
      inverted: true
    update_interval: never
    lambda: |-
      ESP_LOGD("epaper", "=== Display lambda started ===");
      ESP_LOGD("epaper", "battery_soc has_state: %s", id(battery_soc).has_state() ? "yes" : "no");
      ESP_LOGD("epaper", "weather_temp has_state: %s", id(weather_temp).has_state() ? "yes" : "no");
      if (!id(battery_soc).has_state() || !id(weather_temp).has_state()) {
        ESP_LOGD("epaper", "Sensors not ready, skipping update");
        return;
      }
      ESP_LOGD("epaper", "Proceeding to draw...");
      time_t now_ts = id(ha_time).now().timestamp;
      struct tm now_lt;
      localtime_r(&now_ts, &now_lt);
      it.fill(id(black));
      // ═══ HEADER ═══
      char hdr_buf[48];
      strftime(hdr_buf, sizeof(hdr_buf), "%A, %d %B", &now_lt);
      it.print(400, 8, id(font_title), id(white), TextAlign::TOP_CENTER, hdr_buf);
      // ═══ TOP-RIGHT DEVICE BATTERY ═══
      float dbat = id(device_battery_pct).state;
      if (std::isnan(dbat)) dbat = 0;
      int bx = 740, by = 14, bw = 40, bh = 16;
      it.rectangle(bx, by, bw, bh, id(white));
      it.filled_rectangle(bx + bw, by + 4, 3, 8, id(white));
      int fill_w = (int)((bw - 4) * dbat / 100.0);
      if (fill_w < 0) fill_w = 0;
      if (fill_w > bw - 4) fill_w = bw - 4;
      if (fill_w > 0)
        it.filled_rectangle(bx + 2, by + 2, fill_w, bh - 4, id(white));
      auto num_color = (dbat > 50) ? id(black) : id(white);
      it.printf(bx + bw/2, by + bh/2 + 1, id(font_footnote), num_color,
                TextAlign::CENTER, "%.0f", dbat);
      // ═══ AESTHETIC SECTION DIVIDERS ═══
      it.line(20, 50, 780, 50, id(white));
      it.line(410, 50, 410, 445, id(white));
      it.line(20, 445, 780, 445, id(white));
      // ═══ TODAY — bold heading, regular events ═══
      it.print(20, 65, id(font_bold), id(white), "Today");
      it.line(20, 92, 390, 92, id(white));
      int y = 112;
      std::string today_txt = id(calendar_today).state;
      if (today_txt.empty() || today_txt == "unknown") {
        it.print(20, y, id(font_body), id(white), "No events");
        y += 22;
      } else {
        std::string line;
        for (size_t i = 0; i < today_txt.length(); i++) {
          if (today_txt[i] == '\n') {
            if (!line.empty() && y < 210) {
              it.print(20, y, id(font_body), id(white), line.c_str());
              y += 22;
              line.clear();
            }
          } else {
            line += today_txt[i];
          }
        }
        if (!line.empty() && y < 210) {
          it.print(20, y, id(font_body), id(white), line.c_str());
          y += 22;
        }
      }
      // ═══ TOMORROW — regular heading, compact events ═══
      y = 225;
      it.print(20, y, id(font_header), id(white), "Tomorrow");
      it.line(20, y + 27, 390, y + 27, id(white));
      y += 35;
      std::string tomorrow_txt = id(calendar_tomorrow).state;
      if (tomorrow_txt.empty() || tomorrow_txt == "unknown") {
        it.print(20, y, id(font_body_small), id(white), "No events");
        y += 18;
      } else {
        std::string line;
        for (size_t i = 0; i < tomorrow_txt.length(); i++) {
          if (tomorrow_txt[i] == '\n') {
            if (!line.empty() && y < 295) {
              it.print(20, y, id(font_body_small), id(white), line.c_str());
              y += 18;
              line.clear();
            }
          } else {
            line += tomorrow_txt[i];
          }
        }
        if (!line.empty() && y < 295) {
          it.print(20, y, id(font_body_small), id(white), line.c_str());
          y += 18;
        }
      }
      // ═══ WEEKEND — small heading, date labels, tiny events ═══
      y = 300;
      it.print(20, y, id(font_body_small), id(white), "Weekend");
      it.line(20, y + 25, 390, y + 25, id(white));
      int wd = now_lt.tm_wday;
      int days_to_sat = (6 - wd + 7) % 7;
      int days_to_sun = (7 - wd + 7) % 7;
      if (days_to_sat == 0 && wd != 6) days_to_sat = 7;
      if (days_to_sun == 0 && wd != 0) days_to_sun = 7;
      time_t sat_ts = now_ts + (days_to_sat * 86400);
      time_t sun_ts = now_ts + (days_to_sun * 86400);
      struct tm sat_tm, sun_tm;
      localtime_r(&sat_ts, &sat_tm);
      localtime_r(&sun_ts, &sun_tm);
      char sat_label[16], sun_label[16];
      strftime(sat_label, sizeof(sat_label), "%a %d %b", &sat_tm);
      strftime(sun_label, sizeof(sun_label), "%a %d %b", &sun_tm);
      y += 28;
      it.printf(20, y, id(font_body_small), id(white), "%s", sat_label);
      y += 17;
      std::string sat_txt = id(calendar_sat).state;
      if (sat_txt.empty() || sat_txt == "unknown") {
        it.print(28, y, id(font_footnote), id(white), "No events");
        y += 15;
      } else {
        std::string line;
        for (size_t i = 0; i < sat_txt.length(); i++) {
          if (sat_txt[i] == '\n') {
            if (!line.empty() && y < 444) {
              it.print(28, y, id(font_footnote), id(white), line.c_str());
              y += 15;
              line.clear();
            }
          } else {
            line += sat_txt[i];
          }
        }
        if (!line.empty() && y < 444) {
          it.print(28, y, id(font_footnote), id(white), line.c_str());
          y += 15;
        }
      }
      y += 4;
      it.printf(20, y, id(font_body_small), id(white), "%s", sun_label);
      y += 17;
      std::string sun_txt = id(calendar_sun).state;
      if (sun_txt.empty() || sun_txt == "unknown") {
        it.print(28, y, id(font_footnote), id(white), "No events");
        y += 15;
      } else {
        std::string line;
        for (size_t i = 0; i < sun_txt.length(); i++) {
          if (sun_txt[i] == '\n') {
            if (!line.empty() && y < 444) {
              it.print(28, y, id(font_footnote), id(white), line.c_str());
              y += 15;
              line.clear();
            }
          } else {
            line += sun_txt[i];
          }
        }
        if (!line.empty() && y < 444) {
          it.print(28, y, id(font_footnote), id(white), line.c_str());
          y += 15;
        }
      }
      // ═══ RIGHT COLUMN: WEATHER ═══
      it.line(410, 65, 410, 430, id(white));
      std::string cond = id(weather_condition).state;
      std::string wicon = "\U000F0599";
      if (cond == "Clear Night")       wicon = "\U000F0594";
      else if (cond == "Cloudy")       wicon = "\U000F0F2F";
      else if (cond == "Foggy")        wicon = "\U000F0594";
      else if (cond == "Partly Cloudy") wicon = "\U000F0595";
      else if (cond == "Heavy Rain")   wicon = "\U000F0597";
      else if (cond == "Rainy")        wicon = "\U000F0597";
      else if (cond == "Snowy")        wicon = "\U000F0598";
      else if (cond == "Sleet")        wicon = "\U000F0598";
      else if (cond == "Sunny")        wicon = "\U000F0599";
      else if (cond == "Windy")        wicon = "\U000F059B";
      else if (cond == "Windy Variant") wicon = "\U000F059B";
      else if (cond == "Stormy")       wicon = "\U000F0596";
      it.printf(605, 70, id(font_mdi_large), id(white), TextAlign::TOP_CENTER, "%s", wicon.c_str());
      float tmp = id(weather_temp).state;
      if (!std::isnan(tmp))
        it.printf(605, 160, id(font_pct), id(white), TextAlign::CENTER, "%.0f°C", tmp);
      else
        it.print(605, 160, id(font_body), id(white), TextAlign::CENTER, "--°C");
      float lo = id(weather_lo).state;
      float hi = id(weather_hi).state;
      it.printf(525, 210, id(font_mdi_small), id(white), TextAlign::TOP_CENTER, "\U000F0E02");
      if (!std::isnan(lo))
        it.printf(555, 212, id(font_body), id(white), "%.0f°", lo);
      else
        it.print(555, 212, id(font_body), id(white), "--°");
      it.printf(645, 210, id(font_mdi_small), id(white), TextAlign::TOP_CENTER, "\U000F0E03");
      if (!std::isnan(hi))
        it.printf(675, 212, id(font_body), id(white), "%.0f°", hi);
      else
        it.print(675, 212, id(font_body), id(white), "--°");
      // ═══ RIGHT COLUMN: SIGENERGY BATTERY ═══
      it.line(430, 260, 780, 260, id(white));
      it.printf(605, 275, id(font_mdi_large), id(white), TextAlign::TOP_CENTER, "\U000F1902");
      float bat = id(battery_soc).state;
      if (!std::isnan(bat))
        it.printf(605, 360, id(font_pct_small), id(white), TextAlign::TOP_CENTER, "%.0f%%", bat);
      else
        it.print(605, 360, id(font_body), id(white), TextAlign::TOP_CENTER, "--%");
      std::string bat_status;
      if (id(bat_charging).state)
        bat_status = "Charging";
      else if (id(bat_discharging).state)
        bat_status = "Discharging";
      else
        bat_status = "Idle";
      it.printf(605, 400, id(font_body), id(white), TextAlign::TOP_CENTER, "%s", bat_status.c_str());
      // ═══ FOOTER ═══
      it.print(20, 455, id(font_footnote), id(white), "Built with ESPHome");
      char ft_buf[32];
      strftime(ft_buf, sizeof(ft_buf), "Display refreshed at %H:%M", &now_lt);
      it.print(780, 455, id(font_footnote), id(white), TextAlign::TOP_RIGHT, ft_buf);
      ESP_LOGD("epaper", "=== Display lambda completed ===");      
Entity ID in trmnl.yaml Replace with yours
sensor.your_battery_soc Your battery SoC sensor
binary_sensor.your_battery_charging Your battery charging binary sensor
binary_sensor.your_battery_discharging Your battery discharging binary sensor
sensor.trmnl_weather_temperature Created above (keep same name)
sensor.trmnl_weather_low Created above (keep same name)
sensor.trmnl_weather_condition Created above (keep same name)
sensor.trmnl_weather_high Created above (keep same name)
input_text.trmnl_calendar Created above (keep same name)
input_text.trmnl_calendar_tomorrow Created above (keep same name)
input_text.trmnl_calendar_sat Created above (keep same name)
input_text.trmnl_calendar_sun Created above (keep same name)

Also set:

  • time.timezone: your IANA timezone
  • deep_sleep.sleep_duration: refresh interval (default: 60min)
  • Footer text: customize or remove

Weather Icons Link to heading

If your weather integration uses different condition labels, update the wicon if-else chain in the lambda to match.

Enclosure Link to heading

I used the TRMNL enclosure and drilled small holes in the back for 2mm round magnets so it sticks to any magnetic surface.

Battery Life Link to heading

~2000mAh LiPo with 60-minute refresh, roughly 4-6 weeks per charge.

Customization Ideas Link to heading

  • Multiple calendars: add more entity_id entries to calendar.get_events and merge results
  • Different weather source: swap weather.forecast_home with your entity, update the condition map
  • More sensors: the right column has space, extend the lambda to draw additional elements
  • Faster refresh: reduce sleep_duration (e.g. 30min), at the cost of battery life

Troubleshooting Link to heading

Symptom Fix
Blank screen Verify display model is 7.50inv2 in YAML; check GPIO wiring matches the Seeed guide
“No events” always Restart the calendar automation; verify calendar entity ID
Weather shows “–°C” Template sensors not updating; check entity in Developer Tools > States
Battery shows 0% Verify ADC wiring and voltage divider multiplier in sensor block
Won’t wake from deep sleep Hold BOOT, press RESET, release BOOT; re-flash via USB