ESPHome Oracle Code Card ePaper Display

October 20, 2020

I'm a big fan of ePaper displays - it's easier on the eyes compared to LED based displays and is great for low powered devices like Pebble Watches and Kindle eBook readers.

I'm using an Oracle Code Card badge I got at a conference. It has a 2.7 inch display and is based on the $29 badgy if you want to do this project yourself.

Finished Product:

codecard

The Setup:

You need to add a couple sensors to home assistant first. You can find the specific file here in @fredrike's github page. Add this file to your packages folder inside your config folder. (here: ~/config/packages). If you don't already have a packages folder, make sure to include this line under the homeassistant: header like so:

homeassistant:
  packages: !include_dir_named packages

Home Assistant Configuration:

the blank " " squares you see in the config are Material Design Icons. Sometimes the icons get lost in translation when copy/pasting them.

If you're unable to copy/paste the code below and retain the Material Design Icons, try copying/pasting from here:

https://github.com/fredrike/esphome-nodes/blob/master/homeassistant-config/package-display_node.yaml

If that doesn't work, most of them are commented, so you can copy/paste each individual one from here: Material Design Icons.

~/config/packages/display_node.yaml

sensor:
  - platform: moon
  - platform: template
    sensors:
      moon_tpl:
        entity_id: sensor.moon
        value_template: >-
          {% set map = {
            "new_moon": "󰽤",
            "waxing_crecent": "󰽧",
            "first_quarter": "󰽡",
            "waxing_gibbous": "󰽨",
            "full_moon": "󰽢",
            "waning_gibbous": "󰽦",
            "last_quarter": "󰽣",
            "waning_crescent": "󰽥"
          } %}
          {{- map[states('sensor.moon')] -}}
  - platform: template
    sensors:
      forecast_today:
        entity_id: sensor.time
        value_template: >-
          {% set weather = {
            "clear-day": "󰖙", 
            "clear-night": "󰖔", 
            "cloudy": "󰖐", 
            "rain": "󰖖", 
            "sleet": "󰙿", 
            "snow": "󰼶", 
            "wind": "󰖞", 
            "fog": "󰖑", 
            "partly-cloudy-day": "󰖕", 
            "partly-cloudy-night": "󰼱" 
          } %}
          {% set base = 'sensor.dark_sky_forecast_' %}

          {{- weather[states('{}icon_{}d'.format(base, 0))] -}};
          {{- states('{}apparent_temperature'.format(base)) -}}°;
          {{- states('{}precip_probability_{}d'.format(base, 0))|int -}}
  - platform: darksky #https://pastebin.com/iX7u4ZSg
    api_key: !secret darksky_api_key
    name: dark_sky_forecast
    scan_interval: '00:10'
    forecast:
      - 0
      - 1
      - 2
      - 3
      - 4
    monitored_conditions:
      - icon
      - temperature
      - apparent_temperature
      - temperature_high
      - temperature_low
      - precip_probability
      - precip_intensity
  - platform: template
    sensors:
      forecast:
        entity_id: sensor.time
        value_template: >-
          {% set dow = {
           0: "Mon",
           1: "Tue",
           2: "Wed",
           3: "Thu",
           4: "Fri",
           5: "Sat",
           6: "Sun"
          } %}

          {% set weather = {
            "clear-day": "mdi:weather-sunny",
            "clear-night": "mdi:weather-night",
            "cloudy": "mdi:weather-cloudy",
            "rain": "mdi:weather-pouring",
            "sleet": "mdi:weather-snowy-rainy",
            "snow": "mdi:weather-snowy",
            "wind": "mdi:weather-windy",
            "fog": "mdi:weather-fog",
            "partly-cloudy-day": "mdi:weather-partly-cloudy",
            "partly-cloudy-night": "mdi:weather-night-partly-cloudy"
          } %}
          {% set weather = {
            "clear-day": "󰖙", 
            "clear-night": "󰖔", 
            "cloudy": "󰖐", 
            "rain": "󰖖", 
            "sleet": "󰙿", 
            "snow": "󰼶", 
            "wind": "󰖞", 
            "fog": "󰖑", 
            "partly-cloudy-day": "󰖕", 
            "partly-cloudy-night": "󰼱" 
          } %}

          {% set base = 'sensor.dark_sky_forecast_' %}

          {%- for i in [ 1, 2, 3, 4] -%}
            {{ dow[(now().weekday() + i) % 7]  }};
            {{- weather[states('{}icon_{}d'.format(base, i))] -}};
            {{- states('{}daytime_high_temperature_{}d'.format(base, i))|int
            -}}°/
            {{- states('{}overnight_low_temperature_{}d'.format(base, i))|int -}}°;
            {{- states('{}precip_probability_{}d'.format(base, i))|int -}}%
            {%- if not loop.last %};{% endif %}
          {%- endfor %}

