Categories
IT

How to Use the Seeedstudio reTerminal E1001 with Home Assistant via ESPHome

The Seeedstudio reTerminal E1001 is more than just an IoT device — it is a powerful ESP32-S3 powered ePaper terminal that can act as a dedicated Home Assistant dashboard.

With its 7.5″ monochrome 800×480 screen, onboard sensors, and Wi-Fi/Bluetooth connectivity, it can display critical home automation data while consuming very little power.

In this guide, you’ll learn how to connect it to Home Assistant via ESPHome and create your own smart home control panel.

Why Use the reTerminal E1001 with Home Assistant?

Unlike a regular tablet, the reTerminal E1001 is built for embedded applications. It integrates an ESP32-S3 microcontroller with 32MB flash, PSRAM, and a 2000mAh battery. Its ultra-low power ePaper screen means you can run a smart home dashboard for weeks without frequent charging. Typical use cases include:

  • Displaying a (near) real-time smart home dashboard
  • Weather and calendar panels
  • Energy consumption and sensor data
  • Status for lights, HVAC, or alarms
  • Wall-mounted status display

What You Need

  • Seeedstudio reTerminal E1001 (7.5” ePaper, ESP32-S3 inside)
  • A running Home Assistant setup
  • ESPHome installed (via Home Assistant add-on or standalone)
  • USB-C cable for flashing
  • Wi-Fi network access
  • Some integrations in Home Assistant like the weather, a calendar

ESPHome Setup Steps in Home Assistant – Part 1

Follow these steps to flash ESPHome to the reTerminal E1001 and add it to Home Assistant.

  1. Connect the reTerminal E1001 to your computer with a USB-C data cable.
  2. In Home Assistant, open the ESPHome add-on from the sidebar and click New Device. Click Continue and give it a name, for example reTerminal 1001.
  3. Click Next, then select ESP32-S3 as the device type. A new device will be created in the background.
  4. Click Skip and then Edit on your device card. Copy the generated API code and keep it, you will need values from it later, to add it to ESPHome’s secrets

Now, replace the whole YAML code with this:

substitutions:
  name: reterminal-1001
  friendly_name: reTerminal 1001

  # Font sizes (compact preset)
  fs_date: "54"
  fs_h2: "28"
  fs_value: "34"
  fs_label: "22"
  fs_small: "18"

  # HA entities (keep your actual entity IDs)
  weather: weather.forecast_maison
  calendar: calendar.airbnb

  pages: 1
  run_duration: 60s
  sleep_duration: 3600s

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  on_boot:
    priority: 600
    then:
      - output.turn_on: bsp_battery_enable
      - delay: 200ms
      - component.update: battery_voltage
      - component.update: battery_level
      # Disable deep sleep when powered, allow it when on battery
      - if:
          condition:
            binary_sensor.is_on: is_powered
          then:
            - deep_sleep.prevent: deep_sleep_1
          else:
            - deep_sleep.allow: deep_sleep_1

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

logger:

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: "${friendly_name} Fallback"
    password: !secret fallback_ap_password

captive_portal:

i2c:
  scl: GPIO20
  sda: GPIO19

# -------- Debounced redraw script --------
script:
  - id: redraw_debounced
    mode: restart
    then:
      - delay: 1s
      - lambda: |-
          id(epaper_display).update();

# -------- Outputs --------
output:
  - platform: gpio
    pin: GPIO21       # VBAT_EN (enable battery measurement bridge)
    id: bsp_battery_enable

# -------- Fonts (include ° and :) --------
# Save this file as UTF-8 (no BOM) if you change text.
font:
  - file: "gfonts://Inter@800"
    id: font_date_big
    size: ${fs_date}
    glyphs: " !\"#%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz°"

  - file: "gfonts://Inter@700"
    id: font_h2
    size: ${fs_h2}
    glyphs: " !\"#%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz°"

  - file: "gfonts://Inter@700"
    id: font_value
    size: ${fs_value}
    glyphs: " !\"#%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz°"

  - file: "gfonts://Inter@600"
    id: font_label
    size: ${fs_label}
    glyphs: " !\"#%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz°"

  - file: "gfonts://Inter"
    id: font_small
    size: ${fs_small}
    glyphs: " !\"#%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz°"

