본문 바로가기

홈어시스턴트 IoT

미세먼지와 이산화탄소에 진심? Waveshare e-ink에 항상 표시

실내 미세먼지의 관리 여부와 외부 마스크 착용 여부에 따라 '코'나 '목'의 (콧물, 가래나 불편함)상태가 바뀌는 것을 체감한 후부터는 항상 실내 이산화탄소와 미세먼지 수치를 관리하고 있습니다. 특히 실내 초미세와 미세먼지 수치는 5㎍/㎥ 미만으로 365일 24시간 관리하고 있습니다. 

 

거실에 놓아둔 라즈베리파이 7인치 모니터로 Home Assistant가 표시되는 부모님 댁과 달리, 저는 컴퓨터나 스마트폰으로만 확인이 가능했는데, 잉여로 남아 돌던 e-ink 2.7인치에 표시해 보았습니다. 

 

간단하지만 좋네요. 

 

실외/실내 초미세먼지, 이산화탄소

[사용 장치] 

  • 2.7인치 e-ink 디스플레이 : 원래 RPi용 HAT에 붙어 있던 것을 떼어 내어 이용(제품 설명)
  • Waveshare e-Paper ESP8266 Driver Board(제품 설명)

[ESPHome 코드] 

Home Assistant에 HACS를 통해 설치한 네이버 날씨 정보에서 외부 초미세먼지를 가져오고, ESP8266보드 기반 DIY로 만든 미세먼지센서(SDS021)와 이산화탄소센서(MH-Z19B)는 ESPHome을 통해 Home Assitant로 연결되어 있습니다. 

 

ESP8266 Driver Board에 올린 ESPHome .YAML 파일 내용은 다음과 같습니다. 

esphome:
  name: epaper8266

esp8266:
  board: nodemcu

# Enable logging
logger:

# Enable Home Assistant API
api:
  password: ""

ota:
  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.177
    # 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

captive_portal:

time:
  - platform: homeassistant
    id: ntp

# Example configuration entry
font:
  - file: "fonts/verdana.ttf"
    id: font1
    size: 36
    glyphs: |-
      !"%()+=,-_./:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz

spi:
  clk_pin: D5
  mosi_pin: D7

sensor: 
  - platform: homeassistant 
    entity_id: sensor.2_5um
    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
    id: co2
    internal: true 

    
display:
  - platform: waveshare_epaper
    cs_pin: D8
    dc_pin: D2
    busy_pin: D1
    reset_pin: D4
    model: 2.70in
    rotation: 270
    update_interval: 60s 
    lambda: |-
      // position 
      #define xRes 264
      #define yRes 176 
      #define pad 10
      
      it.strftime(0 + pad, 0 + pad, id(font1), "%m-%d %H:%M", id(ntp).now());
      it.printf(0 + pad, 0 + pad + 36, id(font1), TextAlign::TOP_LEFT, "0PM25: %.0f", id(outdoor_pm25).state);
      it.printf(0 + pad, 0 + pad + 36 * 2, id(font1), TextAlign::TOP_LEFT, "IPM25: %.0f", id(indoor_pm25).state);
      it.printf(0 + pad, 0 + pad + 36 * 3, id(font1), TextAlign::TOP_LEFT, "CO2: %.0f", id(co2).state);

 

 

아래는 7.5인치에 Waveshare ESP32 driver board로 구현한 것입니다. 이케아의 포토프레임에 넣어 보았습니다. 

 

 

 

 

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.0.219
    # Set this to the IP address of the router. Often ends with .1
#    gateway: 192.168.0.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.pm_2_5mm              # 초미세먼지(실내) DIY 센서 
    id: indoor_pm25
    internal: true 
  - platform: homeassistant 
    entity_id: sensor.comisemeonji        # 초미세먼지 
    id: outdoor_pm25
    internal: true 
  - platform: homeassistant 
    entity_id: sensor.mh_z19_co2_value     # 이산화탄소 DIY 센서
    id: co2
    internal: true
  - platform: homeassistant 
    entity_id: sensor.misemeonji               # 미세먼지 
    id: outdoor_pm10
    internal: true
  - platform: homeassistant 
    entity_id: sensor.hyeonjaeondo             # 현재 온도
    id: nowtemp
    internal: true
  - platform: homeassistant 
    entity_id: sensor.coegoondo     # 오늘 최고 온도
    id: todaymaxtemp
    internal: true
  - platform: homeassistant 
    entity_id: sensor.naeilcoegoondo    # 내일최고온도
    id: tomorrowmaxtemp
    internal: true
  - platform: homeassistant 
    entity_id: sensor.naeilcoejeoondo   # 내일최저온도
    id: tomorrowmintemp 
    internal: true

text_sensor: 
  - platform: homeassistant 
    entity_id: sensor.hyeonjaenalssi         # 현재 날씨 
    id: nowweather
    internal: true
  - platform: homeassistant 
    entity_id: sensor.naeilojeonnalssi   # 내일오전날씨 
    id: tomorrowweather1
    internal: true
  - platform: homeassistant 
    entity_id: sensor.naeilohunalssi    # 내일오후날씨
    id: tomorrowweather2
    internal: true
  - platform: homeassistant 
    entity_id: sensor.bisijagsiganoneul             # 오늘 비시작 시간 
    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  # -bwr #7.50in-bv3  
    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 + 350, 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 + 350, 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 + 350, 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 + 350, 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,"✽✽ 잠시만 기다리세요... ✽✽"); 
      }