How can I go sleep as soon as the e-ink display is updated?

Hello.
Thanks to the help of this forum, I have been able to configure a e-ink device from lilygo.
It is battery powered, so I try to make it as efficient as possible. The way I do it is using lambdas, and as soon as everything has been updated it goes to sleep for a minute.

This mostly works, and the device has been running for about 2 weeks now. However, I’m getting a lot of error logs about the screen not updating properly, and that is noticeable in the screen state. Sometimes it looks correct after an update, sometimes it shows artifacts (partially erased numbers, or extra black areas).
I suspect that my code to detect when the screen has completed the udpate is not correct, so if someone can help me figure out why or fix it that will be awesome.

Here is the device configuration (esphome):

# https://gist.github.com/dedalodaelus/159b73a036c250267d0dbbf496eb96f6
substitutions:
  devicename: esphome-web-663194
  friendly_name: E-ink esphome
  sleep_time: 2min
  wifi_signal_update: 60s
  uptime_update: 60s


globals:
  - id: uptime_saved
    type: time_t
    initial_value: "0"
    restore_value: yes
  - id: ts_saved
    type: time_t
    initial_value: "0"
    restore_value: yes
  - id: wifi_signal_sent
    type: bool
    initial_value: "false"
    restore_value: no
  - id: uptime_sent
    type: bool
    initial_value: "false"
    restore_value: no
  - id: tsync_read
    type: bool
    initial_value: "false"
    

esphome:
  name: $devicename
  on_boot:
    then:
      - deep_sleep.prevent: deep_sleep_control
  on_shutdown:
    then:
      - lambda: |-
         id(ts_saved) = id(homeassistant_time).now().timestamp;

esp32:
  board: esp32dev
  framework:
    type: arduino

logger:
api:
ota:

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: true
  power_save_mode: HIGH
  manual_ip:
    static_ip: 192.168.0.40
    gateway: 192.168.0.1
    subnet: 255.255.255.0

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Esphome-Web-663194"
    password: "blablabla"

captive_portal:

deep_sleep:
  # run_duration: 2min # automatically optimized by script
  sleep_duration: $sleep_time
  id: deep_sleep_control
  esp32_ext1_wakeup:
    pins: GPIO39      #This is the same as button 1 on the 4.7" T5 board; they have button 2 tied to GPIO 34 and button 3 tied to GPIO 35, but GPIO 35 on this board is (I think) the battery voltage, so these are likely different.
    mode: ALL_LOW

spi:
  clk_pin: 18
  mosi_pin: 23

time:
  - platform: homeassistant
    id: homeassistant_time
    on_time_sync:
      then:
        - script.execute: calculate_uptime

binary_sensor:
  - platform: homeassistant
    id: prevent_deep_sleep
    name: $friendly_name Prevent Deep Sleep
    entity_id: input_boolean.e_ink_prevent_deep_sleep
    on_state:
      then:
        - script.execute: consider_deep_sleep

  - platform: status
    id: status_sensor
    name:  $friendly_name Status
    
text_sensor:
  - platform: template
    name: $friendly_name Uptime Human Readable
    id: uptime_eink_human
    icon: mdi:clock-start

sensor:
  - platform: wifi_signal
    name: $friendly_name WiFi Signal
    on_value:
        then:
          - lambda: |-
              id(wifi_signal_sent) = true;
    update_interval: $wifi_signal_update

  - platform: homeassistant
    id: office_temp
    entity_id: sensor.mijia_temperature
    on_value:
      script.execute: update_screen

  - platform: homeassistant
    id: salon_temp
    entity_id: sensor.mj_salon_temp
    on_value:
      script.execute: update_screen

  - platform: uptime
    name: Uptime Sensor
    id: uptime_sensor
    update_interval: $uptime_update
    filters:
      - lambda: |-
          if (
            !id(homeassistant_time).now().is_valid() 
            &&
            !id(tsync_read)
          ) {
            ESP_LOGD("uptime", "time-not-valid");
            return id(uptime_sensor).raw_state;
          }
          time_t time_now = id(homeassistant_time).now().timestamp;
          // ESP_LOGD("uptime", "prev ts %d", id(ts_saved));
          time_t uptime_delta = time_now-id(ts_saved);
          ESP_LOGD("uptime", "uptime delta: %d", uptime_delta);
          id(uptime_saved)+=uptime_delta;  
          if (id(uptime_saved)<0)
          {
            ESP_LOGD("uptime", "Something went wrong negative uptime");
            id(uptime_saved)=0;
          }
          id(ts_saved) = time_now;  
          // ESP_LOGD("uptime", "time now %d", time_now);
          id(uptime_sent) = true;
          return id(uptime_saved);
    
