Skip to content

Instantly share code, notes, and snippets.

@bruvv
Created August 6, 2025 19:04
Show Gist options
  • Save bruvv/9e54aa40410d99ecc8e3ea7e15305066 to your computer and use it in GitHub Desktop.
Save bruvv/9e54aa40410d99ecc8e3ea7e15305066 to your computer and use it in GitHub Desktop.
M5 AirQ esphome
substitutions:
devicename: lounge-airq
friendlyname: Lounge AirQ
location: Lounge
sensor_interval: 10s
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: 800
then:
- pcf8563.read_time
esp32:
board: esp32-s3-devkitc-1 #m5stack-stamps3
variant: esp32s3
framework:
type: arduino
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: REDACTED
ota:
- platform: esphome
password: REDACTED
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Lounge-Airq Fallback Hotspot"
password: REDACTED
captive_portal:
output:
- platform: gpio
pin: GPIO10
id: enable
web_server:
port: 80
include_internal: true
i2c:
sda: GPIO11
scl: GPIO12
scan: true
frequency: 100kHz
id: bus_a
spi:
clk_pin: GPIO05
mosi_pin: GPIO06
time:
- platform: pcf8563
address: 0x51
update_interval: 10min
- platform: homeassistant
id: esptime
light:
- platform: esp32_rmt_led_strip
rgb_order: GRB
pin: GPIO21
num_leds: 1
rmt_channel: 0
chipset: SK6812
name: "LED"
restore_mode: ALWAYS_OFF
id: id_led
text_sensor:
- platform: wifi_info
ip_address:
name: IP
ssid:
name: SSID
bssid:
name: BSSID
mac_address:
name: MAC
dns_address:
name: DNS
- 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: 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: 0m
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: 0
normalized_offset_slope: 0
time_constant: 0
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
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
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: 6
reset_duration: 2ms
update_interval: 10s
lambda: |-
auto now = id(esptime).now().strftime("%H:%M %d/%m/%y").c_str();
it.printf(it.get_width()/2, 0, id(f12), TextAlign::TOP_CENTER, "${location} @ %s", now);
it.print(0, 23, id(f24), TextAlign::TOP_LEFT, "PM 1: ");
it.print(0, 48, id(f24), TextAlign::TOP_LEFT, "PM 2.5: ");
it.print(0, 73, id(f24), TextAlign::TOP_LEFT, "PM 4: ");
it.print(0, 98, id(f24), TextAlign::TOP_LEFT, "PM 10: ");
it.print(0, 123, id(f24), TextAlign::TOP_LEFT, "CO2: ");
it.print(0, 148, id(f24), TextAlign::TOP_LEFT, "VOC: ");
it.print(0, 173, id(f24), TextAlign::TOP_LEFT, "NOX: ");
it.printf(it.get_width(), 23, id(f24), TextAlign::TOP_RIGHT, "%.0f", id(PM1_0).state);
it.printf(it.get_width(), 48, id(f24), TextAlign::TOP_RIGHT, "%.0f", id(PM2_5).state);
it.printf(it.get_width(), 73, id(f24), TextAlign::TOP_RIGHT, "%.0f", id(PM4_0).state);
it.printf(it.get_width(), 98, id(f24), TextAlign::TOP_RIGHT, "%.0f", id(PM10_0).state);
it.printf(it.get_width(), 123, id(f24), TextAlign::TOP_RIGHT, "%.0fppm", id(CO2).state);
it.printf(it.get_width(), 148, id(f24), TextAlign::TOP_RIGHT, "%.0f", id(voc).state);
it.printf(it.get_width(), 173, id(f24), TextAlign::TOP_RIGHT, "%.0f", id(nox).state);
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→»"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment