devices.esphome.io

AirGradient-One

AirGradient-One

Device Type: sensor
Electrical Standard: us
Board: esp32

The AirGradient ONE monitor measures PM, CO2, TVOC, NOx, temperature and relative humidity from AirGradient.

If you have multiple of these devices, you will likely need to make each sensor name unique across the boards (e.g. "AirGradient Temperature 1"), so there aren't naming conflicts.

Basic Configuration

# AirGradient ONE - Board v9
# https://www.airgradient.com/open-airgradient/instructions/overview/
#
# This configuration was blatantly yoinked from:
# https://github.com/MallocArray/airgradient_esphome/blob/main/airgradient-one.yaml
# Needs ESPHome 2023.7.0 or later
# Reference for substitutions: https://github.com/ajfriesen/ESPHome-AirGradient/blob/main/air-gradient-pro-diy.yaml
substitutions:
devicename: !secret name
ag_esphome_config_version: 0.1.0
led_strip_brightness: "25%"
esphome:
name: "${devicename}"
esp32:
board: esp32-c3-devkitm-1
# Disable logging
# https://esphome.io/components/logger.html
logger:
baud_rate: 0 # Must disable serial logging as ESP32-C3 only has 2 hardware UART and both are in use
logs:
component: ERROR # Hiding warning messages about component taking a long time https://github.com/esphome/issues/issues/4717
# Enable Home Assistant API
api:
encryption:
key: !secret api_encryption_key
ota:
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
web_server:
port: 9926
version: 1
# Create a switch for safe_mode in order to flash the device
# Solution from this thread:
# https://community.home-assistant.io/t/esphome-flashing-over-wifi-does-not-work/357352/1
switch:
- platform: safe_mode
name: "Flash Mode (Safe Mode)"
icon: "mdi:cellphone-arrow-down"
- platform: template
name: "Display Temperature in °F"
icon: "mdi:thermometer"
id: display_in_f
restore_mode: RESTORE_DEFAULT_ON
optimistic: True
uart:
# https://esphome.io/components/uart.html#uart
- rx_pin: GPIO0 # Pin 12
tx_pin: GPIO1 # Pin 13
baud_rate: 9600
id: senseair_s8_uart
- rx_pin: GPIO20 # Pin 30 or RX
tx_pin: GPIO21 # Pin 31, or TX
baud_rate: 9600
id: pms5003_uart
i2c:
# https://esphome.io/components/i2c.html
sda: GPIO7
scl: GPIO6
frequency: 400kHz # 400kHz eliminates warnings about components taking a long time other than SGP40 component: https://github.com/esphome/issues/issues/4717
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
on_value:
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
id(pm_2_5_aqi).publish_state((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
id(pm_2_5_aqi).publish_state((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
id(pm_2_5_aqi).publish_state((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
id(pm_2_5_aqi).publish_state((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
id(pm_2_5_aqi).publish_state((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
id(pm_2_5_aqi).publish_state((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
id(pm_2_5_aqi).publish_state((500.0 - 401.0) / (500.4 - 350.5) * (id(pm_2_5).state - 350.5) + 401.0);
} else {
id(pm_2_5_aqi).publish_state(500);
}
pm_1_0:
name: "PM 1.0"
id: pm_1_0
pm_10_0:
name: "PM 10.0"
id: pm_10_0
pm_0_3um:
name: "PM 0.3"
id: pm_0_3um
update_interval: 2min
- platform: template
name: "PM 2.5 AQI"
unit_of_measurement: "AQI"
icon: "mdi:air-filter"
accuracy_decimals: 0
id: pm_2_5_aqi
- platform: senseair
# SenseAir S8 https://esphome.io/components/sensor/senseair.html
# https://senseair.com/products/size-counts/s8-lp/
co2:
name: "SenseAir S8 CO2"
id: co2
filters:
- skip_initial: 2
- clamp:
min_value: 400 # 419 as of 2023-06 https://gml.noaa.gov/ccgg/trends/global.html
on_value:
- if:
condition:
lambda: 'return id(co2).state < 800;'
then:
- light.turn_on:
id: led_strip
brightness: "${led_strip_brightness}"
red: 0%
green: 100%
blue: 0%
- if:
condition:
lambda: 'return id(co2).state >= 800 && id(co2).state < 1000;'
then:
- light.turn_on:
id: led_strip
brightness: "${led_strip_brightness}"
red: 100%
green: 100%
blue: 0%
- if:
condition:
lambda: 'return id(co2).state >= 1000 && id(co2).state < 1500;'
then:
- light.turn_on:
id: led_strip
brightness: "${led_strip_brightness}"
red: 100%
green: 50%
blue: 0%
- if:
condition:
lambda: 'return id(co2).state >= 1500 && id(co2).state < 2000;'
then:
- light.turn_on:
id: led_strip
brightness: "${led_strip_brightness}"
red: 100%
green: 0%
blue: 0%
- if:
condition:
lambda: 'return id(co2).state >= 2000 && id(co2).state < 3000;'
then:
- light.turn_on:
id: led_strip
brightness: "${led_strip_brightness}"
red: 60%
green: 0%
blue: 60%
- if:
condition:
lambda: 'return id(co2).state >= 3000 && id(co2).state < 10000;'
then:
- light.turn_on:
id: led_strip
brightness: "${led_strip_brightness}"
red: 40%
green: 0%
blue: 0%
id: senseair_s8
uart_id: senseair_s8_uart
- platform: sht4x
# SHT40 https://esphome.io/components/sensor/sht4x.html
temperature:
name: "Temperature"
id: temp
humidity:
name: "Humidity"
id: humidity
address: 0x44
- platform: wifi_signal
name: "WiFi Signal"
id: wifi_dbm
update_interval: 60s
- platform: uptime
name: "Uptime"
id: device_uptime
update_interval: 10s
- platform: sgp4x
# SGP41 https://esphome.io/components/sensor/sgp4x.html
voc:
name: "VOC Index"
id: voc
nox:
name: "NOx Index"
id: nox
compensation: # Remove this block if no temp/humidity sensor present for compensation
temperature_source: temp
humidity_source: humidity
font:
# Font to use on the display
# Open Source font Liberation Sans by Red Hat
# https://www.dafont.com/liberation-sans.font
# - file: "./fonts/liberation_sans/LiberationSans-Regular.ttf"
- file:
type: gfonts
family: Poppins
weight: light
id: poppins_light
size: 14
glyphs: '!"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz/µ³'
- file:
type: gfonts
family: Poppins
weight: light
id: poppins_light_12
size: 12
glyphs: '!"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz/µ³'
- file: "gfonts://Ubuntu Mono"
id: ubuntu
size: 22
glyphs: '!"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz/µ³'
display:
# https://esphome.io/components/display/ssd1306.html
# Formatting reference: https://www.tutorialspoint.com/c_standard_library/c_function_printf.htm
- platform: ssd1306_i2c
model: "SH1106 128x64"
id: oled_display
address: 0x3C
# rotation: 180°
pages:
- id: summary1
lambda: |-
it.printf(0, 0, id(poppins_light), "CO2:");
it.printf(128, 0, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f ppm", id(co2).state);
it.printf(0, 16, id(poppins_light), "PM2.5:");
it.printf(128, 16, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f µg/m³", id(pm_2_5).state);
it.printf(0, 32, id(poppins_light), "Temp:");
if (id(display_in_f).state) {
it.printf(128, 32, id(poppins_light), TextAlign::TOP_RIGHT, "%.1f°F", id(temp).state*9/5+32);
} else {
it.printf(128, 32, id(poppins_light), TextAlign::TOP_RIGHT, "%.1f°C", id(temp).state);
}
it.printf(0, 48, id(poppins_light), "Humidity:");
it.printf(128, 48, id(poppins_light), TextAlign::TOP_RIGHT, "%.1f%%", id(humidity).state);
- id: summary2
lambda: |-
it.printf(0, 0, id(poppins_light), "CO2:");
it.printf(128, 0, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f ppm", id(co2).state);
it.printf(0, 16, id(poppins_light), "PM2.5:");
it.printf(128, 16, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f µg/m³", id(pm_2_5).state);
it.printf(0, 32, id(poppins_light), "VOC:");
it.printf(128, 32, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f", id(voc).state);
it.printf(0, 48, id(poppins_light), "NOx:");
it.printf(128, 48, id(poppins_light), TextAlign::TOP_RIGHT, "%.0f", id(nox).state);
- id: boot
lambda: |-
it.printf(0, 0, id(poppins_light), "ID:");
it.printf(128, 0, id(poppins_light), TextAlign::TOP_RIGHT, "%s", get_mac_address().c_str());
it.printf(0, 21, id(poppins_light), "Config Ver: $ag_esphome_config_version");
it.printf(0, 42, id(poppins_light), "$devicename");
on_page_change:
to: boot
then:
- if:
# Skip the boot page after initial boot
condition:
lambda: 'return id(device_uptime).state > 30;'
then:
- display.page.show_next: oled_display
- component.update: oled_display
button:
# https://github.com/esphome/issues/issues/2444
- platform: template
name: SenseAir S8 Calibration
id: senseair_s8_calibrate_button
on_press:
then:
- senseair.background_calibration: senseair_s8
- delay: 70s
- senseair.background_calibration_result: senseair_s8
- platform: template
name: SenseAir S8 Enable Automatic Calibration
id: senseair_s8_enable_calibrate_button
on_press:
then:
- senseair.abc_enable: senseair_s8
- platform: template
name: SenseAir S8 Disable Automatic Calibration
id: senseair_s8_disable_calibrate_button
on_press:
then:
- senseair.abc_disable: senseair_s8
output:
- platform: gpio
# Watchdog to reboot if no activity
id: watchdog
pin: GPIO2
light:
# https://esphome.io/components/light/esp32_rmt_led_strip.html
- platform: esp32_rmt_led_strip
color_correct: [50%,50%,50%]
rgb_order: GRB
pin: GPIO10 # Pin 16
num_leds: 11
rmt_channel: 0
chipset: ws2812
name: "LED Strip"
id: led_strip
interval:
- interval: 30s
# Notify watchdog device is still alive
then:
- output.turn_on: watchdog
- delay: 20ms
- output.turn_off: watchdog
- interval: 5s
# Automatically switch to the next page every five seconds
then:
- if:
# Show boot screen for first 10 seconds with serial number and config version
condition:
lambda: 'return id(device_uptime).state < 10;'
then:
- display.page.show: boot
- lambda: id(device_uptime).set_update_interval(1);
else:
# Change page on display
- display.page.show_next: oled_display
- component.update: oled_display
Edit this page on GitHub