ESPHome Code:

If you're unable to copy/paste the code below and retain the Material Design Icons, try copying/pasting from here:

https://gist.github.com/michaellunzer/37f29462ee9a17f22978e672c59e440c

esphome:
  name: codecard3
  platform: ESP8266
  board: esp12e
  # on_boot:
  #     - output.turn_off: onboard_led

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

spi:
  clk_pin: GPIO14
  mosi_pin: GPIO13

# s= '@!"%()+,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz.'
# print([','.join(_) for _ in s])

font:
  - file: 'fonts/Google_Sans_Bold.ttf'
    id: clock_font
    size: 45
    glyphs:
      ['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', '0',
       '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
       'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
       'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f',
       'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
       'u', 'v', 'w', 'x', 'y', 'z','å', 'ä', 'ö', '/']
  - file: 'fonts/Google_Sans_Medium.ttf'
    id: temp_font
    size: 32
    glyphs:
      ['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', '0',
       '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
       'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
       'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f',
       'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
       'u', 'v', 'w', 'x', 'y', 'z','å', 'ä', 'ö', '/']
  - file: 'fonts/Google_Sans_Bold.ttf'
    id: status_font
    size: 18
    glyphs:
      ['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', '0',
       '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
       'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
       'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f',
       'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
       'u', 'v', 'w', 'x', 'y', 'z', 'å', 'ä', 'ö', '/']
  - file: 'fonts/Google_Sans_Medium.ttf'
    id: aqi_font
    size: 18
    glyphs:
      ['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', '0',
       '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
       'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
       'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f',
       'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
       'u', 'v', 'w', 'x', 'y', 'z', 'å', 'ä', 'ö', '/']
  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: icon_font
    size: 32
    glyphs: [
      '󰢙', # mdi-account-heart
      '󰔏', # mdi-thermometer
      '󱄷', # mdi-fire-hydrant
      "󰕊", # mdi-umbrella
      "󰽕", # mdi-home-thermometer-outline
      "󰽔", # mdi-home-thermometer
      "󰵃", # mdi-air-filter
      # Wifi
      '󰤯', # mdi-wifi-strength-outline
      '󰤟', # mdi-wifi-strength-1
      '󰤢', # mdi-wifi-strength-2
      '󰤥', # mdi-wifi-strength-3
      '󰤨', # mdi-wifi-strength-4
      # Weather
      "󰖙", # mdi-weather-sunny
      "󰖔", # mdi-weather-night
      "󰖐", # mdi-weather-cloudy
      "󰖖", # mdi-weather-pouring
      "󰙿", # mdi-weather-snowy-rainy
      "󰼶", # mdi-weather-snowy-heavy
      "󰖞", # mdi-weather-windy-variant
      "󰖑", # mdi-weather-fog
      "󰼱", # mdi-weather-night-partly-cloudy
      "󰖕", # mdi-weather-partly-cloudy
      # Moon
      "󰽤", # new_moon
      "󰽧", # waxing_crecent
      "󰽡", # first_quarter
      "󰽨", # waxing_gibbous
      "󰽢", # full_moon
      "󰽦", # waning_gibbous
      "󰽣", # last_quarter
      "󰽥", # waning_crescent
      # Sun
      "󰖛", # mdi-weather-sunset-down
      "󰖜", # mdi-weather-sunset-up
      ]
  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: weather_font
    size: 50
    glyphs: [
      '󰢙', # mdi-account-heart
      '󰔏', # mdi-thermometer
      '󱄷', # mdi-fire-hydrant
      "󰕊", # mdi-umbrella
      "󰽕", # mdi-home-thermometer-outline
      "󰽔", # mdi-home-thermometer
      "󰵃", # mdi-air-filter
      # Wifi
      '󰤯', # mdi-wifi-strength-outline
      '󰤟', # mdi-wifi-strength-1
      '󰤢', # mdi-wifi-strength-2
      '󰤥', # mdi-wifi-strength-3
      '󰤨', # mdi-wifi-strength-4
      # Weather
      "󰖙", # mdi-weather-sunny
      "󰖔", # mdi-weather-night
      "󰖐", # mdi-weather-cloudy
      "󰖖", # mdi-weather-pouring
      "󰙿", # mdi-weather-snowy-rainy
      "󰼶", # mdi-weather-snowy-heavy
      "󰖞", # mdi-weather-windy-variant
      "󰖑", # mdi-weather-fog
      "󰼱", # mdi-weather-night-partly-cloudy
      "󰖕", # mdi-weather-partly-cloudy
      # Moon
      "󰽤", # new_moon
      "󰽧", # waxing_crecent
      "󰽡", # first_quarter
      "󰽨", # waxing_gibbous
      "󰽢", # full_moon
      "󰽦", # waning_gibbous
      "󰽣", # last_quarter
      "󰽥", # waning_crescent
      # Sun
      "󰖛", # mdi-weather-sunset-down
      "󰖜", # mdi-weather-sunset-up
      ]
  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: current_weather_font
    size: 80
    glyphs: [
      '󰢙', # mdi-account-heart
      '󰔏', # mdi-thermometer
      '󱄷', # mdi-fire-hydrant
      "󰕊", # mdi-umbrella
      "󰽕", # mdi-home-thermometer-outline
      "󰽔", # mdi-home-thermometer
      "󰵃", # mdi-air-filter
      # Wifi
      '󰤯', # mdi-wifi-strength-outline
      '󰤟', # mdi-wifi-strength-1
      '󰤢', # mdi-wifi-strength-2
      '󰤥', # mdi-wifi-strength-3
      '󰤨', # mdi-wifi-strength-4
      # Weather
      "󰖙", # mdi-weather-sunny
      "󰖔", # mdi-weather-night
      "󰖐", # mdi-weather-cloudy
      "󰖖", # mdi-weather-pouring
      "󰙿", # mdi-weather-snowy-rainy
      "󰼶", # mdi-weather-snowy-heavy
      "󰖞", # mdi-weather-windy-variant
      "󰖑", # mdi-weather-fog
      "󰼱", # mdi-weather-night-partly-cloudy
      "󰖕", # mdi-weather-partly-cloudy
      # Moon
      "󰽤", # new_moon
      "󰽧", # waxing_crecent
      "󰽡", # first_quarter
      "󰽨", # waxing_gibbous
      "󰽢", # full_moon
      "󰽦", # waning_gibbous
      "󰽣", # last_quarter
      "󰽥", # waning_crescent
      # Sun
      "󰖛", # mdi-weather-sunset-down
      "󰖜", # mdi-weather-sunset-up
      ]

