Seeed Studio reTerminal E1003 Paper White Display

This blog entry has ended up as a multi-day job thanks to the reTerminal E Series E1003 10.3″ display being fairly new and information being scattered all over – I’m sure I’ve helped Seeed update their info and so if this interests you, between my final ESPHOME YAML code at the bottom, much experimenting with the beep and the recently updated info on the Seeed site, it should be easy to make a working touch display. I’m sure there has to be a better way of lining up touch coordinates with the my actual pump and light examples – but I’ve had enough for a while… it all works just fine.

The display – which runs on it’s internal battery, consuming very little power and USB-chargeable, arrived on my desk just a few days ago and at first, when I looked at the Seeed site, I couldn’t see any reference to setting it up. I emailed Seeed, and within hours their website seemed to be updated. I found sufficient information to run a basic Home Assistant demo in ESPHome because this device uses an ESP32 driving a very pretty 1872 by 1404 grayscale paper display.

Seed E1003

But I’m jumping ahead, I firstly opened the package…

Seeed E1003 display

and here it is (that strange pattern is my cave-home roof, not the display). If you’ve noticed that roof in the image above – I should say that Seeed have taken this into consideration and apparently the next batch of boards will have a less glossy finish.

I plugged the display (which comes with it’s own USB-C to USB-A extension lead) into USB on my PC and ran one of their simple demos requiring no installation on the PC – all from a web page (a bit like setting up TASMOTA or WLED – first time USB connection, from there-on over the wireless lan).

This is what you see on first applying power (mine arrived partly charged and there’s a tiny on-off switch on the side):

E1003 display from Seeed

As a seasoned Tasmota user I understood the above immediately… might’ve been better if they’d said “Open ‘192.168.4.1’ on your phone or other Wi-Fi-enabled device…” but I’m being picky.

Then I found that my ESPHome installation (fully up to date at July 22, 2026) magically had a basic demo for the E1003 – see image further down… but note at the top – the line that says “reTerminal E1003 demo?” – I ran the first install without having a clue what I was doing and it just worked like magic – but then, my first epiphamy – the source code is in ESPHOME and is kind of inderstandable, enough that I could add that “demo?” text to the top line – that’s the ONLY change I made initially to the demo code.

E1003 display

This time I deliberately disconnected the USB lead which had remained connected overnight, safe in the knowledge that the battery would be fully charged and as expected, I could do an update over the air.

he second epiphany came when I took a look back to September last year and realized that I already had some experience of this in the form of their earlier, smaller terminal, the Seeed 7.5″ ePaper panel which had/has a 7-inch display.

Original seed 7.5" display

While nice, it was pretty low resolution and, in the end, not a great deal of use to me. This one however, with a 1872 by 1404 display, I can see lots of uses for this, showing all sorts of sensor states, weather information and more from Home Assistant from my entities in Home Assistant.

Armed with the success of my trivial change, I went back over the original blog and as you’ll see, adding some HA entities was trivial – once I’m done here I’ll set about getting to grips with that and the new display logic in the E1003 – pretty clear in the ESPHome Demo – and figure out how to make some buttons. The only thing I’m not clear about is the touch-sensitivity – but having first tried one of Seeed’s demos which showed basic touch sensitivity logic – how hard can it be?

So if you’re new to all of this – you might be thinking – a display – so what? Well, firstly it is of course programmable and secondly runs for months (depending on how may screen changes you make) on the internal battery. It also has a couple of discrete buttons on the top fo switching screen pages – but bewaer, updates ar not instant so dynamic, high speed movement is out. As for regular updates – if that’s your intende applicationb, well you could always leave it on USB charge. I just have it connected to one of the USB ports on my PC so no major current requirements.

As for adding sensors – I’m pretty sure this code from my old 7.5″ display is useable… just as an example..

sensor: 
  - platform: homeassistant
    id: temp1
    entity_id: sensor.snzb_02ld_temperature

So I’m looking at the code I used in the old 7.5″ display…. above… and the method of displaying is the same for the new display – going from their demo. If you look at my original blog… there’s a line :

      it.printf(it.get_width() - 10, 10, id(font1), COLOR_ON, TextAlign::RIGHT, "Tank temperature %.1f°C", id(temp1).state);

that prints out my old sensor…. looking at the ESPHome demo code, my code above SHOULD pretty much drop into their demo. Can it be THAT easy? My office temperature sensor uses a Sonoff SNZB 02WD sensor – so sensor.snzb_02wd_temperature should just drop right in…

E1003 display from Seeed

Ok, so the actual temperature doesn’t display – it turns out that the stock demo shows only once – a single line in the display section needs changing from “never” to in my case “30s”.

