본문 바로가기

DIY

7.5인치 e-ink 디스플레이 날씨 정보 표시 ESPHome

7인치 라즈베리파이 모니터도 있기는 하지만, 벽에 매달아 놓아서 늘 참고할 수 있도록 만들어 보았습니다. 

 

7.5인치 e-ink에 날씨 정보 표시한 모습

 

이케아 RIBBA 액자 13x18 안에 Waveshare 7.5inch E-Paper (B) Raw Display SPI without PCB와 Waveshare E-Paper ESP32 Driver Board 제품을 넣어서 만들었습니다.  

 

원래는 아래 링크를 보고 시작했습니다. 

https://www.hackster.io/news/nerdiy-s-3d-printed-framework-turns-an-ikea-ribba-into-a-seeed-xiao-esp32c3-powered-epaper-dashboard-3d09f96f7936?s=32

 

Nerdiy's 3D-Printed Framework Turns an IKEA RIBBA Into a Seeed XIAO ESP32C3-Powered ePaper Dashboard

Designed to make hardware installation as easy as possible, this clever 3D-printed adapter is perfectly sized for the components in use.

www.hackster.io

 

Waveshare의 E-Paper ESP8266 Driver Board는 메모리 부족으로 작동이 안되어서 ESP32로 새로 구입해서 작동시켰습니다. E-Paper는 V3가 맞지만, 빨간색 표현은 ESPHome에서 지원이 아직 안되더군요. 

 

전체적인 정보는 Home Assistant의 네이버 날씨(HACS를 통해 제공)를 바탕으로 하고, 실내 미세먼지와 이산화탄소 값은 PMS7003과 MH-Z19B 센서를 DIY로 만든 ESPHome장치를 Home Assistant와 연결하여 받고 있습니다. 

 

ESPHome을 위해 작성한 esp32-eink75.yaml 파일은 다음과 같습니다. 나중에 좀 더 예쁘게 수정해 보겠습니다. 

esphome:
  name: esp32-eink75

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  password: ""

ota:
  platform: esphome
  password: ""

wifi:
  ssid: !secret ssid 
  password: !secret password 
#  domain: !secret domain 
  
  manual_ip:
    # Set this to the IP of the ESP
    static_ip: 192.168.1.219
    # Set this to the IP address of the router. Often ends with .1
    gateway: 192.168.1.1
    # The subnet of the network. 255.255.255.0 works for most home networks.
    subnet: 255.255.255.0

  ap:
    ssid: "Esp32-Eink75 Fallback Hotspot"
    password: "vzdz2ed5KqFN"

captive_portal:

time:
  - platform: homeassistant
    id: ntp

# ttf files are required 
font:
  - file: "NanumBarunGothic-YetHangul.ttf"
    id: font1
    size: 32
    glyphs: |-
      !"%()+=,-_./:°℃℉✽㎍[]0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz초미세먼지외부내이산화탄소
      현재날씨정보온도오늘일최고저풍속강수확률습구름많음비안옴흐림눈나기시월분작후맑전잠만다리요실공질예가끔황
  - file: "materialdesignicons-webfont.ttf"
    id: mdi
    size: 40
    glyphs:
      - "\U000F0590" # mdi-weather-cloudy
      - "\U000F0F2F" # mdi-weather-cloudy-alert
      - "\U000F0E6E" # mdi-weather-cloudy-arrow-right
      - "\U000F0591" # mdi-weather-fog
      - "\U000F0592" # mdi-weather-hail
      - "\U000F0F30" # mdi-weather-hazy
      - "\U000F0898" # mdi-weather-hurricane
      - "\U000F0593" # mdi-weather-lightning
      - "\U000F067E" # mdi-weather-lightning-rainy
      - "\U000F0594" # mdi-weather-night
      - "\U000F0F31" # mdi-weather-night-partly-cloudy
      - "\U000F0595" # mdi-weather-partly-cloudy
      - "\U000F0F32" # mdi-weather-partly-lightning
      - "\U000F0F33" # mdi-weather-partly-rainy
      - "\U000F0F34" # mdi-weather-partly-snowy
      - "\U000F0F35" # mdi-weather-partly-snowy-rainy
      - "\U000F0596" # mdi-weather-pouring
      - "\U000F0597" # mdi-weather-rainy
      - "\U000F0598" # mdi-weather-snowy
      - "\U000F0F36" # mdi-weather-snowy-heavy
      - "\U000F067F" # mdi-weather-snowy-rainy
      - "\U000F0599" # mdi-weather-sunny
      - "\U000F0F37" # mdi-weather-sunny-alert
      - "\U000F14E4" # mdi-weather-sunny-off
      - "\U000F059A" # mdi-weather-sunset
      - "\U000F059B" # mdi-weather-sunset-down
      - "\U000F059C" # mdi-weather-sunset-up
      - "\U000F0F38" # mdi-weather-tornado
      - "\U000F059D" # mdi-weather-windy
      - "\U000F059E" # mdi-weather-windy-variant
      - "\U000F17FF" # mdi-sun-wireless-outline
      - "\U000F018C" # mdi-compass-outline
      - "\U000F0D43" # mdi-air-filter
      - "\U000F0D44" # mdi-air-purifier
      - "\U000F1586" # mdi-face-mask
      - "\U000F1587" # mdi-face-mask-outline
      - "\U000F11DC" # mdi-window-open-variant
      - "\U000F13E1" # mdi-umbrella-closed-outline