# 264 x 176 display
display:
  - platform: waveshare_epaper
    id: epaper
    cs_pin: GPIO2
    busy_pin: GPIO5
    reset_pin: GPIO4
    dc_pin: GPIO0
    model: 2.70in
    rotation: 270°
    # full_update_every: 30
    # 264 x 176 display
    update_interval: 3600s
    lambda: |
      int x, y;
      ESP_LOGI("display", "Updating..");

      /* Print time in HH:MM format */
      it.strftime(0, -10, id(clock_font), TextAlign::TOP_LEFT, "%I:%M", id(current_time).now());
      /* Print AM/PM */
      it.strftime(120, 15, id(status_font), TextAlign::TOP_LEFT, "%P", id(current_time).now());
      it.strftime(65, 40, id(status_font), TextAlign::TOP_LEFT, "%b %e, %Y", id(current_time).now());

      // it.line(259, -10, 259, 200);it.line(261, 0, 261, 200);it.line(260, 0, 260, 200);
      // it.line(0, 200, 259, 200);it.line(0, 201, 261, 201);it.line(0, 202, 260, 202);
      /*
      it.print(125, 120, id(temp_font), TextAlign::BASELINE_CENTER, "HELLO SUNSHINE!");
      it.print(125, 160, id(icon_font), TextAlign::BASELINE_CENTER, "󰢙");
      it.print(125, 182, id(temp_font), TextAlign::BASELINE_CENTER, "Lea & Emma"); // 400 - ((400 - 260)/2)
      */
      /* Moon icon - used to be y = 155, x = 230 */
      if(id(moon_icon).has_state()) {
        y = 165, x = 20;
        ESP_LOGI("Moon icon", "%s", id(moon_icon).state.c_str());
        it.printf(x, y, id(icon_font), TextAlign::BASELINE_CENTER, "%s", id(moon_icon).state.c_str());
      }
      /* WiFi Signal Strenght          x = 398, y = 298; */
      if(id(wifisignal).has_state()) {
        x = 175, y = 30;
        if (id(wifisignal).state >= -50) {
            //Excellent
            it.print(x, y, id(icon_font), TextAlign::BOTTOM_RIGHT, "󰤨");
            ESP_LOGI("WiFi", "Exellent");
        } else if (id(wifisignal).state  >= -60) {
            //Good
            it.print(x, y, id(icon_font), TextAlign::BOTTOM_RIGHT, "󰤥");
            ESP_LOGI("WiFi", "Good");
        } else if (id(wifisignal).state  >= -67) {
            //Fair
            it.print(x, y, id(icon_font), TextAlign::BOTTOM_RIGHT, "󰤢");
            ESP_LOGI("WiFi", "Fair");
        } else if (id(wifisignal).state  >= -70) {
            //Weak
            it.print(x, y, id(icon_font), TextAlign::BOTTOM_RIGHT, "󰤟");
            ESP_LOGI("WiFi", "Weak");
        } else {
            //Unlikely working signal
            it.print(x, y, id(icon_font), TextAlign::BOTTOM_RIGHT, "󰤯");
            ESP_LOGI("WiFi", "Unlikely");
        }
      }

      // Current weather
      if(id(weather_forecast_today).has_state()) {
        std::string str = id(weather_forecast_today).state;
        ESP_LOGI("Weather", "%s", str.c_str());
        std::size_t current, previous = 0;
        char delim = ';';
        current = str.find(delim);
        int y = 100, x = 5;
        for (int i=0; i<3; i++) {
            if(i == 0) {
              it.printf(x - 15, y, id(current_weather_font),
                TextAlign::BASELINE_LEFT,
                "%s", str.substr(previous, current - previous).c_str());
            } else if (i == 1) {
              ESP_LOGD("weather","%dx%d %s", (i % 1 ? 270 : 320), x,
                str.substr(previous, current - previous).c_str());
              /* it.printf(y, x, id(status_font),
                TextAlign::TOP_LEFT,
                "%s", str.substr(previous, current - previous).c_str());
              */
            } else if (i == 2) { // Chance for rain
              it.printf(x, y + 2, id(icon_font), TextAlign::TOP_LEFT, "󰕊"); // Umbrella
              it.printf(x + 30, y + 10, id(status_font),
                TextAlign::TOP_LEFT,
                "%s%%", str.substr(previous, current - previous).c_str());
            }
            // y += 100;
            previous = current + 1;
            current = str.find(delim, previous);
        }
      }

      // Print inside temperature (from homeassistant sensor)
      y = 135, x = 100;
      if (id(inside_temperature).has_state()) {
        it.print(x, y, id(icon_font), TextAlign::BOTTOM_RIGHT, "󰽕");
        it.printf(x, y + 4, id(temp_font), TextAlign::BOTTOM_LEFT , "%5.1f°", id(inside_temperature).state);
      }
      // Print outside temperature (from homeassistant sensor)
      if (id(outside_temperature).has_state()) {
        it.print(x, y + 4, id(icon_font), TextAlign::TOP_RIGHT, "󰽔"); 
        it.printf(x, y, id(temp_font), TextAlign::TOP_LEFT, "%5.1f°", id(outside_temperature).state);
      }

      // Print sniffer0 air quality index sensor (from homeassistant sensor)
      if (id(sniffer0_aqi).has_state()) {
         x = 91, y = 60;
        it.print(x + 10, y + 4, id(icon_font), TextAlign::TOP_RIGHT, "󰵃"); 
        it.printf(x - 4, y, id(temp_font), TextAlign::TOP_LEFT, "%5.0f", id(sniffer0_aqi).state);
        it.print(x + 87, y + 16, id(aqi_font), TextAlign::TOP_RIGHT, "AQI"); 

      }

      /*// Print consumer
      if (id(top_consumer).has_state()) {
        it.printf(0, 295, id(status_font), TextAlign::BOTTOM_LEFT , "Total: %.0f W, %s W", id(sparsnas).state, id(top_consumer).state.c_str());
      }
      */

      it.line(180, 0, 180, 176);
      it.line(0, 103, 180, 103);

      // std::string str = "mon:cloud:12.1:30%:tue:rain:10.2:40%:wed:sunny:18.0:0%:thu:snow:-12.2:10%";
      // Mon;;65°/53°;12%;Tue;;70°/56°;7%;Wed;;73°/55°;2%;Thu;;73°/55°;1%
      if(id(weather_forecast).has_state()) {
        std::string str = id(weather_forecast).state;
        ESP_LOGI("Weather", "%s", str.c_str());
        std::size_t current, previous = 0;
        char delim = ';';
        current = str.find(delim);
        int x = 240, y = 0;
        // changed from x = 340, y = 4
        for (int i=0; i<4; i++) {
          ESP_LOGI("t", "%d", i);
          for (int j=0; j<4; j++) {
            if(j == 1) {  // Weather icon.
              it.printf(x - 65, y + 25, id(weather_font),
                TextAlign::BASELINE_LEFT,
                "%s", str.substr(previous, current - previous).c_str());
            } else {
              ESP_LOGD("weather","%dx%d %s", (j % 1 ? 270 : 320), x,
                str.substr(previous, current - previous).c_str());
              it.printf((j == 0 ? x - 15 : 390), y,
                id(status_font),
                (j == 0 ? TextAlign::TOP_LEFT : TextAlign::TOP_RIGHT),
                "%s", str.substr(previous, current - previous).c_str());
              y += 16;
            }
            previous = current + 1;
            current = str.find(delim, previous);
          }
          y += 2;
        }
      }