For my next trick with help from my pal chatGPT I put the office temperature text and value in a reversed out box. Because the display is high definition, it looks ok even without graphics. It’s a start anyway.

Seeed E1003

The next step is to look over their examples to see how the touch works and add that and touch different parts of the screen to get it to do things. Oh and sound for notifications.

Be aware – as I’ve now found out – a screen refresh for this technology is not pretty – but then I have a very expensive Switchbot AI colour photo frame that is exactly the same when transitioning images.

Update after days of struggle

Depending on your needs, I might just save someone a lot of work here. Between reading various articles that are just plain wrong for this display, contacting Seeed, talking to Claude and ChatGPT and refreshing my C++ skills, I finally have the beginnings of a Home Assistant ePaper dashboard that works for me.

In the code below I have 4 temperature/humidity sensors and 2 test outputs – a pool pump and a light, both of the latter have a fairly optimised beep on touch… (all from my Home Assistant setup) and having remembered how to put often-used code into scripts – here is the entirety of my YAML code (all one file) to run the display in ESPHome with Home Assistant. I like it as a start, at some point (I’ve checked – loads of code space left) I’ll likely have a dozen or more touch-sensitive outputs and maybe 20 or more sensor readings… I’ll likely add the weather – but I’m stopping the blog here while this is at least semi-readable.

I hope it is useful to someone – it all works – I had a 30 second display update – now reduced to only on-state-changes and every minute (at second 0) for the clock – so you can forget long battery life (easy to change the automatic update interval). and touch responses – beep is instant when the output block screen areas are touched, display update on touch takes less than a second – given the nature of these displays, that’s not bad.