# Your sensor values of target Home Assistant 
sensor: 
  - platform: homeassistant 
    entity_id: sensor.2_5um                                           # 초미세먼지(실내) DIY 센서 
    id: indoor_pm25
    internal: true 
  - platform: homeassistant 
    entity_id: sensor.naver_weather_ultrafinedust_1        # 초미세먼지 
    id: outdoor_pm25
    internal: true 
  - platform: homeassistant 
    entity_id: sensor.mh_z19_co2_value                          # 이산화탄소 DIY 센서
    id: co2
    internal: true
  - platform: homeassistant 
    entity_id: sensor.naver_weather_finedust_1               # 미세먼지 
    id: outdoor_pm10
    internal: true
  - platform: homeassistant 
    entity_id: sensor.naver_weather_nowtemp_1             # 현재 온도
    id: nowtemp
    internal: true
  - platform: homeassistant 
    entity_id: sensor.naver_weather_todaymaxtemp_1     # 오늘 최고 온도
    id: todaymaxtemp
    internal: true
  - platform: homeassistant 
    entity_id: sensor.naver_weather_tomorrowatemp_1    # 내일최고온도
    id: tomorrowmaxtemp
    internal: true
  - platform: homeassistant 
    entity_id: sensor.naver_weather_tomorrowmtemp_1   # 내일최저온도
    id: tomorrowmintemp 
    internal: true

text_sensor: 
  - platform: homeassistant 
    entity_id: sensor.naver_weather_nowweather_1         # 현재 날씨 
    id: nowweather
    internal: true
  - platform: homeassistant 
    entity_id: sensor.naver_weather_tomorrowmstate_1   # 내일오전날씨 
    id: tomorrowweather1
    internal: true
  - platform: homeassistant 
    entity_id: sensor.naver_weather_tomorrowastate_1    # 내일오후날씨
    id: tomorrowweather2
    internal: true
  - platform: homeassistant 
    entity_id: sensor.naver_weather_rainystart_1             # 오늘 비시작 시간 
    id: todayraintime
    internal: true  

color:
  - id: color_black
    red: 0%
    green: 0%
    blue: 0%
    white: 0%
  - id: color_white
    red: 0%
    green: 0%
    blue: 0%
    white: 100%
  - id: color_red
    red: 100%
    green: 0%
    blue: 0%
    white: 0%

spi:
  clk_pin: GPIO13  
  mosi_pin: GPIO14 