# output:
#   - platform: gpio
#     id: onboard_led
#     pin:
#       number: 2
#       inverted: False

# light:
#   - platform: binary
#     name: "Onboard LED"
#     output: onboard_led

time:
  - platform: homeassistant
    id: current_time
    timezone: America/Los_Angeles
    on_time:
      # Every 1 minutes
      - seconds: 0
        then:
          - component.update: epaper

sensor:
  - platform: homeassistant
    id: inside_temperature
    entity_id: sensor.sn1_temperature
    internal: true

  - platform: homeassistant
    id: sniffer0_aqi
    entity_id: sensor.sniffer0_pm_2_5_aqi
    internal: true

  - platform: homeassistant
    id: outside_temperature
    entity_id: sensor.openweathermap_forecast_temperature
    internal: true

  # - platform: homeassistant
  #   entity_id: sensor.sparsnas_energy_consumption_momentary
  #   id: sparsnas
  #   internal: true

  - platform: homeassistant
    entity_id: sensor.dark_sky_precip_probability
    id: precip_probability
    internal: true

  - platform: wifi_signal
    name: "WiFi Signal Sensor"
    id: wifisignal
    update_interval: 60s

text_sensor:
  # - platform: homeassistant
  #   id: top_consumer
  #   entity_id: sensor.iotawatt_top_consumer
  #   internal: true

  - platform: homeassistant
    name: forecast
    id: weather_forecast
    entity_id: sensor.forecast
    internal: true

  - platform: homeassistant
    name: forecast
    id: weather_forecast_today
    entity_id: sensor.forecast_today
    internal: true

  - platform: homeassistant
    id: moon_icon
    entity_id: sensor.moon_tpl

This was based heavily on @fredrike's code and so there are still some vestigal structures of his code commented out in my code. @Fredrike, if you see this, thanks for your inspiration!

Future hardware:

I realize that I'm pretty lucky to get ahold of a limited run piece of hardware specially made for a conference. I was hoping that I was watching YouTube while writing this post and saw a new product from Adafruit that this code could potentially work on in the future.

Also, the badgy looks like a great alternative.

Display Iterations

codecard_hello_world

IMG 4021

codecard_version2

codecard_version3

codecard_version5

codecard_version4

codecard

Share: