devices.esphome.io
M5Stack AirQ
M5Stack AirQ
Device Type: sensorElectrical Standard: globalDifficulty: Plug-n-flash, 2/5
Product Images
Description
M5Stack AirQ is an integrated, low-power air quality monitoring device designed to provide comprehensive air quality monitoring solutions. It features multiple components, including the SEN55 air quality sensor and the SCD40 CO2 sensor, enabling the monitoring of PM1.0, PM2.5, PM4, PM10 particles, temperature, humidity, VOC, and CO2 concentrations. Powered by the M5stack StampS3 main controller utilizing ESP32-S3FN8 and equipped with 8M Flash memory.
This ESPhome YAML will enable the ability to display some of the sensor data on the integrated paperwhite display, as well as send all of the sensor data to Home Assistant.
The device comes with a 1.54-inch e-ink display with a resolution of 200x200, providing a clear visual representation of the data.
This YAML was adapted from a sample provided by joshblake87 at https://www.reddit.com/r/Esphome/comments/1e2q8jj/m5_stack_airq_air_quality_sensor/
Known Issues
- It doesn't utilize the onboard 450mAh battery.
GPIO Pinout
Pin | Function |
---|---|
GPIO1 | Ink Screen Busy |
GPIO2 | Ink Screen RST |
GPIO3 | Ink Screen D/C |
GPIO4 | Ink Screen CS |
GPIO5 | Ink Screen SCK |
GPIO6 | Ink Screen MOSI |
GPIO8 | Button B |
GPIO9 | beep |
GPIO10 | SEN55 Power Switch |
GPIO11 | SEN55 SDA |
GPIO12 | SEN55 SCL |
GPIO26 | Speaker Pin 2 |
GPIO15 | GROVE A SCL |
GPIO13 | GROVE A SDA |
Example Configuration
# M5Stack AirQ ESPHome Configuration# ------------------------------------------# Before using this configuration:# 1. Update the substitutions section with your values from Home Assistant:# - encryption_key: 32-character key for API encryption# - ota_password: Password for OTA updates# - ap_password: Password for fallback AP mode (optional)# 2. Ensure your secrets.yaml contains:# wifi_ssid: "YOUR_WIFI_SSID"# wifi_password: "YOUR_WIFI_PASSWORD"# 3. Adjust other substitutions as needed (devicename, location, etc)
substitutions: devicename: airq friendlyname: AirQ location: Office sensor_interval: 60s log_level: DEBUG altitude_compensation: "0m" # Local altitude for CO2 sensor temp_offset: -3.0 temp_time_constant: 1200
esphome: name: ${devicename} friendly_name: ${friendlyname} area: ${location} platformio_options: board_build.mcu: esp32s3 board_build.name: "M5Stack StampS3" board_build.upload.flash_size: 8MB board_build.upload.maximum_size: 8388608 board_build.vendor: M5Stack on_boot: - priority: 800 then: - output.turn_on: enable - priority: 200 # na wifi/sensor init then: - script.execute: warmup_refresh
esp32: board: esp32-s3-devkitc-1 #m5stack-stamps3 variant: esp32s3
logger: level: ${log_level}
api:
ota: - platform: esphome
wifi: ssid: !secret wifi_ssid password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails ap: ssid: "${devicename} Fallback Hotspot"
captive_portal:
output: - platform: gpio pin: GPIO10 id: enable
web_server: port: 80 include_internal: true
i2c: sda: GPIO11 scl: GPIO12 scan: true frequency: 50kHz id: bus_a
spi: clk_pin: GPIO05 mosi_pin: GPIO06
time: - platform: homeassistant id: ha_time
- platform: sntp id: sntp_time timezone: Europe/Amsterdam # optioneel: eigen NTP servers # servers: # - 0.nl.pool.ntp.org # - 1.nl.pool.ntp.org
light: - platform: esp32_rmt_led_strip rgb_order: GRB pin: GPIO21 num_leds: 1 chipset: SK6812 name: "LED" restore_mode: RESTORE_AND_OFF id: id_led color_correct: [20%, 20%, 20%]
script: - id: warmup_refresh mode: restart then: - while: condition: lambda: |- return id(uptime_sensor).state < 120; then: - component.update: disp - delay: 1s
text_sensor: - platform: wifi_info ssid: name: SSID id: ssid
- platform: template name: "VOC IAQ Classification" id: iaq_voc icon: "mdi:checkbox-marked-circle-outline" lambda: |- if (int(id(voc).state) < 100.0) { return {"Great"}; } else if (int(id(voc).state) <= 200.0) { return {"Good"}; } else if (int(id(voc).state) <= 300.0) { return {"Light"}; } else if (int(id(voc).state) <= 400.0) { return {"Moderate"}; } else if (int(id(voc).state) <= 500.0) { return {"Heavy"}; } else { return {"unknown"}; }
- platform: template name: "NOX IAQ Classification" id: iaq_nox icon: "mdi:checkbox-marked-circle-outline" lambda: |- if (int(id(nox).state) < 100.0) { return {"Great"}; } else if (int(id(nox).state) <= 200.0) { return {"Good"}; } else if (int(id(nox).state) <= 300.0) { return {"Light"}; } else if (int(id(nox).state) <= 400.0) { return {"Moderate"}; } else if (int(id(nox).state) <= 500.0) { return {"Heavy"}; } else { return {"unknown"}; }
sensor: - platform: uptime name: "Uptime" id: uptime_sensor update_interval: 1s internal: true
- platform: scd4x co2: name: CO2 id: CO2 filters: - lambda: |- float MIN_VALUE = 300.0; float MAX_VALUE = 2500.0; if (MIN_VALUE <= x && x <= MAX_VALUE) return x; else return {}; temperature: name: CO2 Temperature id: CO2_temperature filters: - lambda: |- float MIN_VALUE = -40.0; float MAX_VALUE = 100.0; if (MIN_VALUE <= x && x <= MAX_VALUE) return x; else return {}; humidity: name: CO2 Humidity id: CO2_humidity filters: - lambda: |- float MIN_VALUE = 0.0; float MAX_VALUE = 100.0; if (MIN_VALUE <= x && x <= MAX_VALUE) return x; else return {}; altitude_compensation: ${altitude_compensation} address: 0x62 update_interval: $sensor_interval
- platform: wifi_signal # Reports the WiFi signal strength/RSSI in dB name: "Wifi Signal dB" id: wifi_signal_db update_interval: 60s entity_category: "diagnostic"
- platform: sen5x id: sen55 pm_1_0: name: "PM 1" id: PM1_0 accuracy_decimals: 2 pm_2_5: name: "PM 2.5" id: PM2_5 accuracy_decimals: 2 pm_4_0: name: "PM 4" id: PM4_0 accuracy_decimals: 2 pm_10_0: name: "PM 10" id: PM10_0 accuracy_decimals: 2 temperature: name: "SEN55 Temperature" id: sen55_temperature accuracy_decimals: 2 humidity: name: "SEN55 Humidity" id: sen55_humidity accuracy_decimals: 2 voc: name: VOC id: voc accuracy_decimals: 2 algorithm_tuning: index_offset: 100 learning_time_offset_hours: 12 learning_time_gain_hours: 12 gating_max_duration_minutes: 180 std_initial: 50 gain_factor: 230 nox: name: NOX id: nox accuracy_decimals: 2 algorithm_tuning: index_offset: 100 learning_time_offset_hours: 12 learning_time_gain_hours: 12 gating_max_duration_minutes: 180 std_initial: 50 gain_factor: 230 temperature_compensation: offset: ${temp_offset} normalized_offset_slope: 0 time_constant: ${temp_time_constant} acceleration_mode: low store_baseline: true address: 0x69 update_interval: $sensor_interval
- platform: template name: Temperature id: temperature lambda: |- return (( id(sen55_temperature).state + id(CO2_temperature).state ) / 2 ) - id(temperature_offset).state; unit_of_measurement: "°C" icon: "mdi:thermometer" device_class: "temperature" state_class: "measurement" update_interval: $sensor_interval accuracy_decimals: 2
- platform: template name: Humidity id: humidity lambda: |- return (( id(sen55_humidity).state + id(CO2_humidity).state ) / 2) - id(humidity_offset).state; unit_of_measurement: "%" icon: "mdi:water-percent" device_class: "humidity" state_class: "measurement" update_interval: $sensor_interval accuracy_decimals: 2
binary_sensor: - platform: gpio name: Button A pin: number: GPIO0 ignore_strapping_warning: true mode: input: true inverted: true on_press: then: - component.update: disp
- platform: gpio pin: number: GPIO08 # ignore_strapping_warning: true mode: input: true pullup: true inverted: true name: Button B
- platform: gpio pin: number: GPIO46 ignore_strapping_warning: true name: Button Hold
- platform: gpio pin: number: GPIO42 # ignore_strapping_warning: true name: Button Power
button: - platform: restart name: Restart
- platform: template name: "CO2 Force Manual Calibration" entity_category: "config" on_press: then: - scd4x.perform_forced_calibration: value: !lambda "return id(co2_cal).state;"
- platform: template name: "SEN55 Force Manual Clean" entity_category: "config" on_press: then: - sen5x.start_fan_autoclean: sen55
number: - platform: template name: "CO2 Calibration Value" optimistic: true min_value: 400 max_value: 1000 step: 5 id: co2_cal icon: "mdi:molecule-co2" entity_category: "config"
- platform: template name: Humidity Offset id: humidity_offset restore_value: true initial_value: 0.0 min_value: -70.0 max_value: 70.0 entity_category: "CONFIG" unit_of_measurement: "%" optimistic: true update_interval: never step: 0.1 mode: box
- platform: template name: Temperature Offset id: temperature_offset restore_value: true initial_value: 0.0 min_value: -70.0 max_value: 70.0 entity_category: "CONFIG" unit_of_measurement: "°C" optimistic: true update_interval: never step: 0.1 mode: box
display: - platform: waveshare_epaper model: 1.54inv2 id: disp cs_pin: GPIO04 dc_pin: GPIO03 reset_pin: GPIO02 busy_pin: number: GPIO01 inverted: false full_update_every: 5 reset_duration: 2ms update_interval: $sensor_interval lambda: |- // 1) Warming up check: eerste 120 s enkel melding tonen if (id(uptime_sensor).state < 120) { it.fill(COLOR_OFF); it.printf(it.get_width()/2, it.get_height()/2 - 16, id(f18), COLOR_ON, TextAlign::CENTER, "Warming up sensors"); it.printf(it.get_width()/2, it.get_height()/2 + 16, id(f16), COLOR_ON, TextAlign::CENTER, "Waiting for 2 minutes"); return; }
// 1) GRID lijnen // Verticale verdeellijn it.line(100, 0, 100, 200); // Horizontale bovenste scheiding //it.line(0, 50, 200, 0); // Middenlijn onder Sen55 alleen rechts (x ≥ 100) // it.line(100,120,200,120); // Onderste scheiding it.line(it.get_width()-1, 0, it.get_width()-1, it.get_height());
// 2) Vakken // SCD40 linksonder, zonder onderrand it.line(0, 50, 0, 160); // linkerkant it.line(0, 50, 100, 50); // bovenkant //it.line(100, 50, 100,160); // rechterkant // Sen55 rechtsboven (0→120) it.rectangle(100, 0, 100, 160); // Wi-Fi rechtsonder (160→200) //it.rectangle(100,160,100, 40); // Optioneel: vak linksonder voor je logo it.rectangle(0, 160,100, 40);
// KLOK & DATUM (kleiner font) auto t = id(sntp_time).now(); char buf[20]; snprintf(buf, sizeof(buf), "%02d:%02d", t.hour, t.minute); it.printf(2, 0, id(f24), COLOR_ON, TextAlign::TOP_LEFT, "%s", buf); t.strftime(buf, sizeof(buf), "%Y-%m-%d"); it.printf(2, 30, id(f12), COLOR_ON, TextAlign::TOP_LEFT, "%s", buf);
// SCD40 LINKS it.printf(5, 52, id(f16), COLOR_ON, TextAlign::TOP_LEFT, "SCD40"); it.printf(5, 75, id(f12), COLOR_ON, TextAlign::TOP_LEFT, "Co2:"); it.printf(90,75, id(f16), COLOR_ON, TextAlign::TOP_RIGHT, "%.0f", id(CO2).state); it.printf(5, 95, id(f12), COLOR_ON, TextAlign::TOP_LEFT, "Temp:"); it.printf(90,95, id(f16), COLOR_ON, TextAlign::TOP_RIGHT, "%.1f", id(temperature).state); it.printf(5,115, id(f12), COLOR_ON, TextAlign::TOP_LEFT, "Humid:"); it.printf(90,115, id(f16), COLOR_ON, TextAlign::TOP_RIGHT, "%.1f", id(humidity).state);
// SEN55 RECHTS it.printf(105,5, id(f16), COLOR_ON, TextAlign::TOP_LEFT, "SEN55"); const char* labels[] = {"PM1.0:","PM2.5:","PM4.0:","PM10:","VOC:","NOX:"}; float vals[] = { id(PM1_0).state, id(PM2_5).state, id(PM4_0).state, id(PM10_0).state, id(voc).state, id(nox).state }; for(int i = 0; i < 6; i++) { int y = 25 + i * 20; it.printf(105, y, id(f12), COLOR_ON, TextAlign::TOP_LEFT, labels[i]); it.printf(190, y, id(f16), COLOR_ON, TextAlign::TOP_RIGHT, i < 4 ? "%.1f" : "%.0f", vals[i]); }
// 6) Wi-Fi (rechtsonder, y=160→200) it.printf(105,161, id(f16), COLOR_ON, TextAlign::TOP_LEFT, "WIFI"); it.printf(105,180, id(f12), COLOR_ON, TextAlign::TOP_LEFT, "%s", id(ssid).state.c_str());
// 7) Logo of friendlyname (linksonder) it.filled_rectangle(1, 161, 98, 39, COLOR_ON); it.print(50, 180, id(f18), COLOR_OFF, TextAlign::CENTER, "${friendlyname}");
font: - file: type: gfonts family: Noto Sans Display weight: 500 glyphs: [ "&", "@", "!", ",", ".", '"', "%", "(", ")", "+", "-", "_", ":", "°", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", " ", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "å", "ä", "ö", "/", "µ", "³", "’", ] id: f16 size: 16 - file: type: gfonts family: Noto Sans Display weight: 500 glyphs: [ "&", "@", "!", ",", ".", '"', "%", "(", ")", "+", "-", "_", ":", "°", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", " ", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "å", "ä", "ö", "/", "µ", "³", "’", ] id: f18 size: 18 - file: type: gfonts family: Noto Sans Display weight: 500 id: f12 size: 12 glyphs: [ "&", "@", "!", ",", ".", '"', "%", "(", ")", "+", "-", "_", ":", "°", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", " ", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "å", "ä", "ö", "/", "µ", "³", "’", ] - file: type: gfonts family: Noto Sans Display weight: 500 id: f24 size: 24 glyphs: [ "&", "@", "!", ",", ".", '"', "%", "(", ")", "+", "-", "_", ":", "°", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", " ", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "å", "ä", "ö", "/", "µ", "³", "’", ] - file: type: gfonts family: Noto Sans Display weight: 500 id: f36 size: 36 glyphs: [ "&", "@", "!", ",", ".", '"', "%", "(", ")", "+", "-", "_", ":", "°", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", " ", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "å", "ä", "ö", "/", "µ", "³", "’", ] - file: type: gfonts family: Noto Sans Display weight: 500 id: f48 size: 48 glyphs: [ "&", "@", "!", ",", ".", '"', "%", "(", ")", "+", "-", "_", ":", "°", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", " ", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "å", "ä", "ö", "/", "µ", "³", "’", ] - file: type: gfonts family: Noto Sans Display weight: 500 id: f32 size: 32 glyphs: [ "&", "@", "!", ",", ".", '"', "%", "(", ")", "+", "-", "_", ":", "°", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", " ", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "å", "ä", "ö", "/", "µ", "³", "’", ]
- file: type: gfonts family: Noto Sans Display weight: 500 id: f64 size: 64 glyphs: [ "&", "@", "!", ",", ".", '"', "%", "(", ")", "+", "-", "_", ":", "°", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", " ", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "å", "ä", "ö", "/", "µ", "³", "’", ]
- file: type: gfonts family: Noto Sans Display weight: 800 id: f64b size: 64 glyphs: [ "&", "@", "!", ",", ".", '"', "%", "(", ")", "+", "-", "_", ":", "°", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", " ", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "å", "ä", "ö", "/", "µ", "³", "’", ]
- file: type: gfonts family: Noto Sans Display weight: 800 id: f55b size: 55 glyphs: [ "&", "@", "!", ",", ".", '"', "%", "(", ")", "+", "-", "_", ":", "°", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", " ", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "å", "ä", "ö", "/", "µ", "³", "’", ]
- file: type: gfonts family: Material Symbols Sharp weight: 400 id: font_weather_icons_xsmall size: 20 glyphs: - "\U0000F159" # clear-night - "\U0000F15B" # cloudy - "\U0000F172" # partlycloudy - "\U0000E818" # fog - "\U0000F67F" # hail - "\U0000EBDB" # lightning, lightning-rainy - "\U0000F61F" # pouring - "\U0000F61E" # rainy - "\U0000F61C" # snowy - "\U0000F61D" # snowy-rainy - "\U0000E81A" # sunny - "\U0000EFD8" # windy, windy-variant - "\U0000F7F3" # exceptional - file: type: gfonts family: Material Symbols Sharp weight: 400 id: font_weather_icons_small size: 32 glyphs: - "\U0000F159" # clear-night - "\U0000F15B" # cloudy - "\U0000F172" # partlycloudy - "\U0000E818" # fog - "\U0000F67F" # hail - "\U0000EBDB" # lightning, lightning-rainy - "\U0000F61F" # pouring - "\U0000F61E" # rainy - "\U0000F61C" # snowy - "\U0000F61D" # snowy-rainy - "\U0000E81A" # sunny - "\U0000EFD8" # windy, windy-variant - "\U0000F7F3" # exceptional
- file: type: gfonts family: Open Sans weight: 700 id: font_clock glyphs: "0123456789:" size: 70 - file: type: gfonts family: Open Sans weight: 700 id: font_clock_big glyphs: "0123456789:" size: 100 - file: "gfonts://Roboto" id: font_temp size: 28 - file: type: gfonts family: Open Sans weight: 500 id: font_small size: 30 glyphs: '!"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz»' - file: type: gfonts family: Open Sans weight: 500 id: font_medium size: 45 glyphs: '!"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz»' - file: type: gfonts family: Open Sans weight: 300 id: font_xsmall size: 16 glyphs: '!"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz»'