Note the weather icons are there for the future. A couple of times I ended up with small – maybe 96px square images (starting off as transparent PNGs taken from the web then using Windows Paint to remove the transparency before bringing into ESPHome and storing in the ESPHOME folder (I know, I should really have put them in esphome/images to be professional 🙂 )

The various sensor names are straight out of my Home Assistant entities. I hope the yaml in here copies ok, indentation is very important.

Oh, last minute update thanks to Claude – substitutions – I originally had to define the button x and y coordinates and widths then separately code the touch areas – thanks to “substitutions” the repetition is gone it should now be EASY to add more outputs or move the buttons about…

Clearly – anyone using this should replace home assistant sensor and output IDs with their own.

Each sensor or output has an on_state or on_value section to force a screen update rather than having regular power-wasting screen updates, though in the interval section I have a 60 second update just for the clock. That can be adjusted or removed as needed (if you don’t display a clock, you don’t need it).

If you want to use the code below – in the YAML box – hover at the top right – click RAW then COPY….

substitutions:
  name: reterminal-e1003
  friendly_name: "reTerminal E1003"
  # Button layout: x, y = top-left corner, w/h = size of the drawn box
  pool_pump_x: 1410
  pool_pump_y: 1000
  pool_pump_w: 430
  pool_pump_h: 90

  office_lights_x: 1410
  office_lights_y: 1100
  office_lights_w: 430
  office_lights_h: 90
  
esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  on_boot:
    priority: -100
    then:
      - delay: 2s
      - it8951.update:
          id: epaper_display
          mode: GC16
          
esp32:
  board: seeed_xiao_esp32s3
  framework:
    type: esp-idf

psram:
  mode: octal

logger:
  hardware_uart: UART0
  baud_rate: 115200
  level: INFO

api:

ota:
  - platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: "${name} Fallback"
    password: "12345678"

globals: ##to set default reboot behavior
  - id: wifi_connection
    type: bool
    restore_value: no
    initial_value: "false"

interval:
  - interval: 10s
    then:
      - if:
          condition:
            wifi.connected:
          then:
            - lambda: |-
                bool changed = !id(wifi_connection);
                id(wifi_connection) = true;
                if (changed) {
                  id(refresh_display)->execute();
                }
          else:
            - lambda: |-
                bool changed = id(wifi_connection);
                id(wifi_connection) = false;
                if (changed) {
                  id(refresh_display)->execute();
                }
         
time:
  - platform: homeassistant
    id: homeassistant_time
    on_time:
      - seconds: 0
        then:
          - script.execute: refresh_display

captive_portal:
  
# Not needed for ESPHOME 2026.7.0 onwards
#external_components:
#  - source: github://pr#15346
#    components: [it8951]
#    refresh: 1h

spi:
  - id: epaper_spi_bus
    clk_pin: GPIO7
    miso_pin: GPIO8
    mosi_pin: GPIO9

sensor:
  - platform: homeassistant
    id: outside_temperature
    entity_id: sensor.outdoor_meter_65_temperature 
    on_value:
      then:
        - script.execute: refresh_display
  - platform: homeassistant
    id: outside_humidity
    entity_id: sensor.outdoor_meter_65_humidity
    on_value:
      then:
        - script.execute: refresh_display
  - platform: homeassistant
    id: office_temperature
    entity_id: sensor.snzb02m_temperature 
    on_value:
      then:
        - script.execute: refresh_display
  - platform: homeassistant
    id: office_humidity
    entity_id: sensor.snzb02m_humidity
    on_value:
      then:
        - script.execute: refresh_display
  - platform: homeassistant
    id: livingroom_temperature
    entity_id: sensor.shellyhtg3_54320463831c_temperature
    on_value:
      then:
        - script.execute: refresh_display
  - platform: homeassistant
    id: livingroom_humidity
    entity_id: sensor.shellyhtg3_54320463831c_humidity
    on_value:
      then:
        - script.execute: refresh_display
  - platform: homeassistant
    id: uk_outside_temperature
    entity_id: sensor.d1_am2301_temperature 
    on_value:
      then:
        - script.execute: refresh_display
  - platform: homeassistant
    id: uk_outside_humidity
    entity_id: sensor.d1_am2301_humidity 
    on_value:
      then:
        - script.execute: refresh_display
    
binary_sensor:
  - platform: homeassistant
    id: office_lights
    entity_id: light.office_lights 
    on_state:
      then:
        - script.execute: refresh_display   
          
  - platform: homeassistant
    id: pool_pump
    entity_id: switch.pool_pump
    on_state:
      then:
        - script.execute: refresh_display
          
  - platform: touchscreen
    name: "Pool Pump Touch"
    id: pool_pump_touch
    x_min: ${pool_pump_x}
    x_max: ${pool_pump_x + pool_pump_w}
    y_min: ${pool_pump_y}
    y_max: ${pool_pump_y + pool_pump_h}
    filters:
      - delayed_on_off: 100ms
    on_press:
      then:
        - homeassistant.action:
            action: switch.toggle
            data:
              entity_id: switch.pool_pump
        - script.execute: beep
          
  - platform: touchscreen
    name: "Office Lights Touch"
    id: office_lights_touch
    x_min: ${office_lights_x}
    x_max: ${office_lights_x + office_lights_w}
    y_min: ${office_lights_y}
    y_max: ${office_lights_y + office_lights_h}
    filters:
      - delayed_on_off: 100ms
    on_press:
      then:
        - homeassistant.action:
            action: light.toggle
            data:
              entity_id: light.office_lights
        - script.execute: beep

script:
  - id: beep
    then:
      - output.set_level:
          id: buzzer_pwm
          level: 50%
      - delay: 80ms
      - output.turn_off: buzzer_pwm

  - id: refresh_display
    mode: restart
    then:
      - delay: 200ms
      - component.update: epaper_display
      
image:
  - file: "images/homeassistant-icon-96px.png"
    id: ha_logo
    type: BINARY
    
  - file: "toggle_switch_off.png"
    id: switch_off
    type: BINARY

  - file: "toggle_switch_on.png"
    id: switch_on
    type: BINARY
    
  - file: "lightbulb_off.png"
    id: light_off
    type: BINARY
    resize: 82x82

  - file: "lightbulb_on.png"
    id: light_on
    type: BINARY
    resize: 82x82

i2c:
  sda: GPIO19
  scl: GPIO20

output:
  - platform: ledc
    pin: GPIO45
    id: buzzer_pwm
    frequency: 2000Hz  

touchscreen:
  platform: gt911
  id: touch_screen
  interrupt_pin: GPIO2   # board's actual interrupt pin contrary to initial advice from Claude
  #reset_pin: GPIO48     # only if your wiring uses one - that IS the reset pin according to Seeed engineers


  
display:
  - platform: it8951
    id: epaper_display
    spi_id: epaper_spi_bus
    model: seeed-reterminal-e1003
    update_interval: never # was 30s
    full_update_every: 30
    grayscale: true
    dithering: true
    update_mode: GC16
    transform:
      mirror_x: true
      mirror_y: false
    lambda: |-
      const Color light_grey(240, 240, 240);
      const Color dark_grey(100, 100, 100);
      enum IconType
      {
          LIGHT,
          POWER
      };      
      
      // function to print a sensor and it's bounding box
      auto draw_sensor = [&](int h1, int v1,
                             const char *title,
                             float temp,
                             float hum=NAN)
      {
    
          it.filled_rectangle(h1, v1, 320, 90, Color::BLACK);
          it.filled_rectangle(h1+320, v1, 320, 90, light_grey);
          it.rectangle(h1+320, v1, 320, 90, Color::BLACK);
          it.rectangle(h1+321, v1+1, 318, 88, Color::BLACK);   
          it.print(h1+10, v1+45, id(text_font), Color::WHITE, TextAlign::CENTER_LEFT, title);
      
          if (isnan(temp))
          {
             it.print(h1+480, v1+45,
             id(text_font), Color::BLACK,
             TextAlign::CENTER,
             "--");
          }
          else if (isnan(hum))
          {
               it.printf(h1+480, v1+45,
                    id(text_font), Color::BLACK,
                    TextAlign::CENTER,
                    "%.0f°C",
                    temp);
          }
          else
          {
               it.printf(h1+480, v1+45,
                    id(text_font), Color::BLACK,
                    TextAlign::CENTER,
                    "%.0f°C  %.0f%%",
                    temp, hum);
          }          
      };
      // end function


      auto draw_state = [&](int h1, int v1,
                            const char *title,
                            bool is_on,
                            IconType type)
      {
          // Black title box
          it.filled_rectangle(h1, v1, 320, 90, Color::BLACK);
          
          // Title
          it.print(h1 + 15, v1 + 45,
                   id(text_font),
                   Color::WHITE,
                   TextAlign::CENTER_LEFT,
                   title);
      
          // Draw state indicator
          switch (type)
          {
              case LIGHT:
                  it.image(h1 + 332, v1+2,
                           is_on ? id(light_on) : id(light_off));
                  break;   
                  
              case POWER:
                  it.image(h1 + 325, v1,
                           is_on ? id(switch_on) : id(switch_off));
                  break;
          }
          // Icon/image box
          it.rectangle(h1 + 320, v1, 110, 90, Color::BLACK);
          it.rectangle(h1 + 321, v1 + 1, 108, 88, Color::BLACK);
          
      };     
      
      it.fill(Color::WHITE); // screen white

      it.rectangle(20, 20, it.get_width() - 40, it.get_height() - 40, Color::BLACK);  // thin border    
      it.line(20, 1230, it.get_width()-20, 1230, Color::BLACK);  // thin divider    
      it.printf(it.get_width() / 2, 1300, id(title_font), Color::BLACK, TextAlign::CENTER, "Home Assistant ePaper Dashboard"); // title

      // WiFi indicator top left
      it.filled_rectangle(30, 30, 130, 40, Color::BLACK);
      it.filled_rectangle(115, 30, 2, 40, Color::WHITE);
      it.printf(35, 30, id(small_font),Color::WHITE, "WiFi");                  
      it.printf(120, 33, id(icon_font),Color::WHITE, "%s", id(wifi_connection) ? "\U000F05A9" : "\U000F05AA");                  

      // timestamp top right
      char str[22];
      time_t currTime = id(homeassistant_time).now().timestamp;
      it.filled_rectangle(1280, 30, 560, 60, Color::BLACK);
      strftime(str, sizeof(str), "%a %H:%M %d/%m/%Y", localtime(&currTime));
      it.printf(1280,30, id(text_font), Color::WHITE, " %s", str);

      draw_sensor(30,800,"UK outside", id(uk_outside_temperature).state, id(uk_outside_humidity).state);
      draw_sensor(30,900,"Outside", id(outside_temperature).state, id(outside_humidity).state);
      draw_sensor(30,1000,"Office", id(office_temperature).state, id(office_humidity).state);
      draw_sensor(30,1100,"Livingroom", id(livingroom_temperature).state, id(livingroom_humidity).state);

      draw_state(${pool_pump_x},${pool_pump_y},"Pool pump",id(pool_pump).state,POWER);      
      draw_state(${office_lights_x},${office_lights_y},"Office lights",id(office_lights).state,LIGHT); 
      
      it.image(40, 1260, id(ha_logo));     
      it.image(it.get_width() - 140, 1260, id(ha_logo));      

font:
  - file: "gfonts://Roboto"
    id: title_font
    size: 96
  - file: "gfonts://Roboto"
    id: text_font
    size: 52
  - file: "gfonts://Roboto"
    id: small_font
    size: 36
    
  - file: "fonts/materialdesignicons-webfont.ttf"
    id: icon_font
    size: 36
    glyphs: [
      "\U000F05A9", #wifi
      "\U000F05AA", #no wifi
      "\U000F050F", # Thermometer
      "\U000F058E", # Humidity
      "\U000F1AAF", # heating coil
      "\U000F059D", # Wind speed
      "\U000F0D60", # Atmospheric pressure
      "\U000F0590", # Cloudy weather
      "\U000F0596", # Rainy weather
      "\U000F0598", # Snowy weather
      "\U000F0599", # Sunny weather
      "\U000F078F", # light bulb on
      "\U000F171E", # light bulb off
      "\U000F0F92", # boiler on
      "\U000F0433", # boiler off
      "\U000F07E4", # CO2
      ]

  - file: "fonts/materialdesignicons-webfont.ttf"
    id: large_icon_font
    size: 72
    glyphs: [
      "\U000F05A9", #wifi
      "\U000F05AA", #no wifi
      "\U000F050F", # Thermometer
      "\U000F058E", # Humidity
      "\U000F1AAF", # heating coil
      "\U000F059D", # Wind speed
      "\U000F0D60", # Atmospheric pressure
      "\U000F0590", # Cloudy weather
      "\U000F0596", # Rainy weather
      "\U000F0598", # Snowy weather
      "\U000F0599", # Sunny weather
      "\U000F078F", # light bulb on
      "\U000F171E", # light bulb off
      "\U000F0F92", # boiler on
      "\U000F0433", # boiler off
      "\U000F07E4", # CO2
      ]

Finally some links – Seeed have been so helpful.

Getting Started guide.

Touch example.

That’s it for now. I hope this is useful. Ok I lied about pausing for a while – I’ve done more but as the code now looks a little more complicated, I suggest anyone wanting to use this tries the above code first.

Now I’m starting to split the display into pages – what’s the big deal about that ? Well not a lot unless you are using the touch facility. You can split your display code into one or more pages and jump between them using the physical buttons… I’ve used the green button to go to the home page and the other two buttons to sequence through any available pages. All very good – but I made an empty test page… then on a hunch, touched the area where one of my control buttons WOULD have been – sure enough – I heard the beep and managed to turn off my office lights – even on a blank page. The solution is simple – you can make a touch-screen action page-dependent. Check out the updated code below… notice the additional condition on each button area in platform: touchscreen.

substitutions:
  name: reterminal-e1003
  friendly_name: "reTerminal E1003"
  # Button layout: x, y = top-left corner, w/h = size of the drawn box
  pool_pump_x: 1410
  pool_pump_y: 1000
  pool_pump_w: 430
  pool_pump_h: 90

  office_lights_x: 1410
  office_lights_y: 1100
  office_lights_w: 430
  office_lights_h: 90
  
esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  on_boot:
    priority: -100
    then:
      - delay: 2s
      #- it8951.update:
      #    id: epaper_display
      #    mode: GC16
      - display.page.show: main_page
      - component.update: epaper_display         
esp32:
  board: seeed_xiao_esp32s3
  framework:
    type: esp-idf

psram:
  mode: octal

logger:
  hardware_uart: UART0
  baud_rate: 115200
  level: INFO

api:

ota:
  - platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: "${name} Fallback"
    password: "12345678"

globals: ##to set default reboot behavior
  - id: wifi_connection
    type: bool
    restore_value: no
    initial_value: "false"

interval:
  - interval: 10s
    then:
      - if:
          condition:
            wifi.connected:
          then:
            - lambda: |-
                bool changed = !id(wifi_connection);
                id(wifi_connection) = true;
                if (changed) {
                  id(refresh_display)->execute();
                }
          else:
            - lambda: |-
                bool changed = id(wifi_connection);
                id(wifi_connection) = false;
                if (changed) {
                  id(refresh_display)->execute();
                }
         
time:
  - platform: homeassistant
    id: homeassistant_time
    on_time:
      - seconds: 0
        then:
          - script.execute: refresh_display

captive_portal:
  
# Not needed for ESPHOME 2026.7.0 onwards
#external_components:
#  - source: github://pr#15346
#    components: [it8951]
#    refresh: 1h

spi:
  - id: epaper_spi_bus
    clk_pin: GPIO7
    miso_pin: GPIO8
    mosi_pin: GPIO9

sensor:
  - platform: homeassistant
    id: outside_temperature
    entity_id: sensor.outdoor_meter_65_temperature 
    on_value:
      then:
        - script.execute: refresh_display
  - platform: homeassistant
    id: outside_humidity
    entity_id: sensor.outdoor_meter_65_humidity
    on_value:
      then:
        - script.execute: refresh_display
  - platform: homeassistant
    id: office_temperature
    entity_id: sensor.snzb02m_temperature 
    on_value:
      then:
        - script.execute: refresh_display
  - platform: homeassistant
    id: office_humidity
    entity_id: sensor.snzb02m_humidity
    on_value:
      then:
        - script.execute: refresh_display
  - platform: homeassistant
    id: livingroom_temperature
    entity_id: sensor.shellyhtg3_54320463831c_temperature
    on_value:
      then:
        - script.execute: refresh_display
  - platform: homeassistant
    id: livingroom_humidity
    entity_id: sensor.shellyhtg3_54320463831c_humidity
    on_value:
      then:
        - script.execute: refresh_display
  - platform: homeassistant
    id: uk_outside_temperature
    entity_id: sensor.d1_am2301_temperature 
    on_value:
      then:
        - script.execute: refresh_display
  - platform: homeassistant
    id: uk_outside_humidity
    entity_id: sensor.d1_am2301_humidity 
    on_value:
      then:
        - script.execute: refresh_display
    
binary_sensor:
  - platform: homeassistant
    id: office_lights
    entity_id: light.office_lights 
    on_state:
      then:
        - script.execute: refresh_display   
          
  - platform: homeassistant
    id: pool_pump
    entity_id: switch.pool_pump
    on_state:
      then:
        - script.execute: refresh_display

  - platform: gpio
    pin:
      number: GPIO3   # Key 0 - green button
      inverted: true
    name: "Home Button"
    on_press:
      then:
        - display.page.show: main_page
        - component.update: epaper_display          
          
  - platform: gpio
    pin:
      number: GPIO4   # Key 1
      inverted: true
    name: "Page Down Button"
    on_press:
      then:
        - display.page.show_next: epaper_display
        - component.update: epaper_display

  - platform: gpio
    pin:
      number: GPIO5   # Key 2
      inverted: true
    name: "Page Up Button"
    on_press:
      then:
        - display.page.show_previous: epaper_display
        - component.update: epaper_display
          
          
          
  - platform: touchscreen
    name: "Pool Pump Touch"
    id: pool_pump_touch
    x_min: ${pool_pump_x}
    x_max: ${pool_pump_x + pool_pump_w}
    y_min: ${pool_pump_y}
    y_max: ${pool_pump_y + pool_pump_h}
    filters:
      - delayed_on_off: 100ms
    on_press:
      then:
        - if:
            condition:
              display.is_displaying_page:
                id: epaper_display
                page_id: main_page      
            then:
              - homeassistant.action:
                  action: switch.toggle
                  data:
                    entity_id: switch.pool_pump
              - script.execute: beep
          
  - platform: touchscreen
    name: "Office Lights Touch"
    id: office_lights_touch
    x_min: ${office_lights_x}
    x_max: ${office_lights_x + office_lights_w}
    y_min: ${office_lights_y}
    y_max: ${office_lights_y + office_lights_h}
    filters:
      - delayed_on_off: 100ms
    on_press:
      then:
        - if:
            condition:
              display.is_displaying_page:
                id: epaper_display
                page_id: main_page 
            then:
              - homeassistant.action:
                  action: light.toggle
                  data:
                    entity_id: light.office_lights
              - script.execute: beep

script:
  - id: beep
    then:
      - output.set_level:
          id: buzzer_pwm
          level: 50%
      - delay: 80ms
      - output.turn_off: buzzer_pwm

  - id: refresh_display
    mode: restart
    then:
      - delay: 200ms
      - component.update: epaper_display
      
image:
  - file: "images/homeassistant-icon-96px.png"
    id: ha_logo
    type: BINARY
    
  - file: "toggle_switch_off.png"
    id: switch_off
    type: BINARY

  - file: "toggle_switch_on.png"
    id: switch_on
    type: BINARY
    
  - file: "lightbulb_off.png"
    id: light_off
    type: BINARY
    resize: 82x82

  - file: "lightbulb_on.png"
    id: light_on
    type: BINARY
    resize: 82x82

i2c:
  sda: GPIO19
  scl: GPIO20

output:
  - platform: ledc
    pin: GPIO45
    id: buzzer_pwm
    frequency: 2000Hz  

touchscreen:
  platform: gt911
  id: touch_screen
  interrupt_pin: GPIO2   # board's actual interrupt pin contrary to initial advice from Claude
  #reset_pin: GPIO48     # only if your wiring uses one - that IS the reset pin according to Seeed engineers


  
display:
  - platform: it8951
    id: epaper_display
    spi_id: epaper_spi_bus
    model: seeed-reterminal-e1003
    update_interval: never # was 30s
    full_update_every: 30
    grayscale: true
    dithering: true
    update_mode: GC16
    transform:
      mirror_x: true
      mirror_y: false
    pages:

      - id: title_page
        lambda: |-
          it.fill(Color::WHITE);
          it.printf(it.get_width() / 2, it.get_height() / 2,
                    id(title_font), Color::BLACK, TextAlign::CENTER, 
                    "Home Assistant ePaper Dashboard");

      
      - id: main_page
      
        lambda: |-
          const Color light_grey(210, 210, 210);
          const Color dark_grey(100, 100, 100);
          enum IconType
          {
              LIGHT,
              POWER
          };      
          
          // function to print a sensor and it's bounding box
          auto draw_sensor = [&](int h1, int v1,
                                 const char *title,
                                 float temp,
                                 float hum=NAN)
          {
        
              it.filled_rectangle(h1, v1, 320, 90, Color::BLACK);
              it.filled_rectangle(h1+320, v1, 320, 90, light_grey);
              it.rectangle(h1+320, v1, 320, 90, Color::BLACK);
              it.rectangle(h1+321, v1+1, 318, 88, Color::BLACK);   
              it.print(h1+10, v1+45, id(text_font), Color::WHITE, TextAlign::CENTER_LEFT, title);
          
              if (isnan(temp))
              {
                 it.print(h1+480, v1+45,
                 id(text_font), Color::BLACK,
                 TextAlign::CENTER,
                 "--");
              }
              else if (isnan(hum))
              {
                   it.printf(h1+480, v1+45,
                        id(text_font), Color::BLACK,
                        TextAlign::CENTER,
                        "%.0f°C",
                        temp);
              }
              else
              {
                   it.printf(h1+480, v1+45,
                        id(text_font), Color::BLACK,
                        TextAlign::CENTER,
                        "%.0f°C  %.0f%%",
                        temp, hum);
              }          
          };
          // end function
      
      
          auto draw_state = [&](int h1, int v1,
                                const char *title,
                                bool is_on,
                                IconType type)
          {
              // Black title box
              it.filled_rectangle(h1, v1, 320, 90, Color::BLACK);
          
      
          
              // Title
              it.print(h1 + 15, v1 + 45,
                       id(text_font),
                       Color::WHITE,
                       TextAlign::CENTER_LEFT,
                       title);
          
              // Draw state indicator
              switch (type)
              {
                  case LIGHT:
                      it.image(h1 + 332, v1+2,
                               is_on ? id(light_on) : id(light_off));
                      break;   
                      
                  case POWER:
                      it.image(h1 + 325, v1,
                               is_on ? id(switch_on) : id(switch_off));
                      break;
              }
              // Icon/image box
              it.rectangle(h1 + 320, v1, 110, 90, Color::BLACK);
              it.rectangle(h1 + 321, v1 + 1, 108, 88, Color::BLACK);
              
          };     
          
          it.fill(Color::WHITE); // screen white
      
          it.rectangle(20, 20, it.get_width() - 40, it.get_height() - 40, Color::BLACK);  // thin border    
          it.line(20, 1230, it.get_width()-20, 1230, Color::BLACK);  // thin divider    
          it.printf(it.get_width() / 2, 1300, id(title_font), Color::BLACK, TextAlign::CENTER, "Home Assistant ePaper Dashboard"); // title
      
          // WiFi indicator top left
          it.filled_rectangle(30, 30, 130, 40, Color::BLACK);
          it.filled_rectangle(115, 30, 2, 40, Color::WHITE);
          it.printf(35, 30, id(small_font),Color::WHITE, "WiFi");                  
          it.printf(120, 33, id(icon_font),Color::WHITE, "%s", id(wifi_connection) ? "\U000F05A9" : "\U000F05AA");                  
      
          // timestamp top right
          char str[22];
          time_t currTime = id(homeassistant_time).now().timestamp;
          it.filled_rectangle(1280, 30, 560, 60, Color::BLACK);
          strftime(str, sizeof(str), "%a %H:%M %d/%m/%Y", localtime(&currTime));
          it.printf(1280,30, id(text_font), Color::WHITE, " %s", str);
      
          draw_sensor(30,800,"UK outside", id(uk_outside_temperature).state, id(uk_outside_humidity).state);
          draw_sensor(30,900,"Outside", id(outside_temperature).state, id(outside_humidity).state);
          draw_sensor(30,1000,"Office", id(office_temperature).state, id(office_humidity).state);
          draw_sensor(30,1100,"Livingroom", id(livingroom_temperature).state, id(livingroom_humidity).state);
      
      
          draw_state(${pool_pump_x},${pool_pump_y},"Pool pump",id(pool_pump).state,POWER);      
          draw_state(${office_lights_x},${office_lights_y},"Office lights",id(office_lights).state,LIGHT); 
          
          it.image(40, 1260, id(ha_logo));     
          it.image(it.get_width() - 140, 1260, id(ha_logo));   

    
font:
  - file: "gfonts://Roboto"
    id: title_font
    size: 96
  - file: "gfonts://Roboto"
    id: text_font
    size: 52
  - file: "gfonts://Roboto"
    id: small_font
    size: 36
    
  - file: "fonts/materialdesignicons-webfont.ttf"
    id: icon_font
    size: 36
    glyphs: [
      "\U000F05A9", #wifi
      "\U000F05AA", #no wifi
      "\U000F050F", # Thermometer
      "\U000F058E", # Humidity
      "\U000F1AAF", # heating coil
      "\U000F059D", # Wind speed
      "\U000F0D60", # Atmospheric pressure
      "\U000F0590", # Cloudy weather
      "\U000F0596", # Rainy weather
      "\U000F0598", # Snowy weather
      "\U000F0599", # Sunny weather
      "\U000F078F", # light bulb on
      "\U000F171E", # light bulb off
      "\U000F0F92", # boiler on
      "\U000F0433", # boiler off
      "\U000F07E4", # CO2

      ]

  - file: "fonts/materialdesignicons-webfont.ttf"
    id: large_icon_font
    size: 72
    glyphs: [
      "\U000F05A9", #wifi
      "\U000F05AA", #no wifi
      "\U000F050F", # Thermometer
      "\U000F058E", # Humidity
      "\U000F1AAF", # heating coil
      "\U000F059D", # Wind speed
      "\U000F0D60", # Atmospheric pressure
      "\U000F0590", # Cloudy weather
      "\U000F0596", # Rainy weather
      "\U000F0598", # Snowy weather
      "\U000F0599", # Sunny weather
      "\U000F078F", # light bulb on
      "\U000F171E", # light bulb off
      "\U000F0F92", # boiler on
      "\U000F0433", # boiler off
      "\U000F07E4", # CO2
      ]

Having gotten multiple pages working I DID spend some time with a full size JPG image and the online image function to pull out an image of palm trees from my Home Assistant www/images folder – but then I took it out – honestly the photo quality isn’t stunning and as for the overhead in screen refresh time – I think I’ll stick to instrumentation. Next stop the weather forecast with icons…

Ok, you may love this.. I took a photo of my E1003 screen just to show you the latest minor improvements (not in the blog code yet) and as usual the photo was AWFUL thanks to my office lighting and lots of reflections from the ceiling, me and a corner lamp not to mention my untidy desk. I fed it to ChatGPT and… this is perfect… and accurate… This is pretty much EXACTLY what you’d see on the paper-white ePaper display. I’ve added a battery level indicator – we discovered that the LED you see referred to on the SEEED site doesnt seem to be brought to the outside world(I’ll ask them) – and the GPIO port attached to charging doesn’t seem to be readable – so I’m simply inferring from the battery voltage changes whether it is charging or not. See the image below this full screen… up arrow for charge – down arrow for discharge… code for this latest version is on Github.

So this design isn’t going to win any awards for battery longevity. I was working on it last night, and this afternoon the battery is down just over 60%, but at least you get a full 24 hours out of it. I guess you could give it a charge overnight on cheap rate (assuming you have overnight electricity cheap rate). I’ve yet to find out how long it takes to fully charge, but let’s see: a couple of hours, put it on a cheap rate at night, and then you can have it sitting without charging during the day, no problem… pretty much like a phone really. The code is in one file on github.. You’ll need your own secrets file for WiFi and password.. and a couple of widely available fonts from the web.

2 thoughts on “Seeed Studio reTerminal E1003 Paper White Display

  1. Hello Pete, I did some similar experimental play with a display based on the same tyoe of processor(ESP32-S3). I created a display to be used for displaying some useful information from my summer house. To be able to achieve this in a simpe way, I have a Tailscale network and just added it to it. Very cool I must admit, now that there is a ESPHome module that makes it so easy! Basically you could have one of yours white paper panels in each of your locations; one in UK that provides info about your house in Spain and vice versa

    Here is a link to the ESPHome module:
    https://github.com/Csontikka/esphome-tailscale

    Here is a link describing my experimental work:
    https://discourse.nodered.org/t/remote-sensors-via-tailscale/101543

    The image is from my display, in Swedish, but you get the idea

    Best regards, Walter

    PS Hope you are in a safe place considering those terrible wild fires

    1. Thanks Walter – my entire setup used Tailscale – on the routers in Spain and the UK, so I have one Home Assistant here in Spain and it talks to the devices in the UK via the WIFI etc thanks to tailscale on the two routers (and of course on my phone so I can access the lot while away from home).

      Safe place, yes, relatively though last weekend a huge area of dying bushes etc set fire – too close for comfort..thankfully by the time the fire services arrived, all the neighbours had clubbed together to put the fire out with hoses and endless buckets of water. WAY too close for comfort.

      I’m sure others who are not up to speed on Tailscale will enjoy your links – thanks a lot.

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave the field below empty!

Are you human? Please solve:Captcha


The maximum upload file size: 512 MB. You can upload: image, audio, video, document, spreadsheet, interactive, text, archive, code, other. Links to YouTube, Facebook, Twitter and other services inserted in the comment text will be automatically embedded. Drop file here