Air Quality Sensors + E-Ink Display using ESPHome

*** I’VE UPDATED MY POST WITH MY LATEST CODE ***
I’m no more using DarkSky as it will be removed soon.

Had to show my latest project as asked here: E-paper display

Parts are (total is about 50€): a 4.2" E-Ink display from Waveshare, a Lolin D32, a SDS011 (Air Quality), a BME280 (Temp, Humidity and Pressure), a buzzer and a LED (for notification):

Software used is ESPHome (what a great project!)
Nearly no soldering (only BME280 if I remember correctly)! This ESP32 has all the GPIO + all 3V3 and 5V needed.

My goal was to make a clone of the Xiaomi Mi Clear Grass Air Detector and to replace my Oregon weather station but where I am able to display what I want. I still need to make a case :slight_smile:

Here is my code
substitutions:
  devicename: AQI Display
  gpio_led_status: GPIO5
  gpio_i2c_sda: GPIO21
  gpio_i2c_scl: GPIO22
  gpio_uart_rx_pin: GPIO14
  gpio_uart_tx_pin: GPIO13
  gpio_spi_clk_pin: GPIO25
  gpio_spi_mosi_pin: GPIO26
  gpio_cs_pin: GPIO32
  gpio_busy_pin: GPIO33
  gpio_reset_pin: GPIO27
  gpio_dc_pin: GPIO17
  gpio_buzzer: GPIO04
  gpio_led_red: GPIO19
  gpio_led_green: GPIO23
  gpio_led_blue: GPIO18

esphome:
  name: aqi-display
  comment: "Air Quality Display"
  platform: ESP32
  board: lolin_d32

status_led:
  pin: 
   number: $gpio_led_status
   inverted: True

i2c:
  sda: $gpio_i2c_sda
  scl: $gpio_i2c_scl
  scan: False
  id: bme_280

uart:
  rx_pin: $gpio_uart_rx_pin
  tx_pin: $gpio_uart_tx_pin
  baud_rate: 9600
  id: sds_011

spi:
  clk_pin: $gpio_spi_clk_pin
  mosi_pin: $gpio_spi_mosi_pin
  id: epaper_display

binary_sensor:
  - platform: status
    name: "$devicename Status"
  
  - platform: homeassistant
    name: "Weather Alert"
    entity_id: binary_sensor.alerte_meteo
    id: weather_alert
    internal: true

  - platform: homeassistant
    name: "Out Temp Rising"
    entity_id: binary_sensor.jardin_temp_rising
    id: out_temp_rising
    internal: true

  - platform: homeassistant
    name: "Out Temp Failing"
    entity_id: binary_sensor.jardin_temp_falling
    id: out_temp_failing
    internal: true