# -------- Icons (B/W) --------
image:
  - file: mdi:wifi-strength-outline
    id: wifi0
    resize: 22x22
    type: BINARY
  - file: mdi:wifi-strength-1
    id: wifi1
    resize: 22x22
    type: BINARY
  - file: mdi:wifi-strength-2
    id: wifi2
    resize: 22x22
    type: BINARY
  - file: mdi:wifi-strength-3
    id: wifi3
    resize: 22x22
    type: BINARY
  - file: mdi:wifi-strength-4
    id: wifi4
    resize: 22x22
    type: BINARY

  - file: mdi:battery
    id: icon_battery
    resize: 26x26
    type: BINARY
  - file: mdi:power-plug
    id: icon_plug
    resize: 26x26
    type: BINARY

  # Weather (larger)
  - file: mdi:weather-sunny
    id: w_sunny
    resize: 96x96
    type: BINARY
  - file: mdi:weather-partly-cloudy
    id: w_partly
    resize: 96x96
    type: BINARY
  - file: mdi:weather-cloudy
    id: w_cloudy
    resize: 96x96
    type: BINARY
  - file: mdi:weather-rainy
    id: w_rain
    resize: 96x96
    type: BINARY
  - file: mdi:weather-pouring
    id: w_pour
    resize: 96x96
    type: BINARY
  - file: mdi:weather-snowy
    id: w_snow
    resize: 96x96
    type: BINARY
  - file: mdi:weather-windy
    id: w_wind
    resize: 96x96
    type: BINARY

  - file: mdi:calendar
    id: icon_calendar
    resize: 28x28
    type: BINARY

# -------- Globals --------
globals:
  - id: page_index
    type: int
    restore_value: no
    initial_value: '0'

# -------- Sensors --------
sensor:
  # Optional local SHT4x
  - platform: sht4x
    temperature:
      id: temp_sensor
      filters:
        - delta: 0.5
        - throttle: 3min
      on_value:
        then:
          - script.execute: redraw_debounced
    humidity:
      id: hum_sensor
      filters:
        - delta: 3
        - throttle: 3min
      on_value:
        then:
          - script.execute: redraw_debounced

  # Battery voltage
  - platform: adc
    pin: GPIO1
    id: battery_voltage
    internal: true
    attenuation: 12db
    filters:
      - multiply: 2.0

  # Battery percentage (internal; we don't print the %)
  - platform: template
    id: battery_level
    unit_of_measurement: "%"
    device_class: battery
    state_class: measurement
    lambda: 'return id(battery_voltage).state;'
    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

  # Wi-Fi RSSI -> only redraw if the number of bars changes
  - platform: wifi_signal
    id: wifi_rssi
    update_interval: 60s
    filters:
      - throttle: 5min
    on_value:
      then:
        - lambda: |-
            static int last_bars = -1;
            int bars = 0;
            if (!isnan(id(wifi_rssi).state)) {
              float r = id(wifi_rssi).state;
              if      (r > -55) bars = 4;
              else if (r > -65) bars = 3;
              else if (r > -72) bars = 2;
              else if (r > -80) bars = 1;
              else               bars = 0;
            }
            if (bars != last_bars) {
              last_bars = bars;
              id(redraw_debounced).execute();
            }

  # Outdoor temperature (from HA weather)
  - platform: homeassistant
    id: ha_temp_now
    entity_id: ${weather}
    attribute: temperature
    internal: true
    on_value:
      then:
        - script.execute: redraw_debounced

  # Outdoor humidity (from HA weather)
  - platform: homeassistant
    id: ha_humidity
    entity_id: ${weather}
    attribute: humidity
    internal: true
    on_value:
      then:
        - script.execute: redraw_debounced

  # Indoor temperature (hall sensor)
  - platform: homeassistant
    id: indoor_temp
    entity_id: sensor.capteur_couloir_temperature
    internal: true
    on_value:
      then:
        - script.execute: redraw_debounced

  # VBUS detection via ADC (GPIO2)
  - platform: adc
    id: vbus_sense
    pin: GPIO2
    attenuation: 11db
    update_interval: 2s
    filters:
      - multiply: 2.0
    on_value:
      then:
        - lambda: |-
            const bool powered = !isnan(id(vbus_sense).state) && id(vbus_sense).state > 4.5; // threshold
            id(is_powered).publish_state(powered);
            id(redraw_debounced).execute();