font:
  - file: 'slkscr.ttf'
    id: font1
    size: 16

  - file: 'BebasNeue-Regular.ttf'
    id: bigfont
    size: 20

  - file: 'arial.ttf'
    id: font3
    size: 16

display:
  - platform: waveshare_epaper
    id: eink_display
    # Remove the # in front of one of the model lines below
    model: 2.90inv2
    #model: 2.13in-ttgo
    #model: 2.13in-ttgo-b73
    cs_pin: 5
    dc_pin: 17
    busy_pin: 4
    reset_pin: 16
    update_interval: 1min
    # If everything works, try to set full_update_every to 30 or 60
    full_update_every: 5
    rotation: 90
    lambda: |-
         it.printf(0, 5, id(bigfont), TextAlign::TOP_LEFT, "Office");
         if (id(office_temp).has_state())
            it.printf(0, 25, id(font1), TextAlign::TOP_LEFT, "%.1fC", id(office_temp).state);
         else 
          ESP_LOGD("screen", "No data from HA for office_temp");
         it.strftime(200, 25, id(font1), TextAlign::TOP_RIGHT, "%H:%M", id(homeassistant_time).now());
         it.printf(240, 25, id(font1), TextAlign::TOP_RIGHT, "%s", id(uptime_eink_human).state.c_str());
         it.line(0, 40, 300, 40);
         it.printf(0, 45, id(bigfont), TextAlign::TOP_LEFT, "SALON");
         if (id(salon_temp).has_state())
            it.printf(140, 45, id(font1), TextAlign::TOP_RIGHT, "%.1f°C", id(salon_temp).state);
         id(consider_deep_sleep).execute();


script:
  - id: update_screen
    mode: restart
    then:
      - if:
          condition:
            lambda: |-
              return id(office_temp).has_state() && id(salon_temp).has_state();
          then:
            component.update: eink_display
  - id: calculate_uptime
    mode: queued
    then:
      - if:
          condition:
            time.has_time:
          then:
            - logger.log: "Saving timestamp"
            - lambda: |-
                time_t time_now = id(homeassistant_time).now().timestamp;
                // ESP_LOGD("uptime", "prev ts %d", id(ts_saved));
                time_t uptime_delta = time_now-id(ts_saved);
                ESP_LOGD("uptime", "uptime delta: %d", uptime_delta);
                id(ts_saved) = id(homeassistant_time).now().timestamp;
                id(tsync_read) = true;

  - id: consider_deep_sleep
    mode: queued
    then:
      - logger.log: "Considering Deep Sleep"
      - delay: 5s
      - if:
          condition:
            and:
              - wifi.connected
              - api.connected
              - binary_sensor.is_off: prevent_deep_sleep
              - lambda: |-
                  if (
                    id(uptime_sent) &&
                    id(wifi_signal_sent)
                  ) {
                    ESP_LOGD("sensors", "All sensors data sent");
                    return true;
                  }
                  return false;
          then:
            - logger.log: "Entering in Deep Sleep"
            - deep_sleep.enter: deep_sleep_control
          else:
            - logger.log: "Skipping Deep Sleep"
            - delay: 1s

You description sounds like an update to an yet not erased screen.
E-ink displays needs to be erased first by switching between all black and all white.

Yes, that is the problem. But how do I get notified when the screen has updated? As you can see, the lambda that updates the screen calls the sleep script as the last thing, so I don’t know what other event I can listen to.

my display will refresh every 20 minutes during daytime and not refresh between 12 and 7am and go into deep sleep. maybe this helps:

-> get all sensors from HA 
    #refresh
    on_value: # Actions to perform once data for the last sensor has been received
      then:
        - script.execute: all_data_received

script:
  - id: all_data_received
    then:
      #- component.update: battery
      #- component.update: batt
      - component.update: t5_display
      - script.execute: enter_sleep

  - id: enter_sleep
    then:
      - if:
          condition:
            lambda: |- 
              auto time = id(ntp).now();
              if (!time.is_valid()) { 
                return false;
              }
              return (time.hour < 6); 
          then:
            - logger.log: "It's nighttime, entering long sleep for ${night_sleep_time}"          
            - deep_sleep.enter: 
                id: deep_sleep_1 
                sleep_duration: ${night_sleep_time}
          else:
            - logger.log: "It's daytime, entering short sleep for ${sleep_time}"   
            #- deep_sleep.prevent: deep_sleep_1
            - deep_sleep.enter:
                id: deep_sleep_1 
                sleep_duration: ${sleep_time}

That looks very interesting.
Do you mind sharing the part where you gather all the sensors and calculate if everything is available too?

Here is the complete code (lots of copy paste from others :slight_smile: ) :

my battery usage seems quite good with these settings. have the display running on a 18650 battery now for a month and still working. for some reason i cannot read the battery level so i will just let it run until it dies.

3 Likes