sensor:
  - platform: uptime
    name: "$devicename Uptime Sec"
    id: uptime_sec
    internal: true

  - platform: wifi_signal
    name: "$devicename WiFi Signal"
    update_interval: 120s

  - platform: template
    id: uptime_timestamp
    name: "$devicename Uptime"
    device_class: "timestamp"
    accuracy_decimals: 0
    update_interval: never
    lambda: |-
      static float timestamp = (
        id(current_time).utcnow().timestamp - id(uptime_sec).state
      );
      return timestamp;

  - platform: bme280
    i2c_id: bme_280
    temperature:
      name: "$devicename Temperature"
      id: bme280_temp
      filters:
        - sliding_window_moving_average:
            window_size: 5
            send_every: 5
    humidity:
      name: "$devicename Humidity"
      id: bme280_hum
      filters:
        - sliding_window_moving_average:
            window_size: 5
            send_every: 5
    pressure:
      name: "$devicename Pressure"
      id: bme280_pressure
      filters:
        - sliding_window_moving_average:
            window_size: 5
            send_every: 5
        - lambda: |-
            const float STANDARD_ALTITUDE = 160; // in meters
            return x / powf(1 - ((0.0065 * STANDARD_ALTITUDE) /
                   (id(bme280_temp).state + (0.0065 * STANDARD_ALTITUDE) + 273.15)), 5.257); // in hPa
    address: 0x76
    update_interval: 60s

  - platform: sds011
    pm_2_5:
      name: "$devicename PM <2.5µm Concentration"
      id: pm_2_5
    pm_10_0:
      name: "$devicename PM <10.0µm Concentration"
      id: pm_10
    update_interval: 10min

  - platform: homeassistant
    name: "1st floor Temp. from HA"
    entity_id: sensor.rdc_temperature
    unit_of_measurement: "°"
    accuracy_decimals: 1
    id: average_1st_temp
    internal: true

  - platform: homeassistant
    name: "1st floor Hum. from HA"
    entity_id: sensor.rdc_humidity
    unit_of_measurement: "%"
    accuracy_decimals: 0
    id: average_1st_hum
    internal: true

  - platform: homeassistant
    name: "2nd floor Temp. from HA"
    entity_id: sensor.1er_temperature
    unit_of_measurement: "°"
    accuracy_decimals: 1
    id: average_2nd_temp
    internal: true

  - platform: homeassistant
    name: "2nd floor Hum. from HA"
    entity_id: sensor.1er_humidity
    unit_of_measurement: "%"
    accuracy_decimals: 0
    id: average_2nd_hum
    internal: true

  - platform: homeassistant
    name: "Outdoor Temp. from HA"
    entity_id: sensor.jardin_th_temperature
    unit_of_measurement: "°"
    accuracy_decimals: 1
    id: outdoor_temp
    internal: true

  - platform: homeassistant
    name: "Outdoor Hum. from HA"
    entity_id: sensor.jardin_th_humidity
    unit_of_measurement: "%"
    accuracy_decimals: 0
    id: outdoor_hum
    internal: true

  - platform: template
    name: "$devicename Air Quality Index"
    lambda: |-
      float value_temp = id(bme280_temp).state;
      float index_temp = 5;
      if (value_temp <= 14 || value_temp >= 25) {
          index_temp = 1;
      } else if (value_temp <= 15 || value_temp >= 24) {
          index_temp = 2;
      } else if (value_temp <= 16 || value_temp >= 23) {
          index_temp = 3;
      } else if (value_temp < 18 || value_temp > 21) {
          index_temp = 4;
      }

      float value_hum = id(bme280_hum).state;
      float index_hum = 5;
      if (value_hum < 10 || value_hum > 90) {
          index_hum = 1;
      } else if (value_hum < 20 || value_hum > 80) {
          index_hum = 2;
      } else if (value_hum < 30 || value_hum > 70) {
          index_hum = 3;
      } else if (value_hum < 40 || value_hum > 60) {
          index_hum = 4;
      }
      
      float value_pm2_5 = id(pm_2_5).state;
      float index_pm2_5 = 5;
      if (value_pm2_5 > 64) {
          index_pm2_5 = 1;
      } else if (value_pm2_5 > 53) {
          index_pm2_5 = 2;
      } else if (value_pm2_5 > 41) {
          index_pm2_5 = 3;
      } else if (value_pm2_5 > 23) {
          index_pm2_5 = 4;
      }
      
      float value_pm10 = id(pm_10).state;
      float index_pm10 = 5;
      if (value_pm10 > 64) {
          index_pm10 = 1;
      } else if (value_pm10 > 53) {
          index_pm10 = 2;
      } else if (value_pm10 > 41) {
          index_pm10 = 3;
      } else if (value_pm10 > 23) {
          index_pm10 = 4;
      }
      return ((index_temp + index_hum + index_pm2_5 + index_pm10) * 13) / 4;
    unit_of_measurement: "%"
    icon: "mdi:air-filter"
    update_interval: 60s
    id: aqi_index
    on_value_range:
      - below: 38.0
        then:
          - light.turn_on: 
              id: led
              brightness: 100%
              red: 100%
              green: 0
              blue: 0
      - above: 39
        then: 
          - light.turn_off:
              id: led

switch:
  - platform: restart
    name: "$devicename Restart"

  - platform: gpio
    pin: $gpio_buzzer
    name: "$devicename Buzzer"
    icon: "mdi:volume-high"
    id: buzzer

