Skip to content

Instantly share code, notes, and snippets.

@ali-essam
Created August 16, 2024 20:19
Show Gist options
  • Save ali-essam/14b8ae7eaeef6f6dcfe354b749c4934b to your computer and use it in GitHub Desktop.
Save ali-essam/14b8ae7eaeef6f6dcfe354b749c4934b to your computer and use it in GitHub Desktop.
# 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