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.
- Connect the reTerminal E1001 to your computer with a USB-C data cable.
- 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.
- Click Next, then select ESP32-S3 as the device type. A new device will be created in the background.
- 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 :

- Install. Choose Manual download and wait for the compilation to finish.
- When the build completes, download the firmware in Modern format to obtain a
.bin
file. - Ensure the reTerminal is connected to your computer via the rear USB-C port.
- Open the ESP web flasher page and click Connect. In the popup, select your board and click Connect again.
- Click Install, select the
.bin
file you downloaded, then click Install to flash. - 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.
- 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.