text_sensor:
  - platform: version
    name: "$devicename Version"
    hide_timestamp: true

  - platform: wifi_info
    ip_address:
      name: "$devicename IPv4"
      icon: "mdi:server-network"
    ssid:
      name: "$devicename Connected SSID"
      icon: "mdi:wifi"

  - platform: homeassistant
    name: "Today Weather Forecast"
    entity_id: sensor.weather_forecast_0
    id: forcast
    internal: true

  - platform: homeassistant
    name: "Today Weather Icon"
    entity_id: sensor.weather_condition_0
    id: weather_icon
    internal: true

  - platform: homeassistant
    name: "Tomorrow Weather Forecast"
    entity_id: sensor.weather_forecast_1
    id: forcast_1
    internal: true

  - platform: homeassistant
    name: "Tomorrow Weather Icon"
    entity_id: sensor.weather_condition_0
    id: weather_icon_1
    internal: true

  - platform: template
    name: "$devicename Air Quality Level"
    lambda: |-
      if (id(aqi_index).state <= 25) {
          return {"Très Mauvais"};  // INADEQUATE
      } else if (id(aqi_index).state <= 38) {
          return {"Mauvais"};       // POOR
      } else if (id(aqi_index).state <= 51) {
          return {"Moyen"};         // FAIR
      } else if (id(aqi_index).state <= 60) {
          return {"Bon"};           // GOOD
      } else {
          return {"Très Bon"};      // EXCELLENT
      }
    icon: "mdi:air-filter"
    update_interval: 60s
    id: aqi_level

time:
  - platform: homeassistant
    timezone: Europe/Paris
    id: current_time
    on_time_sync:
      - component.update: uptime_timestamp

output:
  - platform: ledc
    pin: $gpio_led_red
    id: redgpio

  - platform: ledc
    pin: $gpio_led_green
    id: greengpio

  - platform: ledc
    pin: $gpio_led_blue
    id: bluegpio

light:
  - platform: rgb
    name: "$devicename LED"
    red: redgpio
    green: greengpio
    blue: bluegpio
    id: led