display:
  - platform: waveshare_epaper
    cs_pin: GPIO15  
    dc_pin: GPIO27  
    busy_pin: GPIO25    
    reset_pin: GPIO26   
    model: 7.50in-bv3  # 7.50in-bv3-bwr   
    rotation: 270
    update_interval: 60s 
    lambda: |-
      // https://github.com/Nerdiyde/ESPHomeSnippets
      // Map weather states to MDI characters.
      std::map<std::string, std::string> weather_icon_map
        {
          {"구름많음", "\U000F0590"},
          {"안개", "\U000F0591"},
          {"흐림", "\U000F0595"},
          {"가끔비", "\U000F0F33"},
          {"가끔눈", "\U000F0F34"},
          {"가끔비눈", "\U000F0F35"},
          {"폭우", "\U000F0596"},
          {"비", "\U000F0597"},
          {"눈", "\U000F0598"},
          {"폭설", "\U000F0F36"},
          {"비눈", "\U000F067F"},
          {"맑음", "\U000F0599"},
        };

      std::map<std::string, std::string> etc_icon_map
        {
          {"airfilter", "\U000F0D43"}, 
          {"airpurifier", "\U000F0D44"},
          {"facemask", "\U000F1586"},
          {"facemask_outline", "\U000F1587"},
          {"window_open_variant", "\U000F11DC"}, 
          {"umbrella_closed_outline", "\U000F13E1"},
        };

      // position 
      #define padx 24
      #define pady 72
    
      if(id(nowweather).state != "") {
        it.strftime(padx, pady, id(font1), " %Y-%m-%d  %a  %H: %M", id(ntp).now());

        it.printf(padx, pady + 36 * 2, id(font1), TextAlign::TOP_LEFT, "[ 외부 ]");
        it.printf(padx, pady + 36 * 3, id(font1), TextAlign::TOP_LEFT, "✽ 미세먼지: %.0f㎍", id(outdoor_pm10).state);
        if(id(outdoor_pm10).state > 15) { 
          it.printf(padx + 310, pady + 36 * 3, id(mdi), TextAlign::TOP_LEFT, "%s", etc_icon_map["facemask_outline"].c_str());
        }
        it.printf(padx, pady + 36 * 4, id(font1), TextAlign::TOP_LEFT, "✽ 초미세먼지: %.0f㎍", id(outdoor_pm25).state);
        if(id(outdoor_pm25).state > 10) { 
          it.printf(padx + 310, pady + 36 * 4, id(mdi), TextAlign::TOP_LEFT, "%s", etc_icon_map["facemask_outline"].c_str());
        }

        it.printf(padx, pady + 36 * 5, id(font1), TextAlign::TOP_LEFT, "[ 실내 ]");
        it.printf(padx, pady + 36 * 6, id(font1), TextAlign::TOP_LEFT, "✽ 초미세먼지: %.0f㎍", id(indoor_pm25).state);
        if(id(indoor_pm25).state > 5) { 
          it.printf(padx + 350, pady + 36 * 6, id(mdi), TextAlign::TOP_LEFT, "%s", etc_icon_map["airfilter"].c_str());
        }
        it.printf(padx, pady + 36 * 7, id(font1), TextAlign::TOP_LEFT, "✽ 이산화탄소: %.0fppm", id(co2).state);
        if(id(co2).state > 800) { 
          it.printf(padx + 350, pady + 36 * 7, id(mdi), TextAlign::TOP_LEFT, "%s", etc_icon_map["window_open_variant"].c_str());
        }

        it.printf(padx, pady + 36 * 8, id(font1), TextAlign::TOP_LEFT, "[ 오늘 ]");              
        it.printf(padx, pady + 36 * 9, id(font1), TextAlign::TOP_LEFT, "✽ 현재 날씨: %s", id(nowweather).state.c_str());
        it.printf(padx + 240, pady + 36 * 9, id(mdi), color_red, TextAlign::TOP_LEFT, "%s",  weather_icon_map[id(nowweather).state.c_str()].c_str());
        it.printf(padx, pady + 36 * 10, id(font1), TextAlign::TOP_LEFT, "✽ 현재 온도: %.0f℃", id(nowtemp).state);
        it.printf(padx, pady + 36 * 11, id(font1), TextAlign::TOP_LEFT, "✽ 최고 온도: %.0f℃", id(todaymaxtemp).state);
        it.printf(padx, pady + 36 * 12, id(font1), TextAlign::TOP_LEFT, "✽ 비 시작: %s", id(todayraintime).state.c_str());
        if(strcmp(id(todayraintime).state.c_str(), "비안옴") != 0) { 
          it.printf(padx + 240, pady + 36 * 12, id(mdi), TextAlign::TOP_LEFT, "%s", etc_icon_map["umbrella_closed_outline"].c_str());
        }
        it.printf(padx, pady + 36 * 13, id(font1), TextAlign::TOP_LEFT, "[ 내일 ]");  
        it.printf(padx, pady + 36 * 14, id(font1), TextAlign::TOP_LEFT, "✽ 오전 날씨: %s", id(tomorrowweather1).state.c_str());
        it.printf(padx, pady + 36 * 15, id(font1), TextAlign::TOP_LEFT, "✽ 오후 날씨: %s", id(tomorrowweather2).state.c_str());
        it.printf(padx, pady + 36 * 16, id(font1), TextAlign::TOP_LEFT, "✽ 최고 온도: %.0f℃", id(tomorrowmaxtemp).state);
        it.printf(padx, pady + 36 * 17, id(font1), TextAlign::TOP_LEFT, "✽ 최저 온도: %.0f℃", id(tomorrowmintemp).state);
      } else { 
        it.printf(padx, pady + 36 * 8, id(font1), TextAlign::TOP_LEFT,"✽✽ 잠시만 기다리세요... ✽✽"); 
      }

 

아이콘이 모두 표시된 모습은 다음과 같습니다. 

 

마스크, 공기정화기필터, 여닫이 창문 열기, 맑음 표시용 폰트 표시 모습