# -------- Text sensors (from HA) --------
text_sensor:
  # Weather condition for icon mapping
  - platform: homeassistant
    id: ha_condition
    entity_id: ${weather}
    internal: true
    on_value:
      then:
        - script.execute: redraw_debounced

  # Next Airbnb event
  - platform: homeassistant
    id: cal_msg
    entity_id: ${calendar}
    attribute: message
    internal: true
    on_value:
      then:
        - script.execute: redraw_debounced
  - platform: homeassistant
    id: cal_start
    entity_id: ${calendar}
    attribute: start_time
    internal: true
    on_value:
      then:
        - script.execute: redraw_debounced
  - platform: homeassistant
    id: cal_end
    entity_id: ${calendar}
    attribute: end_time
    internal: true
    on_value:
      then:
        - script.execute: redraw_debounced

# -------- Binary sensors --------
binary_sensor:
  # Powered (USB/charger) vs battery
  - platform: template
    id: is_powered
    internal: true

# -------- Time: date only (no clock) --------
time:
  - platform: homeassistant
    id: ha_time
    on_time:
      - minutes: /30
        then:
          - script.execute: redraw_debounced

# -------- SPI + Display --------
spi:
  clk_pin: GPIO7
  mosi_pin: GPIO9

display:
  - platform: waveshare_epaper
    id: epaper_display
    model: 7.50inv2
    cs_pin: GPIO10
    dc_pin: GPIO11
    reset_pin:
      number: GPIO12
      inverted: false
    busy_pin:
      number: GPIO13
      inverted: true
    update_interval: never
    lambda: |-
      // Light anti-ghosting every ~30 updates
      static int redraws = 0;
      if (++redraws >= 30) { it.fill(Color::WHITE); redraws = 0; }

      // ====== Top bar: Date (EN) + Wi-Fi + Power/Battery ======
      auto now = id(ha_time).now();
      struct tm t = now.to_c_tm();

      const char* W[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
      const char* M[] = {"January","February","March","April","May","June","July","August","September","October","November","December"};
      it.printf(400, 10, id(font_date_big), TextAlign::TOP_CENTER,
                "%s %d %s %d",
                W[t.tm_wday], t.tm_mday, M[t.tm_mon], t.tm_year + 1900);

      // Wi-Fi bars (left)
      int bars = 0;
      if (!isnan(id(wifi_rssi).state)) {
        float r = id(wifi_rssi).state;
        if      (r > -55) bars = 4;
        else if (r > -65) bars = 3;
        else if (r > -72) bars = 2;
        else if (r > -80) bars = 1;
        else               bars = 0;
      }
      if      (bars==4) it.image(16, 16, id(wifi4));
      else if (bars==3) it.image(16, 16, id(wifi3));
      else if (bars==2) it.image(16, 16, id(wifi2));
      else if (bars==1) it.image(16, 16, id(wifi1));
      else              it.image(16, 16, id(wifi0));

      // Power/Battery icon (right) — no % text
      bool powered = id(is_powered).state;
      if (powered) it.image(734, 16, id(icon_plug));
      else         it.image(734, 16, id(icon_battery));

      // Separator
      it.line(0, 96, 799, 96);

      // ====== Left column: Weather (current) ======
      std::string cond = id(ha_condition).state;
      int icon_x = 36, icon_y = 120;
      if      (cond == "sunny" || cond == "clear-night") it.image(icon_x, icon_y, id(w_sunny));
      else if (cond == "partlycloudy")                   it.image(icon_x, icon_y, id(w_partly));
      else if (cond == "cloudy")                         it.image(icon_x, icon_y, id(w_cloudy));
      else if (cond == "pouring")                        it.image(icon_x, icon_y, id(w_pour));
      else if (cond == "rainy")                          it.image(icon_x, icon_y, id(w_rain));
      else if (cond == "snowy" || cond == "snowy-rainy") it.image(icon_x, icon_y, id(w_snow));
      else if (cond == "windy")                          it.image(icon_x, icon_y, id(w_wind));
      else                                               it.image(icon_x, icon_y, id(w_partly));

      // Value rows (Indoor, Outdoor, Outdoor humidity)
      auto row = [&](int y, const char* label, float v, const char* unit){
        it.printf(160, y, id(font_label), TextAlign::LEFT, "%s", label);
        if (!isnan(v)) it.printf(380, y, id(font_value), TextAlign::RIGHT, "%.0f%s", v, unit);
        else           it.printf(380, y, id(font_value), TextAlign::RIGHT, "--");
      };

      row(140, "Indoor",           id(indoor_temp).state, "°C");
      row(190, "Outdoor",          id(ha_temp_now).state, "°C");
      row(240, "Outdoor humidity", id(ha_humidity).state, "%");

      // Vertical separator
      it.line(410, 96, 410, 479);

      // ====== Right column: Next event (Airbnb) ======
      it.image(430, 120, id(icon_calendar));
      it.printf(468, 120, id(font_h2), TextAlign::LEFT, "Next event");

      // Simple word-wrap (avoid overflow)
      auto wrap_print = [&](int x, int y, int max_chars_per_line, const std::string& s){
        if (s.empty()) return;
        int start = 0; int line = 0; int n = (int)s.size();
        while (start < n && line < 4) {
          int len = std::min(max_chars_per_line, n - start);
          int cut = len;
          if (start + len < n && s[start+len] != ' ') {
            for (int k = start + len; k > start; --k) { if (s[k] == ' ') { cut = k - start; break; } }
          }
          std::string part = s.substr(start, cut);
          it.printf(x, y + line*30, id(font_label), TextAlign::LEFT, "%s", part.c_str());
          start += cut;
          while (start < n && s[start] == ' ') start++;
          line++;
        }
      };

      std::string msg = id(cal_msg).state;
      wrap_print(430, 160, 34, msg);   // ≈34 chars/line with current fonts

      if (!id(cal_start).state.empty())
        it.printf(430, 300, id(font_small), TextAlign::LEFT, "Start: %s", id(cal_start).state.c_str());
      if (!id(cal_end).state.empty())
        it.printf(430, 324, id(font_small), TextAlign::LEFT, "End  : %s",   id(cal_end).state.c_str());

# -------- Deep sleep --------
deep_sleep:
  id: deep_sleep_1
  run_duration: ${run_duration}
  sleep_duration: ${sleep_duration}
  wakeup_pin: GPIO3
  wakeup_pin_mode: INVERT_WAKEUP

Click Save. Close the window.

    Next: ESPHome – Set up secrets

    In the Secrets menu of your ESPHome plugin, paste and adapt the following

    # Your Wi-Fi SSID and password
    wifi_ssid: "your wifi name"
    wifi_password: "your wifi password"
    fallback_ap_password: "hotspotpassword"
    api_encryption_key: "you got this value before"
    ota_password: "your ota password"
    

    Save this and go back to the main ESPHome window.

    This will be used by our previous YAML and can be mutualized across several devices.

    Go back to your device and click Install, like so :

    1. Install. Choose Manual download and wait for the compilation to finish.
    2. When the build completes, download the firmware in Modern format to obtain a .bin file.
    3. Ensure the reTerminal is connected to your computer via the rear USB-C port.
    4. Open the ESP web flasher page and click Connect. In the popup, select your board and click Connect again.
    5. Click Install, select the .bin file you downloaded, then click Install to flash.
    6. Back in Home Assistant, go to Settings → Devices & Services. Your device should appear with a Configure prompt. If not, click Add Integration, search for ESPHome, and enter the device IP in the Host field.
    7. Done. The reTerminal E1001 is now integrated with Home Assistant and its entities are available.. and the dashboard should show up.

    If values are missing, it is because you need to adapt the code to your Home Assistant “entities”.

    For example these two lines:

      # HA entities (keep your actual entity IDs)
      weather: weather.forecast_maison
      calendar: calendar.airbnb
    

    Conclusion

    The reTerminal E1001 combined with ESPHome and Home Assistant gives you a flexible, low-power, and professional-grade smart home dashboard.

    With YAML configuration, you can fully customize it to match your needs — whether for monitoring, control, or automation display.

    Thanks to Aguacatec for the initial post!

    Leave a Reply