display:
  - platform: waveshare_epaper
    id: epaper
    cs_pin: $gpio_cs_pin
    busy_pin: $gpio_busy_pin
    reset_pin: $gpio_reset_pin
    dc_pin: $gpio_dc_pin
    model: 4.20in # 300x400
    rotation: 270°
    update_interval: 60s
    lambda: |-
      ESP_LOGI("display", "Updating...");
      
      // OUTSIDE
      it.printf(7, 15, id(font_medium_20), TextAlign::BASELINE_LEFT, "Dehors");
      it.line(78, 14, 293, 14);
      
      it.printf(10, 84, id(icon_font_35), TextAlign::BASELINE_LEFT, "\U000F058E"); 
      if (id(outdoor_hum).has_state()) {
          it.printf(41, 82, id(font_regular_45), TextAlign::BASELINE_LEFT, "%2.0f", id(outdoor_hum).state);
          it.printf(95, 82, id(font_regular_30), TextAlign::BASELINE_LEFT, "%%");
      }
      it.printf(120, 80, id(icon_font_40), TextAlign::BASELINE_LEFT, "\U000F050F"); 
      if (id(outdoor_temp).has_state()) {
          it.printf(220, 82, id(font_regular_65), TextAlign::BASELINE_CENTER, "%.1f°", id(outdoor_temp).state);
      }
      
      if (id(out_temp_rising).has_state() && id(out_temp_failing).has_state()) {
          if (id(out_temp_rising).state == true) {
              it.printf(296, 82, id(icon_font_20), TextAlign::BASELINE_RIGHT, "\U000F005C"); 
          }
          if (id(out_temp_failing).state == true) {
              it.printf(296, 82, id(icon_font_20), TextAlign::BASELINE_RIGHT, "\U000F0043"); 
          }
      }
      
      // INSIDE
      it.printf(7, 110, id(font_medium_20), TextAlign::BASELINE_LEFT, "Dedans");
      it.line(81, 109, 293, 109);

      // Floor 1
      it.printf(18, 162, id(icon_font_30), TextAlign::BASELINE_LEFT, "\U000F0D80"); 
      
      it.printf(60, 161, id(icon_font_25), TextAlign::BASELINE_LEFT, "\U000F058E"); 
      if (id(average_2nd_hum).has_state()) {
          it.printf(125, 160, id(font_regular_35), TextAlign::BASELINE_RIGHT, "%.0f", id(average_2nd_hum).state);
          it.printf(128, 160, id(font_regular_30), TextAlign::BASELINE_LEFT, "%%");
      }
      it.printf(160, 159, id(icon_font_30), TextAlign::BASELINE_LEFT, "\U000F050F"); 
      if (id(average_2nd_temp).has_state()) {
          it.printf(290, 160, id(font_regular_45), TextAlign::BASELINE_RIGHT, "%.1f°", id(average_2nd_temp).state);
      }

      // Floor 0
      it.printf(18, 212, id(icon_font_30), TextAlign::BASELINE_LEFT, "\U000F0DD2"); 
      
      it.printf(60, 211, id(icon_font_25), TextAlign::BASELINE_LEFT, "\U000F058E"); 
      if (id(average_1st_hum).has_state()) {
          it.printf(125, 210, id(font_regular_35), TextAlign::BASELINE_RIGHT, "%.0f", id(average_1st_hum).state);
          it.printf(128, 210, id(font_regular_30), TextAlign::BASELINE_LEFT, "%%");
      }
      it.printf(160, 209, id(icon_font_30), TextAlign::BASELINE_LEFT, "\U000F050F"); 
      if (id(average_1st_temp).has_state()) {
          it.printf(290, 210, id(font_regular_45), TextAlign::BASELINE_RIGHT, "%.1f°", id(average_1st_temp).state);
      }
      
      // AQI
      it.printf(20, 255, id(icon_font_25), TextAlign::BASELINE_LEFT, "\U000F0D43"); 
      
      it.printf(68, 255, id(font_regular_30), TextAlign::BASELINE_LEFT, "%s", id(aqi_level).state.c_str());
      
      // WEATHER
      it.printf(7, 285, id(font_medium_20), TextAlign::BASELINE_LEFT, "Météo");
      it.line(70, 284, 293, 284);

      int time = id(current_time).now().hour * 100 + id(current_time).now().minute;

      if (id(weather_icon).has_state()) {
          std::map<std::string, std::string> weather_state { 
            { "clear-night", "\U000F0594" },
            { "cloudy", "\U000F0590" },
            { "exceptional", "\U000F05D6" },
            { "fog", "\U000F0591" },                
            { "hail", "\U000F0592" },               
            { "lightning", "\U000F0593" },              
            { "lightning-rainy", "\U000F067E" },                
            { "partlycloudy", "\U000F0595"},                 
            { "pouring", "\U000F0596" },  
            { "rainy", "\U000F0597" },
            { "snowy", "\U000F0598" },
            { "snowy-rainy", "\U000F067F" },
            { "sunny", "\U000F0599" },
            { "windy", "\U000F059D" },
            { "windy-variant", "\U000F059E" }
          };

          if (time < 1900) {
              it.printf(20, 320, id(icon_font_25), TextAlign::BASELINE_LEFT, weather_state[id(weather_icon).state.c_str()].c_str());
          } else {
              it.printf(20, 320, id(icon_font_25), TextAlign::BASELINE_LEFT, weather_state[id(weather_icon_1).state.c_str()].c_str());
          }
      }

      if (id(weather_alert).has_state() && id(weather_alert).state) {
          // mdi:alert-outline
          it.printf(50, 320, id(icon_font_25), TextAlign::BASELINE_LEFT, "\U000F002A");
      }

      if (id(forcast).has_state() && time < 1900) {
          it.printf(290, 321, id(font_regular_30), TextAlign::BASELINE_RIGHT, "%s", id(forcast).state.c_str());
      } 
      else if (id(forcast_1).has_state() && time >= 1900) {
          it.printf(290, 321, id(font_regular_30), TextAlign::BASELINE_RIGHT, "%s", id(forcast_1).state.c_str());
      }

      // TIME
      it.line(7, 337, 293, 337);
      it.strftime(7, 363, id(font_medium_20), TextAlign::BASELINE_LEFT, "%A", id(current_time).now());
      it.strftime(7, 393, id(font_medium_20), TextAlign::BASELINE_LEFT, "%d %b. %y", id(current_time).now());
      it.strftime(290, 393, id(font_regular_65), TextAlign::BASELINE_RIGHT, "%H:%M", id(current_time).now());

