Created
August 16, 2024 20:19
-
-
Save ali-essam/14b8ae7eaeef6f6dcfe354b749c4934b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Derived from https://github.com/MallocArray/airgradient_esphome | |
esphome: | |
name: aqiprime | |
friendly_name: AQI Prime | |
on_boot: | |
then: | |
- light.turn_on: | |
id: aqi_light | |
brightness: 100% | |
red: 100% | |
green: 0 | |
blue: 0 | |
- delay: 5s | |
- select.set: | |
id: display_page | |
option: !lambda return id(display_page).state; | |
time: | |
- platform: homeassistant | |
id: esptime | |
esp32: | |
board: wemos_d1_mini32 | |
# Enable logging | |
logger: | |
# Enable Home Assistant API | |
api: | |
encryption: | |
key: !secret encryption_key | |
ota: | |
- platform: esphome | |
password: !secret ota_password | |
wifi: | |
ssid: !secret wifi_name | |
password: !secret wifi_pass | |
fast_connect: true | |
uart: | |
- baud_rate: 9600 | |
tx_pin: GPIO18 | |
rx_pin: GPIO19 | |
id: pms5003_uart | |
- tx_pin: GPIO23 | |
rx_pin: GPIO5 | |
baud_rate: 9600 | |
id: senseair_s8_uart | |
i2c: | |
button: | |
- platform: template | |
name: SenseAir S8 Calibration | |
id: senseair_s8_calibrate_button | |
on_press: | |
- then: | |
- senseair.background_calibration: | |
id: senseair_s8 | |
- delay: 70s | |
- senseair.background_calibration_result: | |
id: senseair_s8 | |
disabled_by_default: false | |
- platform: template | |
name: SenseAir S8 Show Calibration Interval | |
id: senseair_s8_show_calibrate_interval | |
on_press: | |
- then: | |
- senseair.abc_get_period: | |
id: senseair_s8 | |
disabled_by_default: false | |
sensor: | |
- platform: pmsx003 | |
# PMS5003 https://esphome.io/components/sensor/pmsx003.html | |
type: PMSX003 | |
uart_id: pms5003_uart | |
pm_2_5: | |
name: "PM 2.5" | |
id: pm_2_5 | |
device_class: pm25 # Added to report properly to HomeKit | |
pm_1_0: | |
name: "PM 1.0" | |
id: pm_1_0 | |
device_class: pm1 # Added to report properly to HomeKit | |
pm_10_0: | |
name: "PM 10.0" | |
id: pm_10_0 | |
device_class: pm10 # Added to report properly to HomeKit | |
pm_0_3um: | |
name: "PM 0.3" | |
id: pm_0_3um | |
# update_interval: 30s | |
- platform: template | |
# Depends on another sensor providing an ID of pm_2_5 such as a pms5003 | |
name: "PM 2.5 AQI" | |
id: pm_2_5_aqi | |
update_interval: 5 min | |
unit_of_measurement: "AQI" | |
icon: "mdi:air-filter" | |
accuracy_decimals: 0 | |
filters: | |
- skip_initial: 1 # Need valid data from PM 2.5 sensor before able to calculate | |
lambda: |- | |
// https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI | |
// Borrowed from https://github.com/kylemanna/sniffer/blob/master/esphome/sniffer_common.yaml | |
if (id(pm_2_5).state <= 12.0) { | |
// good | |
return((50.0 - 0.0) / (12.0 - 0.0) * (id(pm_2_5).state - 0.0) + 0.0); | |
} else if (id(pm_2_5).state <= 35.4) { | |
// moderate | |
return((100.0 - 51.0) / (35.4 - 12.1) * (id(pm_2_5).state - 12.1) + 51.0); | |
} else if (id(pm_2_5).state <= 55.4) { | |
// usg | |
return((150.0 - 101.0) / (55.4 - 35.5) * (id(pm_2_5).state - 35.5) + 101.0); | |
} else if (id(pm_2_5).state <= 150.4) { | |
// unhealthy | |
return((200.0 - 151.0) / (150.4 - 55.5) * (id(pm_2_5).state - 55.5) + 151.0); | |
} else if (id(pm_2_5).state <= 250.4) { | |
// very unhealthy | |
return((300.0 - 201.0) / (250.4 - 150.5) * (id(pm_2_5).state - 150.5) + 201.0); | |
} else if (id(pm_2_5).state <= 350.4) { | |
// hazardous | |
return((400.0 - 301.0) / (350.4 - 250.5) * (id(pm_2_5).state - 250.5) + 301.0); | |
} else if (id(pm_2_5).state <= 500.4) { | |
// hazardous 2 | |
return((500.0 - 401.0) / (500.4 - 350.5) * (id(pm_2_5).state - 350.5) + 401.0); | |
} else { | |
return(500); | |
} | |
- platform: sht3xd | |
# SHT30 https://esphome.io/components/sensor/sht3xd.html | |
heater_enabled: false | |
temperature: | |
name: Temperature | |
id: temp | |
unit_of_measurement: °C | |
icon: mdi:thermometer | |
accuracy_decimals: 2 | |
device_class: temperature | |
state_class: measurement | |
humidity: | |
name: Humidity | |
id: humidity | |
unit_of_measurement: "%" | |
icon: mdi:water-percent | |
accuracy_decimals: 2 | |
device_class: humidity | |
state_class: measurement | |
address: 0x44 | |
- platform: sgp4x | |
voc: | |
name: VOC Index | |
id: voc | |
icon: mdi:radiator | |
accuracy_decimals: 0 | |
device_class: aqi | |
state_class: measurement | |
nox: | |
name: NOx Index | |
id: nox | |
icon: mdi:radiator | |
accuracy_decimals: 0 | |
device_class: aqi | |
state_class: measurement | |
compensation: | |
temperature_source: temp | |
humidity_source: humidity | |
store_baseline: true | |
update_interval: 60s | |
address: 0x59 | |
- platform: senseair | |
co2: | |
name: CO2 | |
id: co2 | |
filters: | |
- skip_initial: 2 | |
unit_of_measurement: ppm | |
icon: mdi:molecule-co2 | |
accuracy_decimals: 0 | |
device_class: carbon_dioxide | |
state_class: measurement | |
id: senseair_s8 | |
uart_id: senseair_s8_uart | |
update_interval: 60s | |
font: | |
- file: "gfonts://Open Sans" | |
id: open_sans_14 | |
size: 14 | |
glyphs: '|!"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz/µ³' | |
- file: "gfonts://Open Sans" | |
id: open_sans_9 | |
size: 9 | |
glyphs: '|!"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz/µ³' | |
- file: "gfonts://Open Sans" | |
id: open_sans_11 | |
size: 11 | |
glyphs: '¤ѧҁ|!"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz/µ³' | |
- file: "gfonts://Open Sans" | |
id: open_sans_20 | |
size: 20 | |
glyphs: '|!"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz/µ³' | |
- file: "gfonts://Open Sans" | |
id: open_sans_48 | |
size: 48 | |
glyphs: '|!"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz/µ³' | |
display: | |
- platform: addressable_light | |
id: aqi_light_addressable | |
addressable_light_id: aqi_light | |
width: 8 | |
height: 1 | |
update_interval: 1s | |
lambda: |- | |
// Draw a bulls-eye pattern | |
Color green = Color(0x00FF00) * id(led_brightness).state; | |
Color yellow = Color(0xFFFF00) * id(led_brightness).state; | |
Color red = Color(0xFF0000) * id(led_brightness).state; | |
Color purple = Color(0xFF00FF) * id(led_brightness).state; | |
Color blue = Color(0x0000FF) * id(led_brightness).state; | |
Color pm_1_0_color; | |
Color pm_2_5_color; | |
Color pm_10_0_color; | |
Color co2_color; | |
Color voc_color; | |
Color nox_color; | |
if (isnan(id(pm_1_0).state)) { | |
// good | |
pm_1_0_color = blue; | |
} | |
else if (id(pm_1_0).state <= 5.0) { | |
// good | |
pm_1_0_color = green; | |
} else if (id(pm_1_0).state <= 55.0) { | |
// moderate | |
pm_1_0_color = yellow; | |
} else if (id(pm_1_0).state <= 200.0) { | |
// bad | |
pm_1_0_color = red; | |
} else { | |
// very bad | |
pm_1_0_color = purple; | |
} | |
if (isnan(id(pm_2_5).state)) { | |
// good | |
pm_2_5_color = blue; | |
} | |
else if (id(pm_2_5).state <= 5.0) { | |
// good | |
pm_2_5_color = green; | |
} else if (id(pm_2_5).state <= 55.0) { | |
// moderate | |
pm_2_5_color = yellow; | |
} else if (id(pm_2_5).state <= 200.0) { | |
// bad | |
pm_2_5_color = red; | |
} else { | |
// very bad | |
pm_2_5_color = purple; | |
} | |
if (isnan(id(pm_10_0).state)) { | |
// good | |
pm_10_0_color = blue; | |
} | |
else if (id(pm_10_0).state <= 5.0) { | |
// good | |
pm_10_0_color = green; | |
} else if (id(pm_10_0).state <= 55.0) { | |
// moderate | |
pm_10_0_color = yellow; | |
} else if (id(pm_10_0).state <= 200.0) { | |
// bad | |
pm_10_0_color = red; | |
} else { | |
// very bad | |
pm_10_0_color = purple; | |
} | |
if (isnan(id(co2).state)) { | |
// good | |
co2_color = blue; | |
} | |
else if (id(co2).state <= 700.0) { | |
// good | |
co2_color = green; | |
} else if (id(co2).state <= 1000.0) { | |
// moderate | |
co2_color = yellow; | |
} else if (id(co2).state <= 2000.0) { | |
// bad | |
co2_color = red; | |
} else { | |
// very bad | |
co2_color = purple; | |
} | |
if (isnan(id(voc).state)) { | |
// good | |
voc_color = blue; | |
} | |
else if (id(voc).state <= 100.0) { | |
// good | |
voc_color = green; | |
} else if (id(voc).state <= 150.0) { | |
// moderate | |
voc_color = yellow; | |
} else if (id(voc).state <= 250.0) { | |
// bad | |
voc_color = red; | |
} else { | |
// very bad | |
voc_color = purple; | |
} | |
if (isnan(id(nox).state)) { | |
// good | |
nox_color = blue; | |
} | |
else if (id(nox).state <= 1.0) { | |
// good | |
nox_color = green; | |
} else { | |
// very bad | |
nox_color = purple; | |
} | |
it.line(7, 0, 7, 0, pm_1_0_color); | |
it.line(6, 0, 6, 0, pm_2_5_color); | |
it.line(5, 0, 5, 0, pm_10_0_color); | |
it.line(3, 0, 3, 0, co2_color); | |
it.line(1, 0, 1, 0, voc_color); | |
it.line(0, 0, 0, 0, nox_color); | |
- platform: ssd1306_i2c | |
# https://esphome.io/components/display/ssd1306.html | |
# Formatting reference: https://www.tutorialspoint.com/c_standard_library/c_function_printf.htm | |
model: "SH1106 128x64" | |
id: oled_display | |
address: 0x3C | |
# rotation: 180° | |
pages: | |
- id: aq_default_page | |
# https://forum.airgradient.com/t/airgradient-one-customized-mallocarray-esphome-display/1328 | |
lambda: |- | |
it.printf(0, 0, id(open_sans_14), "%.1f°C", id(temp).state); | |
it.printf(128, 0, id(open_sans_14), TextAlign::TOP_RIGHT, "%.1f%%", id(humidity).state); | |
it.line(0,17,128,17); | |
it.printf(0,19, id(open_sans_9), "CO2"); | |
it.printf(0,27, id(open_sans_20), "%.0f", id(co2).state); | |
it.printf(0,52, id(open_sans_9), "ppm"); | |
it.line(50,19,50,64); | |
it.printf(54, 19, id(open_sans_9), "PM2.5"); | |
it.printf(54, 27, id(open_sans_20), "%.0f", id(pm_2_5).state); | |
it.printf(54, 52, id(open_sans_9), "µg/m³"); | |
it.line(100,19,100,64); | |
it.printf(104,18, id(open_sans_9), "TVOC"); | |
it.printf(104,29, id(open_sans_9), "%.0f", id(voc).state); | |
it.printf(104,41, id(open_sans_9), "NOx"); | |
it.printf(104,52, id(open_sans_9), "%.0f", id(nox).state); | |
- id: clock_page | |
lambda: |- | |
it.printf(0, 0, id(open_sans_11), "%.1f°C", id(temp).state); | |
it.printf(128, 0, id(open_sans_11), TextAlign::TOP_RIGHT, "%.0f% | %.0f% | %.0f% | %.0f%", id(pm_2_5).state, id(co2).state, id(voc).state, id(nox).state); | |
it.line(0,15,128,15); | |
it.strftime(0, 60, id(open_sans_48), TextAlign::BASELINE_LEFT, "%I:%M", id(esptime).now()); | |
- id: blank_page | |
lambda: |- | |
it.printf(0, 0, id(open_sans_14), " "); | |
select: | |
- platform: template | |
name: "Display Page" | |
id: display_page | |
optimistic: true | |
options: | |
- "Off" | |
- "Air Quality" | |
- "Clock" | |
initial_option: "Air Quality" | |
restore_value: true | |
on_value: | |
then: | |
- display.page.show: !lambda |- | |
if (x == "Air Quality") { | |
return id(aq_default_page); | |
} else if (x == "Clock") { | |
return id(clock_page); | |
} else { | |
return id(blank_page); | |
} | |
- component.update: oled_display | |
number: | |
- platform: template | |
# https://esphome.io/components/number/template.html | |
name: "Display Contrast %" | |
icon: "mdi:lightbulb" | |
id: display_contrast | |
min_value: 0 | |
max_value: 100 | |
step: 1 | |
initial_value: 100 | |
optimistic: true | |
restore_value: true | |
mode: slider | |
on_value: | |
then: | |
# https://www.reddit.com/r/Esphome/comments/sy1d1s/how_to_write_a_lamba_to_change_the_contrast_of/ | |
lambda: id(oled_display).set_contrast(id(display_contrast).state / 100.0); | |
- platform: template | |
# https://esphome.io/components/number/template.html | |
name: "LED Brightness %" | |
icon: "mdi:lightbulb" | |
id: led_brightness | |
min_value: 0 | |
max_value: 255 | |
step: 1 | |
initial_value: 100 | |
optimistic: true | |
restore_value: true | |
mode: slider | |
light: | |
- platform: fastled_clockless | |
name: "AQI Light" | |
id: aqi_light | |
chipset: WS2812B | |
pin: GPIO17 | |
num_leds: 8 | |
rgb_order: GRB | |
internal: true |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment