실내 미세먼지의 관리 여부와 외부 마스크 착용 여부에 따라 '코'나 '목'의 (콧물, 가래나 불편함)상태가 바뀌는 것을 체감한 후부터는 항상 실내 이산화탄소와 미세먼지 수치를 관리하고 있습니다. 특히 실내 초미세와 미세먼지 수치는 5㎍/㎥ 미만으로 365일 24시간 관리하고 있습니다.
거실에 놓아둔 라즈베리파이 7인치 모니터로 Home Assistant가 표시되는 부모님 댁과 달리, 저는 컴퓨터나 스마트폰으로만 확인이 가능했는데, 잉여로 남아 돌던 e-ink 2.7인치에 표시해 보았습니다.
간단하지만 좋네요.
내용도 추가하고 한글 출력 및 3D 인쇄한 케이스도 부착해 보았습니다.
참고로 3D 케이스는 아직도(!) 설계 툴을 배우지 않고 ChatGPT를 괴롭혔습니다. OpenSCAD를 사용하여 .SCAD로부터 .SLT를 만들 수 있었습니다. 인쇄 중에 문제가 있었긴 했는데, 나중에 칼로 잘 잘라내고 긁어냈습니다. 아무래도 인쇄 재료 아끼는 것 때문에 문제가 있었던 것 같고 빼는 것이 나을 것 같습니다.
# 내부가 비어 있는 직육면체가 필요합니다.
# 직육면체의 외곽 크기는 84x58x8 (단위: mm) 이고, 윗면 케이스만 있으면 되고, 아랫면은 비워주세요.
# 전체적인 케이스의 두께는 6mm이고, 특별히 LCD를 결합할 윗면은 2mm로 해주세요.
# LCD viewing 영역은 뚫어 주어야 하는데, 윗면 기준으로 좌표는 mm단위로 좌상단 (17, 10)과 우하단 (74, 48)입니다.
# 3D 인쇄 재료를 아낄 수 있게 해주고, 모서리 r도 적당히 추가해 주세요.
from solid import *
from solid.utils import *
# Dimensions in mm
outer_dimensions = [84, 58, 8] # Length, width, height
wall_thickness = 3 # Reduced to save material
lcd_cover_thickness = 2
lcd_viewing_area_top_left = [17, 10]
lcd_viewing_area_bottom_right = [74, 48]
corner_radius = 2 # Small rounding for edges
# Create the outer box with rounded corners
def create_case():
outer_box = rounded_cube(outer_dimensions, corner_radius)
# Subtract the inner box to create the hollow structure
inner_dimensions = [
outer_dimensions[0] - 2 * wall_thickness,
outer_dimensions[1] - 2 * wall_thickness,
outer_dimensions[2]
]
inner_box = translate([wall_thickness, wall_thickness, 0])(rounded_cube(inner_dimensions, corner_radius))
hollow_case = outer_box - inner_box
# Create the LCD cover part (2mm thick)
lcd_cover = cube([
outer_dimensions[0],
outer_dimensions[1],
lcd_cover_thickness
], center=False)
# Add the viewing area cutout
lcd_viewing_area = translate([
lcd_viewing_area_top_left[0],
lcd_viewing_area_top_left[1],
0
])(cube([
lcd_viewing_area_bottom_right[0] - lcd_viewing_area_top_left[0],
lcd_viewing_area_bottom_right[1] - lcd_viewing_area_top_left[1],
lcd_cover_thickness
], center=False))
lcd_cover_with_cutout = lcd_cover - lcd_viewing_area
# Combine the hollow case and the LCD cover
final_case = hollow_case + translate([0, 0, outer_dimensions[2] - lcd_cover_thickness])(lcd_cover_with_cutout)
return final_case
# Rounded cube function
def rounded_cube(size, radius):
return minkowski()(cube(size), sphere(radius))
# Generate the 3D model
case_model = create_case()
# Save the model to an STL file
scad_render_to_file(case_model, 'rectangular_case.scad')
[사용 장치]
- 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:
- 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.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
# fonts directory and ttf file are required
font:
- file: "NanumBarunGothic-YetHangul.ttf"
id: font1
size: 24
glyphs: |-
!"%()+=,-_./:°℃℉✽㎍[]0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz초미세먼지외부내이산화탄소
현재날씨정보온도오늘일최고저풍속강수확률습구름많음비안옴흐림눈나기시월분작후맑전잠만다리요실공질예가끔황
spi:
clk_pin: D5
mosi_pin: D7
# Your sensor values of target Home Assistant
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
- platform: homeassistant
entity_id: sensor.mi_air_purifier_2s_humidity
id: indoor_humidity
internal: true
- platform: homeassistant
entity_id: sensor.naver_weather_nowtemp_1 # 현재 온도
id: nowtemp
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일(%a) %H:%M", id(ntp).now());
it.printf(0 + pad, 10 + pad + 24, id(font1), TextAlign::TOP_LEFT, "✽ 외부 PM2.5: %.0f㎍", id(outdoor_pm25).state);
it.printf(0 + pad, 10 + pad + 24 * 2, id(font1), TextAlign::TOP_LEFT, "✽ 실내 PM2.5: %.0f㎍", id(indoor_pm25).state);
it.printf(0 + pad, 10 + pad + 24 * 3, id(font1), TextAlign::TOP_LEFT, "✽ 이산화탄소: %.0fppm", id(co2).state);
it.printf(0 + pad, 10 + pad + 24 * 4, id(font1), TextAlign::TOP_LEFT, "✽ 실내 습도: %.0f%%", id(indoor_humidity).state);
it.printf(0 + pad, 10 + pad + 24 * 5, id(font1), TextAlign::TOP_LEFT, "✽ 현재 온도: %.0f℃", id(nowtemp).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,"✽✽ 잠시만 기다리세요... ✽✽");
}
'홈어시스턴트 IoT' 카테고리의 다른 글
드리미 L10s Ultra 물걸레 로봇 청소기 홈어시스턴트 연결 (0) | 2023.08.25 |
---|---|
지그비 리모트 4버튼 스위치 - 홈어시스턴트와 Sonoff 지그비 동글 (0) | 2023.08.22 |
8x8 적외선 온도계 GY-MCU8833의 ESPHome 커스텀 센서 연동 (0) | 2023.07.23 |
구형에어컨 제습운전 자동화 홈어시스턴트 ESPHome DIY 적외선리모콘 (0) | 2023.07.22 |
가정 내 소비전력 모니터링 - 시하스 PMM-300-Z 지그비 (0) | 2023.07.05 |