font:
  - file: 'fonts/Kanit-Medium.ttf'
    id: font_medium_20
    size: 20
    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/Kanit-Regular.ttf'
    id: font_regular_30
    size: 30
    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/Kanit-Regular.ttf'
    id: font_regular_35
    size: 35
    glyphs: 
      ['!', ',', '.', '"', '%', '-', '_', ':', '°', '/',
       '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ']

  - file: 'fonts/Kanit-Regular.ttf'
    id: font_regular_45
    size: 45
    glyphs: 
      ['!', ',', '.', '"', '%', '-', '_', ':', '°', '/',
       '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ']

  - file: 'fonts/Kanit-Regular.ttf'
    id: font_regular_65
    size: 65
    glyphs: 
      ['!', ',', '.', '"', '%', '-', '_', ':', '°', '/',
       '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ']

  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: icon_font_20
    size: 20
    glyphs: [
      "\U000F0043", # mdi-arrow-bottom-right
      "\U000F005C"  # mdi-arrow-top-right
      ]

  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: icon_font_25
    size: 25
    glyphs: [
      "\U000F058E", # mdi-water-percent
      "\U000F0D43", # mdi-air-filter
      "\U000F002A", # mdi-alert-outline
      "\U000F0594", # mdi-weather-night - clear-night
      "\U000F0590", # mdi-weather-cloudy
      "\U000F05D6", # mdi-alert-circle-outline - exeptionnal
      "\U000F0591", # mdi-weather-fog
      "\U000F0592", # mdi-weather-hail
      "\U000F0593", # mdi-weather-lightning
      "\U000F067E", # mdi-weather-lightning-rainy
      "\U000F0595", # mdi-weather-partly-cloudy
      "\U000F0596", # mdi-weather-pouring
      "\U000F0597", # mdi-weather-rainy
      "\U000F0598", # mdi-weather-snowy
      "\U000F067F", # mdi-weather-snowy-rainy
      "\U000F0599", # mdi-weather-sunny
      "\U000F059D", # mdi-weather-windy
      "\U000F059E"  # mdi-weather-windy-variant
      ]

  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: icon_font_30
    size: 30
    glyphs: [
      "\U000F050F", # mdi-thermometer
      "\U000F0D80", # mdi-home-floor-1
      "\U000F0DD2"  # mdi-home-floor-0
      ]

  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: icon_font_35
    size: 35
    glyphs: ["\U000F058E"]  # mdi-water-percent

  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: icon_font_40
    size: 40
    glyphs: ["\U000F050F"] # mdi-thermometer

wifi:
  networks:
  - ssid: !secret ssid
    password: !secret password
  - ssid: !secret ssid_live
    password: !secret password_live
  ap:
    ssid: "$devicename Fallback Hotspot"
    password: !secret api_ota_pwd

captive_portal:

web_server:
  port: 80
  auth:
    username: admin
    password: !secret api_ota_pwd

logger:

api:
  password: !secret api_ota_pwd

ota:
  password: !secret api_ota_pwd

Weather infos are from MétéoFrance integration but it should work with all weather integrations (except DarkSky as it has not been updated). Before 7PM, it display weather forecast for the day, after 7PM it display forecast for tomorrow.

AQI calculation is done directly on the ESP and is based on code from @Limych: Indoor Air Quality Sensor Component (Thanks!)

26 Likes

Looks awesome. What kind of case do you have planned?

My goal is to make a case like the Xiaomi Mi Clear Glass Air Detector:
Original-Xiaomi-Mi-Clear-Grass-Intelligent-Air-Detector-From-Xiaomi-Youpin-Multi-Mode-Compensation-Indoor-Outdoor.jpg_q50
At the beginning I planned to buy a 3D printer and print a case but I’m running out of time to learn how to use FreeCAD… I may end with a laser cut wood case.

Can you please share the aliexpress link for the display? Cheers.

Found this one as the one I bought is not available anymore on Ali

It’s a 4.2" Waveshare 400x300. Full support from ESPHome

1 Like

Hello I think there is a problem with the copy/paste but what is the symbol 
Thnaks

If you want to see what icon it is, check for it’s « mdi » name on https://materialdesignicons.com/
Then find it in the font and copy/past it to your code.

Hi,
I try to connect the same paper with esphome without success.
The screen have no react.
My first try :

