Hi All,
I am not a coder, and I am trying to get a grasp of this.
I built an Air Quality sensor for my Garage to monitor 3D printing emissions.
I used this rather excellent video I made it easier to measure your 3D printer’s emissions!, and I can see it and all the sensors in HA. But the screen is just white.
I can see it within the ESP Builder.
Whilst doing some research, it may be a pin configuration issue, but I am unsure how to resolve this.
From HA, I can access the Web UI
I can also toggle the Backlight on/off from the WebUI.
The Script can be downloaded from 3D Printables. I am also the latest PCB from the project.
I’ve also added it below.
# Sensorbox v2 config for PCB SBR1
# config r2 (20241130)
# https://go.toms3d.org/sbr1
# Leave the "substitutions" block in place, then add this config.
# You can choose a new "friendly name" in the substitutions, this will be its display name in Home Assistant.
esphome:
name: ${name}
friendly_name: ${friendly_name}
min_version: 2024.6.0
name_add_mac_suffix: false
platformio_options:
board_build.flash_mode: dio
project:
name: esphome.web
version: dev
esp32:
board: esp32-s2-saola-1
framework:
type: arduino
# Enable logging
logger:
# Enable Home Assistant API and failsafe mechanisms
api:
# Allow Over-The-Air updates
ota:
- platform: esphome
# Allow provisioning Wi-Fi via serial
improv_serial:
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails, using your wifi's password
ap:
ssid: "Sensorbox v2"
password: !secret wifi_password
# In combination with the `ap` this allows the user
# to provision wifi credentials to the device via WiFi AP.
captive_portal:
dashboard_import:
package_import_url: github://esphome/firmware/esphome-web/esp32s2.yaml@main
import_full_config: true
# To have a "next url" for improv serial
web_server:
debug:
update_interval: 5s
# Display dimming
globals:
- id: dbright
type: int
restore_value: no
initial_value: '0'
output:
- platform: ledc
pin: GPIO15
id: backlight_pwm
light:
- platform: monochromatic
output: backlight_pwm
name: "Display Backlight"
id: back_light
restore_mode: ALWAYS_ON
binary_sensor:
- platform: gpio
pin:
number: GPIO13
mode:
input: true
pullup: true
inverted: true
id: top_btn
filters:
- delayed_on: 10ms
- delayed_off: 500ms
on_press:
then:
- lambda: |-
switch(id(dbright)){
case 0: id(dbright) = 3; id(back_light).turn_on().set_brightness(1.00).perform(); return;
case 1: id(dbright) = 0; id(back_light).turn_off().perform(); return;
case 2: id(dbright) = 1; id(back_light).turn_on().set_brightness(0.05).perform(); return;
case 3: id(dbright) = 2; id(back_light).turn_on().set_brightness(0.33).perform(); return;
}
# /Display dimming
font:
- file: "gfonts://Rubik@300"
id: font_label_14
size: 14
bpp: 4
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: "gfonts://Rubik@400"
id: font_value_30
size: 30
bpp: 4
glyphs: [
"0","1","2","3","4","5","6","7","8","9", "."
]
- file: "gfonts://Orbitron"
id: font_heading_36
size: 36
bpp: 4
glyphs: [
"2",C,O,P,V,a,c,e,i,l,r,s,t
]
color:
- id: white
hex: FFFFFF
- id: grey
hex: AAAAAA
- id: light_grey
hex: CCCCCC
- id: yellow
hex: FFFA72
- id: orange
hex: FF9C32
- id: red
hex: FF1616
- id: purple
hex: FF16e0
display:
- platform: ili9xxx
model: ILI9341
cs_pin: GPIO03
dc_pin: GPIO04
reset_pin: GPIO06
#data_rate: 10MHz
transform:
#mirror_x: true
mirror_y: true
#show_test_card: true
color_order: bgr # CHANGE THIS if your screen has colors mapped wrong
invert_colors: false
lambda: |-
it.fill(Color::BLACK);
// Please forgive me. I know this is horrible spaghetti code, but for now it gets the job done. Will eventually move all this to LVGL.
;
auto cy = 0; // current Y position
auto ix = 0; // current X grid position
auto fv30 = id(font_value_30);
auto fh36 = id(font_heading_36);
auto fl14 = id(font_label_14);
;
auto w = it.get_width();
auto h = it.get_height();
;
auto col = id(white);
;
auto co2 = id(co2_scd40).state;
auto e_co2 = id(eco2_ens160).state;
auto s_co2 = id(eco2_sgp30).state;
auto ch2o = id(ch2o_ze08).state;
auto e_voc = id(tvoc_ens160).state;
auto s_voc = id(tvoc_sgp30).state;
auto pm1 = id(pm1_pms).state;
auto pm25 = id(pm25_pms).state;
auto pm10 = id(pm10_pms).state;
auto aqi_voc = id(aqi_voc_sgp41).state;
auto aqi_nox = id(aqi_nox_sgp41).state;
;
;
;
// SET COLOR FOR EACH READING
;
col = id(white);
if (co2 > 800){col = id(yellow);}
if (co2 > 1200){col = id(orange);}
if (co2 > 1500){col = id(red);}
if (co2 > 1800){col = id(purple);}
auto co2_c = col;
;
col = id(white);
if (e_co2 > 800){col = id(yellow);}
if (e_co2 > 1200){col = id(orange);}
if (e_co2 > 1500){col = id(red);}
if (e_co2 > 1800){col = id(purple);}
auto e_co2_c = col;
;
col = id(white);
if (s_co2 > 800){col = id(yellow);}
if (s_co2 > 1200){col = id(orange);}
if (s_co2 > 1500){col = id(red);}
if (s_co2 > 1800){col = id(purple);}
auto s_co2_c = col;
;
col = id(white);
if (ch2o > 60){col = id(yellow);}
if (ch2o > 100){col = id(orange);}
if (ch2o > 150){col = id(red);}
if (ch2o > 200){col = id(purple);}
auto ch2o_c = col;
;
col = id(white);
if (e_voc > 400){col = id(yellow);}
if (e_voc > 800){col = id(orange);}
if (e_voc > 1400){col = id(red);}
if (e_voc > 2000){col = id(purple);}
auto e_voc_c = col;
;
col = id(white);
if (s_voc > 300){col = id(yellow);}
if (s_voc > 600){col = id(orange);}
if (s_voc > 1000){col = id(red);}
if (s_voc > 1500){col = id(purple);}
auto s_voc_c = col;
;
col = id(white);
if (pm1 > 5){col = id(yellow);}
if (pm1 > 10){col = id(orange);}
if (pm1 > 15){col = id(red);}
if (pm1 > 25){col = id(purple);}
auto pm1_c = col;
;
col = id(white);
if (pm25 > 5){col = id(yellow);}
if (pm25 > 10){col = id(orange);}
if (pm25 > 15){col = id(red);}
if (pm25 > 25){col = id(purple);}
auto pm25_c = col;
;
col = id(white);
if (pm10 > 15){col = id(yellow);}
if (pm10 > 45){col = id(orange);}
if (pm10 > 60){col = id(red);}
if (pm10 > 80){col = id(purple);}
auto pm10_c = col;
;
;
// SELECT BEST AVAILABLE TEMPERATURE SENSOR
float temps[5] = {id(temp_sht40).state, id(temp_aht20).state, id(temp_scd40).state, id(temp_aht20_ens).state, id(temp_bmp280).state};
float hums[4] = {id(hum_sht40).state, id(hum_aht20).state, id(hum_scd40).state, id(hum_aht20_ens).state};
float temp = 0;
float hum = 0;
for(int i = 0; i < 5; i++){
if(!isnan(temps[i])){temp = temps[i]; break;}
}
for(int i = 0; i < 4; i++){
if(!isnan(hums[i])){hum = hums[i]; break;}
}
;
// DRAW HUMITEMP
it.printf(90, 30, fl14, id(grey), TextAlign::BOTTOM_LEFT, "°C");
it.printf(90, 34, fv30, white, display::TextAlign::BOTTOM_RIGHT, "%.1f", temp);
it.printf(197, 18, fl14, id(grey), TextAlign::BOTTOM_LEFT, "RH");
it.print(197, 30, fl14, id(grey), TextAlign::BOTTOM_LEFT, "\%");
it.printf(197, 34, fv30, white, display::TextAlign::BOTTOM_RIGHT, "%.1f", hum);
;
;
// PREPARE VARIABLES
auto title = "";
auto unit = "";
char *names[3];
float values[3];
esphome::Color colors[3];
int count = 0;
;
;
// PREPARE PARTICLES
cy = 30;
;
if (!isnan(pm1)){
count = 0;
title = "Particles";
unit = "µg/m³";
names[0] = "PM1";
names[1] = "PM2.5";
names[2] = "PM10"; //god C++ sucks
values[0] = pm1;
values[1] = pm25;
values[2] = pm10;
colors[0] = pm1_c;
colors[1] = pm25_c;
colors[2] = pm10_c;
;
;
// START REUSABLE
it.line(0,cy,w,cy,id(white));
it.print(2, cy + 22, fh36, id(light_grey), TextAlign::CENTER_LEFT, title);
it.print(w-2, cy + 31, fl14, id(grey), TextAlign::CENTER_RIGHT, unit);
ix = 0;
for (int i = 0; i < 3; i++){
if(isnan(values[i])){continue;}
it.printf(w/6*(1+ix*2), cy + 58, fl14, id(grey), TextAlign::BOTTOM_CENTER, "%s", names[i]);
it.printf(w/6*(1+ix*2), cy + 50, fv30, colors[i], display::TextAlign::TOP_CENTER, "%.0f", values[i]);
ix++;
}
// END REUSABLE
;
cy += 88;
}
;
// END PARTICLES
;
// PREPARE CO2
;
if (!isnan(co2) or !isnan(e_co2) or !isnan(s_co2)){
count = 0;
title = "CO2";
unit = "ppm";
names[0] = "SCD40";
names[1] = "ENS160 est.";
names[2] = "SGP30 est.";
values[0] = co2;
values[1] = e_co2;
values[2] = s_co2;
colors[0] = co2_c;
colors[1] = e_co2_c;
colors[2] = s_co2_c;
;
;
// START REUSABLE
it.line(0,cy,w,cy,id(white));
it.print(2, cy + 22, fh36, id(light_grey), TextAlign::CENTER_LEFT, title);
it.print(w-2, cy + 31, fl14, id(grey), TextAlign::CENTER_RIGHT, unit);
ix = 0;
for (int i = 0; i < 3; i++){
if(isnan(values[i])){continue;}
it.printf(w/6*(1+ix*2), cy + 58, fl14, id(grey), TextAlign::BOTTOM_CENTER, "%s", names[i]);
it.printf(w/6*(1+ix*2), cy + 50, fv30, colors[i], display::TextAlign::TOP_CENTER, "%.0f", values[i]);
ix++;
}
// END REUSABLE
;
;
cy += 88;
}
;
// END CO2
;
// PREPARE VOC
;
bool draw_general_voc = (!isnan(ch2o) or !isnan(e_voc) or !isnan(s_voc));
bool draw_sgp4x = (!isnan(aqi_voc) or !isnan(aqi_nox));
if (draw_general_voc or draw_sgp4x){
count = 0;
title = "VOC";
unit = "ppb";
names[0] = "SGP30";
names[1] = "ENS160";
names[2] = "ZE08-CH2O";
values[0] = s_voc;
values[1] = e_voc;
values[2] = ch2o;
colors[0] = s_voc_c;
colors[1] = e_voc_c;
colors[2] = ch2o_c;
;
;
// START REUSABLE | UNIT CONDITION ADDED
it.line(0,cy,w,cy,id(white));
it.print(2, cy + 22, fh36, id(light_grey), TextAlign::CENTER_LEFT, title);
if (draw_general_voc){it.print(w-2, cy + 31, fl14, id(grey), TextAlign::CENTER_RIGHT, unit);}
ix = 0;
for (int i = 0; i < 3; i++){
if(isnan(values[i])){continue;}
it.printf(w/6*(1+ix*2), cy + 58, fl14, id(grey), TextAlign::BOTTOM_CENTER, "%s", names[i]);
it.printf(w/6*(1+ix*2), cy + 50, fv30, colors[i], display::TextAlign::TOP_CENTER, "%.0f", values[i]);
ix++;
}
// END REUSABLE
;
;
if (draw_general_voc){cy += 88;}
else {cy += 46;}
}
// START SGP4x
cy+=2;
auto sgp4x = "SGP41";
if (isnan(aqi_nox)){sgp4x = "SGP40";}
if(!isnan(aqi_voc)){
col = id(white);
it.printf(2, cy, fl14, id(grey), TextAlign::CENTER_LEFT, "%s VOC Index", sgp4x);
it.printf(180, cy, fl14, id(grey), TextAlign::CENTER_LEFT, "(1 ... 500)");
it.printf(180, cy, fl14, col, TextAlign::CENTER_RIGHT, "%.0f ", aqi_voc);
cy+=15;
}
;
if(!isnan(aqi_nox)){
col = id(white);
it.printf(2, cy, fl14, id(grey), TextAlign::CENTER_LEFT, "%s NOx Index", sgp4x);
it.printf(180, cy, fl14, id(grey), TextAlign::CENTER_LEFT, "(1 ... 500)");
it.printf(180, cy, fl14, col, TextAlign::CENTER_RIGHT, "%.0f ", aqi_nox);
}
;
// END VOC & SGP4x
;
;
;
dimensions:
height: 320
width: 240
uart:
- tx_pin: GPIO10
rx_pin: GPIO09
baud_rate: 9600
id: uart_ze08
parity: none
data_bits: 8
stop_bits: 1
debug:
direction: BOTH
dummy_receiver: true
after:
# bytes: 9
timeout: 10ms
sequence:
- lambda: |-
//UARTDebug::log_int(direction, bytes, ','); // Log the message as int.
//UARTDebug::log_hex(direction, bytes, ','); // Log the message in hex.
ESP_LOGD("custom", "Bytes size: %d", bytes.size());
if (direction == UART_DIRECTION_RX)
{
if (bytes.size() == 9)
{
if ( bytes[0] == 0xFF && // Check sensor identification
bytes[1] == 0x17
)
{
float value = float((bytes[4] * 256) + bytes[5]); // Decode message
id(ch2o_ze08).publish_state(value); // Publish results to a sensor.
}
}
}
- tx_pin: GPIO08
rx_pin: GPIO07
baud_rate: 9600
id: uart_pms
spi:
clk_pin: GPIO01
mosi_pin: GPIO02
miso_pin: GPIO16
i2c:
- sda: GPIO11
scl: GPIO12
scan: true
id: i2c_main
- sda: GPIO17
scl: GPIO18
scan: true
id: i2c_2
sensor:
- platform: template
name: "ZE08 CH2O"
unit_of_measurement: ppb
state_class: measurement
device_class: volatile_organic_compounds
id: ch2o_ze08
filters:
- filter_out: 2000.0
- exponential_moving_average:
send_every: 1
alpha: 0.5
- platform: uptime
name: "Uptime"
id: uptime_sensor
update_interval: 5s
disabled_by_default: True
- platform: debug
free:
name: "Heap Free"
id: heap_free
disabled_by_default: True
loop_time:
name: "Loop Time"
id: loop_time
disabled_by_default: True
- platform: aht10
#address: 0x38
i2c_id: i2c_main
variant: AHT20
temperature:
name: "AHT20 Temperature"
id: temp_aht20
humidity:
name: "AHT20 Humidity"
id: hum_aht20
update_interval: 10s
- platform: bmp280_i2c
address: 0x77
i2c_id: i2c_main
temperature:
name: "BMP280 Temperature"
id: temp_bmp280
oversampling: 16x
pressure:
name: "BMP280 Pressure"
id: pres_bmp280
update_interval: 10s
- platform: sht4x
#addr 0x44
i2c_id: i2c_main
temperature:
name: "SHT40 Temperature"
id: temp_sht40
humidity:
name: "SHT40 Relative Humidity"
id: hum_sht40
update_interval: 10s
- platform: scd4x
#address: 0x62
i2c_id: i2c_main
co2:
name: "SCD40 CO2"
id: co2_scd40
temperature:
name: "SCD40 Temperature"
id: temp_scd40
humidity:
name: "SCD40 Humidity"
id: hum_scd40
update_interval: 30s
- platform: ens160_i2c
address: 0x53
i2c_id: i2c_2
eco2:
name: "ENS160 eCO2"
id: eco2_ens160
tvoc:
name: "ENS160 TVOC"
id: tvoc_ens160
filters:
- exponential_moving_average:
send_every: 1
alpha: 0.5
aqi:
name: "ENS160 Air Quality Index"
id: aqi_ens160
update_interval: 10s
compensation:
temperature: temp_aht20_ens
humidity: hum_aht20_ens
- platform: aht10
#address: 0x38
i2c_id: i2c_2
variant: AHT20
temperature:
name: "AHT20/ENS160 Temperature"
id: temp_aht20_ens
disabled_by_default: True
humidity:
name: "AHT20/ENS160 Humidity"
id: hum_aht20_ens
disabled_by_default: True
update_interval: 10s
- platform: sgp30
i2c_id: i2c_main
address: 0x58
eco2:
name: "SGP30 eCO2"
id: eco2_sgp30
accuracy_decimals: 0
filters:
- exponential_moving_average:
send_every: 1
alpha: 0.2
tvoc:
name: "SGP30 TVOC"
id: tvoc_sgp30
accuracy_decimals: 1
filters:
- exponential_moving_average:
send_every: 1
alpha: 0.2
store_baseline: yes
update_interval: 5s
compensation:
humidity_source: hum_aht20
temperature_source: temp_aht20
- platform: sgp4x
i2c_id: i2c_main
#address: 0x59
voc:
name: "SGP41 VOC Index"
id: aqi_voc_sgp41
icon: "mdi:air-filter"
device_class: aqi
state_class: measurement
unit_of_measurement: "index points"
nox:
name: "SGP41 NOx Index"
id: aqi_nox_sgp41
icon: "mdi:air-filter"
device_class: aqi
state_class: measurement
unit_of_measurement: "index points"
compensation:
humidity_source: hum_aht20
temperature_source: temp_aht20
store_baseline: True
update_interval: 10s
- platform: pmsx003
type: PMSX003
update_interval: 120s
uart_id: uart_pms
pm_1_0:
name: "PMS Particulate Matter <1.0µm Concentration"
id: pm1_pms
pm_2_5:
name: "PMS Particulate Matter <2.5µm Concentration"
id: pm25_pms
pm_10_0:
name: "PMS Particulate Matter <10.0µm Concentration"
id: pm10_pms
I’m unsure where to go with this?