...
substitutions:
  devicename: AQI Display
  gpio_led_status: GPIO5
  gpio_i2c_sda: GPIO21
  gpio_i2c_scl: GPIO22
  gpio_uart_rx_pin: GPIO14
  gpio_uart_tx_pin: GPIO13
  gpio_spi_clk_pin: GPIO25
  gpio_spi_mosi_pin: GPIO26
  gpio_cs_pin: GPIO32
  gpio_busy_pin: GPIO33
  gpio_reset_pin: GPIO27
  gpio_dc_pin: GPIO17
  gpio_buzzer: GPIO04
  gpio_led_red: GPIO19
  gpio_led_green: GPIO23
  gpio_led_blue: GPIO18
  
spi:
  clk_pin: GPIO12
  mosi_pin: GPIO13

display:
  - platform: waveshare_epaper
    id: epaper
    cs_pin: $gpio_cs_pin
    busy_pin: $gpio_busy_pin
    reset_pin: $gpio_reset_pin
    dc_pin: $gpio_dc_pin
    model: 4.20in # 300x400
    rotation: 270°
    update_interval: 60s
    lambda: |-
      it.print(0, 0, id(font_medium_20), "Hello World!");
      
font:
  - file: 'fonts/Kanit-Medium.ttf'
    id: font_medium_20
    size: 20
    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', '/', 'é']

I found a mistake in my substitutions. It’s works for me thx.

@makai
how have implement mdi font.
I download the ttf file. place in Esphom/fonts, but when a try to use it like you I don’t have icon on screen.

You may check this post: Display materialdesign icons on ESPHome attached to screen

If it does not solve your issue, please post your yaml.

1 Like

I use your code to understand how to do mine.

sensor:
  - platform: homeassistant
    name: "Outdoor Hum. from HA"
    entity_id: sensor.dark_sky_humidity
    id: outdoor_hum
    internal: true

display:
  - platform: waveshare_epaper
    id: epaper
    cs_pin: $gpio_cs_pin
    busy_pin: $gpio_busy_pin
    reset_pin: $gpio_reset_pin
    dc_pin: $gpio_dc_pin
    model: 4.20in # 300x400
    rotation: 270°
    update_interval: 60s
    lambda: |-
      ESP_LOGI("display", "Updating...");
      // OUTSIDE
      it.printf(7, 15, id(font_medium_20), TextAlign::BASELINE_LEFT, "Dehors");
      it.line(78, 14, 293, 14);
      it.printf(10, 84, id(icon_font_20), TextAlign::BASELINE_LEFT, ""); 
      if (id(outdoor_hum).has_state()) {
        it.printf(41, 82, id(font_regular_45), TextAlign::BASELINE_LEFT, "%2.0f", id(outdoor_hum).state);
        it.printf(95, 82, id(font_regular_30), TextAlign::BASELINE_LEFT, "%%");
      }
      
font:
  - file: 'fonts/Kanit-Medium.ttf'
    id: font_medium_20
    size: 20
    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_20
    size: 20
    glyphs: [
     "", # mdi-water-percent
     ]

Did you copy/paste the “glyphs” from my code above?
If yes, you should try to open the font on your computer and copy/paste from it.

ok i try it

@makai Bien vu !
Font have change. Thx a lot.

2 Likes

@makai One more question.
How work weather print ?
I use our code without success.

The weather alert binary sensor is a templated binary sensor based on the Météo France integration (you need to add it using GUI):

Under binary_sensor:

- platform: template
  sensors:
    alerte_meteo:
      entity_id: sensor.81_weather_alert
      friendly_name: Alerte Météo
      device_class: safety
      value_template: >-
        {{ is_state('sensor.81_weather_alert', 'Jaune') 
           or is_state('sensor.81_weather_alert', 'Orange')
           or is_state('sensor.81_weather_alert', 'Rouge') }}

For the forecast I use the Darksky integration and a templated sensor (under sensor: in my conf):

  - platform: template
    sensors:
      forecast_0:
        entity_id:
          - sensor.dark_sky_daytime_high_temperature_0d
          - sensor.dark_sky_overnight_low_temperature_0d
          - sensor.dark_sky_precip_probability_0d
          - sensor.dark_sky_icon_0d
          - sensor.dark_sky_uv_index_0d
        friendly_name: "Météo de la journée"
        value_template: 
          "{{ states('sensor.dark_sky_overnight_low_temperature_0d')|round(0) }}°/\
           {{ states('sensor.dark_sky_daytime_high_temperature_0d')|round(0) }}°/\
           {{ states('sensor.dark_sky_precip_probability_0d')|round(0) }}%/\
           {{ states('sensor.dark_sky_uv_index_0d') }}UV"
        icon_template: >-
          {% if is_state("sensor.dark_sky_icon_0d", "clear-day") %}
            mdi:weather-sunny
          {% elif is_state("sensor.dark_sky_icon_0d", "clear-night") %}
            mdi:weather-night
          {% elif is_state("sensor.dark_sky_icon_0d", "cloudy") %}
            mdi:weather-cloudy
          {% elif is_state("sensor.dark_sky_icon_0d", "rain") %}
            mdi:weather-pouring
          {% elif is_state("sensor.dark_sky_icon_0d", "sleet") %}
            mdi:weather-snowy-rainy
          {% elif is_state("sensor.dark_sky_icon_0d", "snow") %}
            mdi:weather-snowy
          {% elif is_state("sensor.dark_sky_icon_0d", "wind") %}
            mdi:weather-windy
          {% elif is_state("sensor.dark_sky_icon_0d", "fog") %}
            mdi:weather-fog
          {% elif is_state("sensor.dark_sky_icon_0d", "partly-cloudy-day") %}
            mdi:weather-partly-cloudy
          {% elif is_state("sensor.dark_sky_icon_0d", "partly-cloudy-night") %}
            mdi:weather-night-partly-cloudy
          {% else %}
            error
          {% endif %}

      forecast_1:
        entity_id:
          - sensor.dark_sky_daytime_high_temperature_1d
          - sensor.dark_sky_overnight_low_temperature_1d
          - sensor.dark_sky_precip_probability_1d
          - sensor.dark_sky_icon_1d
          - sensor.dark_sky_uv_index_1d
        friendly_name_template: '{{ (as_timestamp(now()) + (86400))|timestamp_custom("%A", True) }}'
        value_template:
          "{{ states('sensor.dark_sky_overnight_low_temperature_1d')|round(0) }}°/\
           {{ states('sensor.dark_sky_daytime_high_temperature_1d')|round(0) }}°/\
           {{ states('sensor.dark_sky_precip_probability_1d')|round(0) }}%/\
           {{ states('sensor.dark_sky_uv_index_1d') }}UV"
        icon_template: >-
          {% if is_state("sensor.dark_sky_icon_1d", "clear-day") %}
            mdi:weather-sunny
          {% elif is_state("sensor.dark_sky_icon_1d", "clear-night") %}
            mdi:weather-night
          {% elif is_state("sensor.dark_sky_icon_1d", "cloudy") %}
            mdi:weather-cloudy   
          {% elif is_state("sensor.dark_sky_icon_1d", "rain") %}
            mdi:weather-pouring        
          {% elif is_state("sensor.dark_sky_icon_1d", "sleet") %}
            mdi:weather-snowy-rainy
          {% elif is_state("sensor.dark_sky_icon_1d", "snow") %}
            mdi:weather-snowy
          {% elif is_state("sensor.dark_sky_icon_1d", "wind") %}
            mdi:weather-windy
          {% elif is_state("sensor.dark_sky_icon_1d", "fog") %}
            mdi:weather-fog
          {% elif is_state("sensor.dark_sky_icon_1d", "partly-cloudy-day") %}
            mdi:weather-partly-cloudy
          {% elif is_state("sensor.dark_sky_icon_1d", "partly-cloudy-night") %}
            mdi:weather-night-partly-cloudy            
          {% else %}
            error
          {% endif %}

It’s work ! Are you on the French discord ?

Hi @makai,
Thanks for sharing the project.
Can I use waveshare 7,5” e-ink display that uses red color along with black and white?
https://www.waveshare.com/7.5inch-hd-e-paper-b.htm

I would like to use red color to accent high values.

I have other question regarding 3D printed case, the finish on the 3D printed materials always feels cheap and not smooth at all, do you have some method where it would turn out like a molded plastic?

ESPHome does not currently support color for displays. So only the “black and white” 7.5" display is supported and the red one won’t work at all.

Regarding the case, I have no idea :man_shrugging: