(Un)Official Prusa Enclosure "Smart Box" - Project
This is still work in progress,
but because there is no news on the "Smart Box" for the enclosure (e.g mentioned here), I started working on my own version and wanted to share my progress.
Basic idea:
Use an ESP32, a Sen54 Sensor to measure PM 1.0 / 2.5, VOC, Temperature, and Humidity, add an LCD and 3 Outputs: Light, AirFilter and Fan.
If the airquality drops , auto start the AirFilter, plus allow control via rotary and switches of On/Off, speeds, brightness, auto airfilter on/off and thresholds.
Firmware is "written" in esphome, Hardware is currently a breadboard prototype, and the case is also just mocked so far.
Here are a view screenshots:
Startup time!
Main, screen - Shows Environment data
Rotary opens the settings screen:
Click and Values can be changes...
And the current state of the Hardware
Next steps:
* Get the correct connectors, Click-Mate something, ...
* Build a better Hardware Version + Design a fitting case for the Enclosure (Bottom left like control board but more like the screen?)
* Test Runs with sensor in Enclosure and PLA, PETG, ABS
Current BOM:
1 * ESP32
1 * Sen54 Sensor
1 * Rotary Encoder
3 * Switches (Retro!)
1 * LCD ST7735
1 * IO Expander (MCP23017, overkill but i had it at home)
3 * Dual MOSFET Switch Module - Control 24v Leds, AirFilter, Fan via esp32
1 * Buck Converter (24v to 5v)
Will post updates here and share my progress, happy to share the current yaml for esp if anyone is interessted. My c coding really sucks so using esphome for the wrong purpose is still better 😉
Best Answer by Tobias Stanzel:
I started to create the github page for this project here:
https://github.com/tuct/smart-control-for-prusa-enclosure
Not perfect but should be better than this post!
I now plan to move the heater to the bottom of the enclosure and replace the flap with this iris:
https://www.printables.com/model/687126-servo-automated-iris-sg90-mod-for-esphome-home-ass
If i manage to do this, I will update the github repo!
RE: (Un)Official Prusa Enclosure "Smart Box" - Project
Short update: Found this awseome mod and started to integrate it already!
So besides auto air filtration, i now can also set a target temp based on the flap and heater from the mod!
I also started to move away from the bread board to a hand made pcb, here are some pics:
New Case, LCD, controlls and ESP32
AC / Traget temp control
Lid / Flap test
RE: (Un)Official Prusa Enclosure "Smart Box" - Project
After seeing those wires connecting to board - that's wayyyyyyy above my pay grade. 😱 😱
RE: (Un)Official Prusa Enclosure "Smart Box" - Project
You can do a lot of this with OctoPrint and the existing plugins for it, which might be a little less daunting than assembling prototype boards and building everything from scratch. I myself only use a plugin to control LEDs in the enclosure and to switch the power for everything off when the printer is idle. And I use the Pi camera with OctoPrint of course. But I know there is an enclosure plugin and you can control stuff via the GPIO pins of the RasPi. Integrating temp sensors and controlling some servos and fans into my setup on the Pi is on my todo list for this winter.
RE: (Un)Official Prusa Enclosure "Smart Box" - Project
This is pretty cool. I had ideas to do much the same, though I am (currently) less interested in VOC and particulates. I have a strip of RGBW LEDs and I was thinking of testing those out this weekend with the intention of integrating them soon. I am getting tired of using a flashlight when I want to inspect a print in-progress.
I have some other unrelated microcontroller projects too that I want to wrap up, and spent today learning how to use git and GitHub for code version control and platform independent coding. So I'm all set for another project now... 🤣
RE: (Un)Official Prusa Enclosure "Smart Box" - Project
Any chance to get the source code?
See my GitHub and printables.com for some 3d stuff that you may like.
RE: (Un)Official Prusa Enclosure "Smart Box" - Project
I will share the source code as soon as I can here, it's just a few hundred lines of yaml for esphome. Only a few a lambdas / c code.
Works without wlan, home assistant but could be enhanced to be monitored and controlled from home assistant as well (or via mqtt).
And yes, I would not recommend this as your first esphome / esp32 project as it requires a lot of soldering 😂
RE: (Un)Official Prusa Enclosure "Smart Box" - Project
Made some progress and the inputs and sensors are now done and not as messy as before!
Need to add the output board with 3 Mosfets (light, fan, air filter), 2 Relais(Heater, Printer Power), and the Servo (Flap) + Buck Converter and AC stuff.
Here is the current esphome source (WIP!)
substitutions:
node_name: mista-environment-sensor-test
id_prefix: mista_environment_sensor_test
name: Environment-Sensor-Test
esphome:
name: ${node_name}
on_boot:
priority: 600
then:
- script.execute:
id: display_off_timer
esp32:
board: az-delivery-devkit-v4
framework:
type: arduino
# Enable logging
logger:
# Enable Home Assistant API
api:
password: "SetAPassword"
ota:
password: "SetAPassword"
wifi:
ssid: "yourssd"
password: "yourwifips"
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "${name}"
password: "chooseApassword"
#captive_portal:
spi:
clk_pin: GPIO14
mosi_pin: GPIO26
i2c:
sda: GPIO21
scl: GPIO22
frequency: 100kHz
mcp23017:
- id: 'mcp23017_hub'
address: 0x20
globals:
- id: auto_filter_pm_min_value
type: int
restore_value: yes
initial_value: '150'
- id: auto_filter_voc_min_value
type: int
restore_value: yes
initial_value: '120'
- id: light_a_brightness
type: int
restore_value: yes
initial_value: '100'
- id: filter_a_speed
type: int
restore_value: yes
initial_value: '100'
- id: fan_a_speed
type: int
restore_value: yes
initial_value: '100'
- id: filter_a_auto
type: int
restore_value: yes
initial_value: '1'
- id: ac_active
type: int
restore_value: yes
initial_value: '0'
- id: ac_target_temp_value
type: int
restore_value: yes
initial_value: '25'
select:
- platform: template
id: display_mode
optimistic: true
options:
- environment
- settings
- single
- display_off
initial_option: environment
set_action:
- display.page.show: !lambda |-
if (strcmp(x.c_str(), "settings") == 0 || strcmp(x.c_str(), "single") == 0) {
return id(page_settings);
} else {
return id(page_environment);
}
- platform: template
id: settings
optimistic: true
options:
- ac_mode
- ac_target_temp
- default_light_brightness
- default_filter_speed
- default_fan_speed
- auto_filter
- auto_filter_pm_min
- auto_filter_voc_min
- back
initial_option: ac_mode
script:
- id: display_off_timer
mode: restart
then:
- if:
condition:
lambda: 'return (strcmp(id(display_mode).state.c_str(), "display_off") == 0 );'
then:
- light.turn_on:
id: lcd_light
- select.set:
id: display_mode
option: "environment"
- if:
condition:
lambda: 'return (strcmp(id(display_mode).state.c_str(), "environment") == 0 );'
then:
- delay: 1min
- select.set:
id: display_mode
option: "display_off"
- light.turn_off:
id: lcd_light
else:
- delay: 5s
- select.set:
id: display_mode
option: "environment"
- script.execute:
id: display_off_timer
- id: update_light # update the light brightness based on setting
mode: queued
then:
- lambda: |-
float brightness= ((float)id(light_a_brightness)/100);
auto call = id(light_a).turn_on();
call.set_transition_length(0); // in ms
call.set_brightness(brightness);
call.perform();
- id: update_filter # update the filter speed based on setting
mode: queued
then:
- lambda: |-
auto currentState = id(filter_a).current_values.is_on();
auto currentValue = id(filter_a).current_values.get_brightness();
if(id(switch_filter).state){ // filter on
float speed= ((float)id(filter_a_speed)/100);
if(id(filter_a_auto) == 1){
if(currentState == 1 && id(auto_filter_sensor).state == 0.0 ){ // turn off if auto says no and currently on
auto call = id(filter_a).turn_off();
call.set_transition_length(0); // in ms
call.set_brightness(0);
call.perform();
}
if((currentState == 0 || currentValue != speed) && id(auto_filter_sensor).state == 1.0 ){ // turn on if auto says yes and currently off
auto call = id(filter_a).turn_on();
call.set_transition_length(0); // in ms
call.set_brightness(speed);
call.perform();
}
}else{
if(currentState == 0 || currentValue != speed ){
auto call = id(filter_a).turn_on();
call.set_transition_length(0); // in ms
call.set_brightness(speed);
call.perform();
}
}
}else{
if(currentState == 1){
auto call = id(filter_a).turn_off();
call.set_transition_length(0); // in ms
call.set_brightness(0);
call.perform();
}
}
- id: update_fan # update the filter speed based on setting
mode: queued
then:
- lambda: |-
float speed= ((float)id(fan_a_speed)/100);
auto call = id(fan_a).turn_on();
call.set_transition_length(0); // in ms
call.set_brightness(speed);
call.perform();
- id: update_ac_target # update the filter speed based on setting
mode: queued
then:
- lambda: |-
auto call = id(my_climate).make_call();
call.set_target_temperature_low(id(ac_target_temp_value));
call.set_target_temperature_high(id(ac_target_temp_value)+1);
if(id(ac_active)==1){
call.set_mode("HEAT_COOL");
}else{
call.set_mode("OFF");
}
call.perform();
- id: open_lid # update the filter speed based on setting
mode: single
then:
- servo.write:
id: lid_servo
level: -50.0%
- id: close_lid # update the filter speed based on setting
mode: single
then:
- servo.write:
id: lid_servo
level: 0.0%
- id: update_ac # update the filter speed based on setting
mode: queued
then:
- lambda: |-
bool ac_is_active = false;
float servoOpen = 0.5;
float servoClosed = 0.0;
if(id(ac_active)==1){
ac_is_active = true;
}
bool fan_switch = id(switch_fan).state;
auto ac_mode = id(my_climate).mode;
auto ac_action = id(my_climate).action;
ESP_LOGI("main", "AC Active: %s",ac_is_active?"true":"false");
ESP_LOGI("main", "Fan switch: %s",fan_switch?"true":"false");
ESP_LOGI("main", "AC Mode: %d",ac_mode);
ESP_LOGI("main", "AC Action: %d",ac_action);
if(!fan_switch){
//fan/ac is dissabled!
ESP_LOGI("main", "AC: OFF, HEATER: OFF, FAN: OFF, LID: closed!");
//lid
//id(lid_servo).write(servoClosed);
id(close_lid)->execute();
//heater
id(heater_relais).turn_off();
//fan - off
auto call = id(fan_a).turn_off();
call.set_transition_length(0); // in ms
call.set_brightness(1.0); // 1.0 is full brightness
call.perform();
}else{
//fan is active, check if we are in AC mode
if(ac_is_active){
if(ac_action == CLIMATE_ACTION_HEATING){
ESP_LOGI("main", "AC: ON, HEATER: ON, FAN: ON, LID: closed!");
//fan on full
//id(lid_servo).write(servoClosed);
id(close_lid)->execute();
id(heater_relais).turn_on();
auto calla = id(fan_a).turn_on();
calla.set_transition_length(0); // in ms
calla.set_brightness(1.0); // 1.0 is full brightness
calla.perform();
}
if(ac_action == CLIMATE_ACTION_COOLING){
ESP_LOGI("main", "AC: ON, HEATER: OFF, FAN: ON, LID: open!");
//id(lid_servo).write(servoOpen);
id(open_lid)->execute();
id(heater_relais).turn_off();
//fan on full
auto callb = id(fan_a).turn_on();
callb.set_transition_length(0); // in ms
callb.set_brightness(1.0); // 1.0 is full brightness
callb.perform();
}
if(ac_action != CLIMATE_ACTION_HEATING && ac_action != CLIMATE_ACTION_COOLING){
ESP_LOGI("main", "AC: ON, HEATER: OFF, FAN: OFF, LID: closed!");
id(heater_relais).turn_off();
//id(lid_servo).write(servoClosed);
id(close_lid)->execute();
auto callc = id(fan_a).turn_off();
callc.set_transition_length(0); // in ms
callc.perform();
}
}else{
//fan only, open lid
ESP_LOGI("main", "AC: OFF, HEATER: OFF, FAN: ON, LID: open!");
//id(lid_servo).write(servoOpen);
id(open_lid)->execute();
id(heater_relais).turn_off();
//fan on full
auto calld = id(fan_a).turn_on();
calld.set_transition_length(0); // in ms
calld.set_brightness(1.0); // 1.0 is full brightness, use configured here!!!!
calld.perform();
}
}
- id: update_setting # used to update the settings
mode: queued
parameters:
dir: int
then:
- lambda: |-
uint stepSize = 10;
//for speed/brightness
uint minValue = 20;
if(dir==0){
// reduce
if(strcmp(id(settings).state.c_str(), "ac_mode")==0){
if(id(ac_active)==1){
id(ac_active) = 0;
}else{
id(ac_active) = 1;
}
id(update_ac_target)->execute();
}
if(strcmp(id(settings).state.c_str(), "ac_target_temp")==0 && id(ac_target_temp_value)>18){
id(ac_target_temp_value) = id(ac_target_temp_value)-1;
id(update_ac_target)->execute();
}
if(strcmp(id(settings).state.c_str(), "default_light_brightness")==0 && id(light_a_brightness)>minValue){
id(light_a_brightness) = id(light_a_brightness)-stepSize;
//update brightness of light!
if(id(switch_light).state){
id(update_light)->execute();
}
}
if(strcmp(id(settings).state.c_str(), "default_filter_speed")==0 && id(filter_a_speed)>minValue){
id(filter_a_speed) = id(filter_a_speed)-stepSize;
//update brightness of light!
id(update_filter)->execute();
}
if(strcmp(id(settings).state.c_str(), "default_fan_speed")==0 && id(fan_a_speed)>minValue){
id(fan_a_speed) = id(fan_a_speed)-stepSize;
//update brightness of light!
if(id(switch_fan).state){
id(update_fan)->execute();
}
}
if(strcmp(id(settings).state.c_str(), "auto_filter")==0){
if(id(filter_a_auto)==1){
id(filter_a_auto) = 0;
}else{
id(filter_a_auto) = 1;
}
}
if(strcmp(id(settings).state.c_str(), "auto_filter_pm_min")==0 && id(auto_filter_pm_min_value)>0){
id(auto_filter_pm_min_value) = id(auto_filter_pm_min_value)-stepSize;
}
if(strcmp(id(settings).state.c_str(), "auto_filter_voc_min")==0 && id(auto_filter_voc_min_value)>0){
id(auto_filter_voc_min_value) = id(auto_filter_voc_min_value)-stepSize;
}
}else{
// add
if(strcmp(id(settings).state.c_str(), "ac_mode")==0){
if(id(ac_active)==1){
id(ac_active) = 0;
}else{
id(ac_active) = 1;
}
id(update_ac_target)->execute();
}
if(strcmp(id(settings).state.c_str(), "ac_target_temp")==0 && id(ac_target_temp_value)<50){
id(ac_target_temp_value) = id(ac_target_temp_value)+1;
id(update_ac_target)->execute();
}
if(strcmp(id(settings).state.c_str(), "default_light_brightness")==0 && id(light_a_brightness)<100){
id(light_a_brightness) = id(light_a_brightness)+stepSize;
//update brightness of light!
if(id(switch_light).state){
id(update_light)->execute();
}
}
if(strcmp(id(settings).state.c_str(), "default_filter_speed")==0 && id(filter_a_speed)<100){
id(filter_a_speed) = id(filter_a_speed)+stepSize;
//update brightness of light!
id(update_filter)->execute();
}
if(strcmp(id(settings).state.c_str(), "default_fan_speed")==0 && id(fan_a_speed)<100){
id(fan_a_speed) = id(fan_a_speed)+stepSize;
//update brightness of light!
if(id(switch_fan).state){
id(update_fan)->execute();
}
}
if(strcmp(id(settings).state.c_str(), "auto_filter")==0){
if(id(filter_a_auto)==1){
id(filter_a_auto) = 0;
}else{
id(filter_a_auto) = 1;
}
}
if(strcmp(id(settings).state.c_str(), "auto_filter_pm_min")==0 && id(auto_filter_pm_min_value)<990){
id(auto_filter_pm_min_value) = id(auto_filter_pm_min_value)+stepSize;
}
if(strcmp(id(settings).state.c_str(), "auto_filter_voc_min")==0 && id(auto_filter_voc_min_value)<500){
id(auto_filter_voc_min_value) = id(auto_filter_voc_min_value)+stepSize;
}
}
color:
- id: my_red
red: 100%
green: 3%
blue: 5%
- id: my_green
red: 0%
green: 100%
blue: 0%
- id: my_blue
red: 0%
green: 0%
blue: 100%
- id: my_yellow
red: 50%
green: 50%
blue: 0%
- id: my_gray
red: 30%
green: 30%
blue: 30%
font:
# gfonts://family[@weight]
- file:
type: gfonts
family: Inconsolata
weight: 700
id: roboto
glyphs: |-
!"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyzµ³>/
size: 13
- file:
type: gfonts
family: Inconsolata
weight: 700
id: roboto_small
glyphs: |-
!"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyzµ³>/
size: 11
image:
- file: mdi:lightbulb
id: icon_light
resize: 16x16
- file: mdi:air-filter
id: icon_filter
resize: 16x16
- file: mdi:air-conditioner
id: icon_ac
resize: 18x18
- file: mdi:fan
id: icon_fan
resize: 16x16
- file: "thermometer_FILL0_wght400_GRAD0_opsz24.svg"
id: icon_temp
resize: 18x18
type: TRANSPARENT_BINARY
dither: FLOYDSTEINBERG
- file: "humidity_mid_FILL0_wght400_GRAD0_opsz24.svg"
id: icon_hum
resize: 18x18
type: TRANSPARENT_BINARY
dither: FLOYDSTEINBERG
graph:
# Show bare-minimum auto-ranged graph
- id: ${id_prefix}_pm10_graph
duration: 5min
width: 50
height: 20
y_grid: 10000
border: false
traces:
- sensor: ${id_prefix}_pm10
line_type: SOLID
line_thickness: 2
color: my_blue
- id: ${id_prefix}_pm25_graph
duration: 5min
width: 50
height: 20
y_grid: 10000
border: false
traces:
- sensor: ${id_prefix}_pm25
line_type: SOLID
line_thickness: 2
color: my_blue
- id: ${id_prefix}_voc_graph
duration: 5min
width: 50
height: 20
max_value: 500
min_value: 1
y_grid: 500
border: false
traces:
- sensor: ${id_prefix}_voc
line_type: SOLID
line_thickness: 2
color: my_blue
- id: ${id_prefix}_temperature_graph
duration: 5min
width: 50
height: 20
max_value: 70
min_value: 20
y_grid: 200
traces:
- sensor: ${id_prefix}_temperature
line_type: SOLID
line_thickness: 2
color: my_blue
- id: ${id_prefix}_humidity_graph
duration: 5min
width: 50
height: 20
max_value: 90
min_value: 30
y_grid: 200
traces:
- sensor: ${id_prefix}_humidity
line_type: SOLID
line_thickness: 2
color: my_blue
display:
- platform: st7735
model: "INITR_18BLACKTAB"
id: my_display
reset_pin: 33
cs_pin: 27
dc_pin: 13
rotation: 180
device_width: 128
device_height: 160
col_start: 0
row_start: 0
eight_bit_color: false
update_interval: 300ms
pages:
- id: page_environment
lambda: |-
it.print(64, 2, id(roboto),COLOR_ON,TextAlign::TOP_CENTER, "ENVIRONMENT");
if(isnan(id(${id_prefix}_temperature).state)){
it.print(64, 24, id(roboto),COLOR_ON,TextAlign::TOP_CENTER, "Waiting for");
it.print(64, 40, id(roboto),COLOR_ON,TextAlign::TOP_CENTER, "sensor data");
it.print(64, 56, id(roboto),COLOR_ON,TextAlign::TOP_CENTER, "...");
}else{
//it.fill(Color::BLACK);
//PM 2.5 - LEFT
//it.line(6, 19, 48, 19);
uint tempoffsetY = 16;
//it.print(3, tempoffsetY+20, id(roboto),COLOR_ON,TextAlign::BOTTOM_LEFT, "TEMP");
it.image(3, tempoffsetY+2, id(icon_temp), ImageAlign::TOP_LEFT);
it.graph(42, tempoffsetY+2, id(${id_prefix}_temperature_graph),Color::BLACK);
it.printf(112, tempoffsetY+1, id(roboto), TextAlign::TOP_CENTER, "%.1f", id(${id_prefix}_temperature).state);
it.print(112, tempoffsetY+11, id(roboto_small), COLOR_ON, TextAlign::TOP_CENTER , "°C");
uint humoffsetY = 40;
//it.print(3, humoffsetY+20, id(roboto),COLOR_ON,TextAlign::BOTTOM_LEFT, "HUM");
it.image(3, humoffsetY+2, id(icon_hum), ImageAlign::TOP_LEFT);
it.graph(42, humoffsetY+2, id(${id_prefix}_humidity_graph),Color::BLACK);
it.printf(112, humoffsetY+1, id(roboto), TextAlign::TOP_CENTER, "%.1f", id(${id_prefix}_humidity).state);
it.print(112, humoffsetY+11, id(roboto_small), COLOR_ON, TextAlign::TOP_CENTER , "%");
uint pm10offsetY = 64;
it.print(3, pm10offsetY+20, id(roboto),COLOR_ON,TextAlign::BOTTOM_LEFT, "PM1.0");
it.graph(42, pm10offsetY+2, id(${id_prefix}_pm10_graph),Color::BLACK);
it.printf(112, pm10offsetY+1, id(roboto), TextAlign::TOP_CENTER, "%.0f", id(${id_prefix}_pm10).state);
it.print(112, pm10offsetY+11, id(roboto_small), COLOR_ON, TextAlign::TOP_CENTER , "µg/m³");
uint pm25offsetY = 88;
it.print(3, pm25offsetY+20, id(roboto),COLOR_ON,TextAlign::BOTTOM_LEFT, "PM2.5");
it.graph(42, pm25offsetY+2, id(${id_prefix}_pm25_graph),Color::BLACK);
it.printf(112, pm25offsetY+1, id(roboto), TextAlign::TOP_CENTER, "%.0f", id(${id_prefix}_pm25).state);
it.print(112, pm25offsetY+11, id(roboto_small), COLOR_ON, TextAlign::TOP_CENTER , "µg/m³");
uint vocoffsetY = 112;
it.print(3, vocoffsetY+20, id(roboto),COLOR_ON,TextAlign::BOTTOM_LEFT, "VOC");
it.graph(42, vocoffsetY+2, id(${id_prefix}_voc_graph),Color::BLACK);
it.printf(112, vocoffsetY+5, id(roboto), TextAlign::TOP_CENTER, "%.0f", id(${id_prefix}_voc).state);
//it.print(112, vocoffsetY+10, id(roboto_small), COLOR_ON, TextAlign::TOP_CENTER , "%");
uint iconY = 152;
if(id(switch_light).state){
it.image(16, iconY, id(icon_light), ImageAlign::BOTTOM_CENTER,id(my_green));
}else{
it.image(16, iconY, id(icon_light), ImageAlign::BOTTOM_CENTER,id(my_gray));
}
if(id(switch_filter).state){
if(id(filter_a_auto) == 1 && id(auto_filter_sensor).state == 0.0){ //auto is on
it.image(64, iconY, id(icon_filter), ImageAlign::BOTTOM_CENTER,id(my_yellow));
}else{
it.image(64, iconY, id(icon_filter), ImageAlign::BOTTOM_CENTER,id(my_green));
}
}else{
it.image(64, iconY, id(icon_filter), ImageAlign::BOTTOM_CENTER,id(my_gray));
}
if(id(switch_fan).state){
if(id(ac_active) ==1){
it.image(112, iconY+2, id(icon_ac), ImageAlign::BOTTOM_CENTER,id(my_green));
//get target from climate?!?!
it.printf(99, 150, id(roboto_small),COLOR_ON,TextAlign::BOTTOM_CENTER, "%d",id(ac_target_temp_value));
}else{
it.image(112, iconY, id(icon_fan), ImageAlign::BOTTOM_CENTER,id(my_green));
}
}else{
it.image(112, iconY, id(icon_fan), ImageAlign::BOTTOM_CENTER,id(my_gray));
}
if(id(filter_a_auto) ==1){
auto msg = "Auto - Run";
if(id(auto_filter_sensor).state == 0.0){
msg = "Auto - Idle";
}
it.printf(64, 160, id(roboto_small),COLOR_ON,TextAlign::BOTTOM_CENTER, "%s",msg);
}
}
- id: page_settings
lambda: |-
uint offsetY = 2;
uint offsetX = 10;
uint lineheight = 16;
uint activeYoffset = offsetY+12;
uint activeXoffset = offsetX/2-2;
auto displayModeIndex = id(display_mode).active_index();
auto settingsIndex = id(settings).active_index();
if (settingsIndex.has_value()) {
activeYoffset = settingsIndex.value();
if(settingsIndex.value()==1 || settingsIndex.value()==6 || settingsIndex.value()==7){
activeXoffset=offsetX/2+3;
}
} else {
ESP_LOGI("main", "No option is active");
}
if (displayModeIndex.has_value()) {
if(displayModeIndex.value()==2 ){
activeXoffset=98;
}
} else {
ESP_LOGI("main", "No option is active");
}
it.print(64, offsetY, id(roboto),COLOR_ON,TextAlign::TOP_CENTER, "SETTINGS");
it.print(offsetX, offsetY+(1*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_LEFT, "A/C ........");
it.print(offsetX, offsetY+(2*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_LEFT, " TARGET C°..");
it.print(offsetX, offsetY+(3*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_LEFT, "LIGHT.......");
it.print(offsetX, offsetY+(4*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_LEFT, "FILTER......");
it.print(offsetX, offsetY+(5*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_LEFT, "FAN SPEED...");
it.print(offsetX, offsetY+(6*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_LEFT, "AUTO FILTER.");
it.print(offsetX, offsetY+(7*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_LEFT, " PM 2.5 >...");
it.print(offsetX, offsetY+(8*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_LEFT, " VOC >......");
it.print(offsetX, offsetY+(9*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_LEFT, "BACK");
it.circle(offsetX/2-2, offsetY+(1*lineheight)+7, 3);
it.circle(offsetX/2+3, offsetY+(2*lineheight)+7, 3);
it.circle(offsetX/2-2, offsetY+(3*lineheight)+7, 3);
it.circle(offsetX/2-2, offsetY+(4*lineheight)+7, 3);
it.circle(offsetX/2-2, offsetY+(5*lineheight)+7, 3);
it.circle(offsetX/2-2, offsetY+(6*lineheight)+7, 3);
it.circle(offsetX/2+3, offsetY+(7*lineheight)+7, 3);
it.circle(offsetX/2+3, offsetY+(8*lineheight)+7, 3);
it.circle(offsetX/2-2, offsetY+(9*lineheight)+7, 3);
//mark selected setting!
it.filled_circle(activeXoffset, offsetY+((activeYoffset+1)*lineheight)+7, 3);
//print selected setting!
//it.printf(64, offsetY+(8*lineheight), id(roboto), TextAlign::TOP_CENTER, "%s", id(display_mode).state.c_str() );
//values
uint lightValue = id(light_a_brightness);
uint filterValue = id(filter_a_speed);
uint fanValue = id(fan_a_speed);
if(id(ac_active) ==1){
it.printf(125, offsetY+(1*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_RIGHT, "%s","ON");
}else{
it.printf(125, offsetY+(1*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_RIGHT, "%s","OFF");
}
it.printf(125, offsetY+(2*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_RIGHT, "%d",id(ac_target_temp_value));
it.printf(125, offsetY+(3*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_RIGHT, "%d",lightValue);
it.printf(125, offsetY+(4*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_RIGHT, "%d",filterValue);
it.printf(125, offsetY+(5*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_RIGHT, "%d",fanValue);
if(id(filter_a_auto) ==1){
it.printf(125, offsetY+(6*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_RIGHT, "%s","ON");
}else{
it.printf(125, offsetY+(6*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_RIGHT, "%s","OFF");
}
it.printf(125, offsetY+(7*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_RIGHT, "%d",id(auto_filter_pm_min_value));
it.printf(125, offsetY+(8*lineheight), id(roboto),COLOR_ON,TextAlign::TOP_RIGHT, "%d",id(auto_filter_voc_min_value));
ESP_LOGI("main", "auto_filter_pm_max: %d is active", id(auto_filter_pm_min_value));
sensor:
- platform: template
id: "auto_filter_sensor"
lambda: |-
id(update_filter)->execute();
id(update_ac)->execute();
if (id(filter_a_auto) == 1) {
if(id(${id_prefix}_pm25).state>id(auto_filter_pm_min_value) || id(${id_prefix}_voc).state>id(auto_filter_voc_min_value)){
return 1.0;
}
return 0.0;
} else {
//on and no auto, so...
return 0.0;
}
update_interval: 1s
- platform: sen5x
id: sen54
pm_1_0:
name: " PM <1µm Weight concentration"
id: ${id_prefix}_pm10
accuracy_decimals: 1
filters:
- sliding_window_moving_average:
window_size: 9
send_every: 1
pm_2_5:
name: " PM <2.5µm Weight concentration"
id: ${id_prefix}_pm25
accuracy_decimals: 1
filters:
- sliding_window_moving_average:
window_size: 9
send_every: 1
pm_4_0:
name: " PM <4µm Weight concentration"
id: ${id_prefix}_pm40
accuracy_decimals: 1
pm_10_0:
name: " PM <10µm Weight concentration"
id: ${id_prefix}_pm100
accuracy_decimals: 1
temperature:
name: "Temperature"
accuracy_decimals: 1
id: ${id_prefix}_temperature
humidity:
name: "Humidity"
accuracy_decimals: 0
id: ${id_prefix}_humidity
voc:
name: "VOC"
id: ${id_prefix}_voc
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
filters:
- sliding_window_moving_average:
window_size: 9
send_every: 1
temperature_compensation:
offset: 0
normalized_offset_slope: 0
time_constant: 0
acceleration_mode: low
store_baseline: true
address: 0x69
update_interval: 10s
- platform: rotary_encoder
name: "Rotary Encoder"
id: rotary
pin_a: GPIO05
pin_b: GPIO17
min_value: 1
max_value: 20
on_clockwise:
- if:
condition:
lambda: 'return (strcmp(id(display_mode).state.c_str(), "environment") == 0);'
then:
- select.set_index:
id: display_mode
index: 1
- if:
condition:
lambda: 'return (strcmp(id(display_mode).state.c_str(), "settings") == 0);'
then:
- select.next:
id: settings
cycle: true
- if:
condition:
lambda: 'return (strcmp(id(display_mode).state.c_str(), "single") == 0);'
then:
- lambda: |-
ESP_LOGI("main", "UP NOW: %d is active", id(light_a_brightness));
id(update_setting)->execute(1);
- lambda: |-
id(display_off_timer)->execute();
on_anticlockwise:
- if:
condition:
lambda: 'return (strcmp(id(display_mode).state.c_str(), "environment") == 0);'
then:
- select.set_index:
id: display_mode
index: 1
- if:
condition:
lambda: 'return (strcmp(id(display_mode).state.c_str(), "settings") == 0);'
then:
- select.previous:
id: settings
cycle: true
- if:
condition:
lambda: 'return (strcmp(id(display_mode).state.c_str(), "single") == 0 );'
then:
- lambda: |-
id(update_setting)->execute(0);
- lambda: |-
id(display_off_timer)->execute();
binary_sensor:
- platform: gpio
pin:
mcp23xxx: mcp23017_hub
number: 3
mode:
input: true
inverted: true
name: "Rotary"
on_multi_click:
- timing:
- ON for at most 1s
- OFF for at least 0.5s
then:
- lambda: |-
//ENVIRONMENT
uint clickHandled = 0;
if(strcmp(id(display_mode).state.c_str(), "environment") == 0){
auto displayCall = id(display_mode).make_call();
displayCall.set_option("settings");
displayCall.perform();
clickHandled = 1;
}
//SINGLE
if(clickHandled==0 && strcmp(id(display_mode).state.c_str(), "single") ==0){
auto displayCall = id(display_mode).make_call();
displayCall.set_option("settings");
displayCall.perform();
clickHandled = 1;
}
//SETTINGS
// back -> go back to ENVIRONMENT
if(clickHandled==0 && strcmp(id(display_mode).state.c_str(), "settings") ==0){
if(strcmp(id(settings).state.c_str(), "back") == 0){
auto displayCall = id(display_mode).make_call();
displayCall.set_option("environment");
displayCall.perform();
}else{
auto displayCall = id(display_mode).make_call();
displayCall.set_option("single");
displayCall.perform();
}
clickHandled = 1;
}
id(display_off_timer)->execute();
- platform: gpio
pin:
mcp23xxx: mcp23017_hub
number: 0
mode:
input: true
pullup: true
inverted: true
id: switch_light
publish_initial_state: true
on_state:
- lambda: |-
if(id(switch_light).state){
id(update_light)->execute();
}else{
auto call = id(light_a).turn_off();
call.set_transition_length(0); // in ms
call.set_brightness(0);
call.perform();
}
id(display_off_timer)->execute();
filters:
- delayed_on_off:
time_on: 200ms
time_off: 200ms
- platform: gpio
pin:
mcp23xxx: mcp23017_hub
number: 2
mode:
input: true
pullup: true
inverted: true
id: switch_filter
publish_initial_state: true
on_state:
- lambda: |-
id(update_filter)->execute();
id(display_off_timer)->execute();
filters:
- delayed_on_off:
time_on: 200ms
time_off: 200ms
- platform: gpio
pin:
mcp23xxx: mcp23017_hub
number: 1
mode:
input: true
pullup: true
inverted: true
id: switch_fan
publish_initial_state: true
on_state:
- lambda: |-
if(id(switch_fan).state){
id(update_fan)->execute();
}else{
auto call = id(fan_a).turn_off();
call.set_transition_length(0); // in ms
call.set_brightness(0);
call.perform();
}
id(display_off_timer)->execute();
filters:
- delayed_on_off:
time_on: 200ms
time_off: 200ms
- platform: gpio
pin:
mcp23xxx: mcp23017_hub
number: 4
mode:
input: true
pullup: true
inverted: true
id: switch_printer
publish_initial_state: true
on_state:
- logger.log: PRINTER
- lambda: |-
if(id(switch_printer).state){
id(printer_relais).turn_on();
}else{
id(printer_relais).turn_off();
}
filters:
- delayed_on_off:
time_on: 200ms
time_off: 200ms
climate:
- platform: thermostat
name: "Enclosure Climate Controller"
id: my_climate
sensor: ${id_prefix}_temperature
visual:
min_temperature: 18
max_temperature: 55
temperature_step: 1
min_cooling_off_time: 10s
min_cooling_run_time: 10s
min_heating_off_time: 10s
min_heating_run_time: 10s
min_idle_time: 10s
cool_action:
- script.execute: update_ac
heat_action:
- script.execute: update_ac
idle_action:
- script.execute: update_ac
on_state:
- script.execute: update_ac
on_control:
- script.execute: update_ac
default_preset: default
preset:
- name: default
default_target_temperature_low: 25
default_target_temperature_high: 26
mode: HEAT_COOL
output:
- platform: ledc
pin: GPIO16
id: gpio_16
frequency: 25kHz
channel: 0
- platform: ledc
pin: GPIO23
id: gpio_23
channel: 2
frequency: 25kHz
- platform: ledc
pin: GPIO19
id: gpio_19
channel: 4
frequency: 25kHz
- platform: ledc
id: pwm_servo
pin: GPIO18
channel: 6
frequency: 50Hz
- platform: gpio
pin: GPIO04
# mcp23xxx: mcp23017_hub
# number: 13
id: lcd_out
number:
- platform: template
id: servo_pos
min_value: -100
initial_value: 0
max_value: 100
step: 10
optimistic: true
# set_action:
# then:
# - servo.write:
# id: lid_servo
# level: !lambda 'return x / 100.0;'
servo:
- id: lid_servo
output: pwm_servo
transition_length: 500000s
auto_detach_time: 1s
switch:
- platform: gpio
pin: GPIO33
# mcp23xxx: mcp23017_hub
# number: 5
id: printer_relais
restore_mode: RESTORE_DEFAULT_OFF
- platform: gpio
pin: GPIO25
# mcp23xxx: mcp23017_hub
# number: 5
id: heater_relais
restore_mode: ALWAYS_OFF
# Example usage in a light
light:
- platform: monochromatic
output: gpio_16
id: light_a
name: "Prusa Enclosure - Light"
restore_mode: ALWAYS_OFF
- platform: monochromatic
output: gpio_23
id: filter_a
name: "Prusa Enclosure - Air Filter"
restore_mode: ALWAYS_OFF
- platform: monochromatic
output: gpio_19
id: fan_a
name: "Prusa Enclosure - Fan"
restore_mode: ALWAYS_OFF
- platform: binary
id: "lcd_light"
restore_mode: ALWAYS_ON
output: lcd_out
RE: (Un)Official Prusa Enclosure "Smart Box" - Project
Updated POM:
1 * ESP32
1 * Sen54 Sensor
1 * Rotary Encoder
3 * Switches (Retro!)
1 * LCD ST7735
1 * IO Expander (MCP23017, overkill but i had it at home)
3 * Dual MOSFET Switch Module - Control 24v Leds, AirFilter, Fan via esp32
1 * Buck Converter (24v to 5v)
2 * 3.3V Relais (Heater and to control Printer power)
From Automated Heating System
1 * 200W heater+fan combo (i use a MOSFET to control the fan)
1* SG-90 type servo
RE: (Un)Official Prusa Enclosure "Smart Box" - Project
Thanks!
Maybe you should put it all into a git repo?
See my GitHub and printables.com for some 3d stuff that you may like.
RE: (Un)Official Prusa Enclosure "Smart Box" - Project
I will post the code and everything to github / printables when I am done.
Here are some updates:
Added DS18B20 temperature probe for the heater (as in the Automated Heating System)
Created power/breakout board and a case for it + PSU mount with power switch and plug
The case and PSU will be mounted to the back like in the Automated Heating System, case is a bit bigger 😉
RE: (Un)Official Prusa Enclosure "Smart Box" - Project
I just got my enclosure built yesterday and was immediately looking for something like this. Especially since I’m using the HEPA filter and the LED lighting. I absolutely love this! Especially tying it together with the heater automation as I was very interested in that as well. I’m able to get the inside up to 35C but it takes 3 hours of printing. Obviously not ideal for ABS / Nylon. Have you had any more updates to this project? Do you need help with anything? I’d love to help if you need it!
RE: (Un)Official Prusa Enclosure "Smart Box" - Project
Hi all,
It been a while, in the meantime I finished the build and already using it!
I added an additional temp sensor and also a runtime counter for the airfilter + a nevermore v6 to speed up the filtration.
I want to add a timer for the heater or couple it somehow with the printer, so that it stops heating If the print is finished.
Heater and filter is great!
Fun fact, when heating starts, VOC is going up only from the heater! Otherwise I can see that, based on the material there is quite some pm1/2 and or VOC, and my automation autostarts the airfilter and stops when below a treshhold.
I started to work on more documentation (fitzing diagram,...) but I guess I need the Christmas vacation to finish it and upload it to github.
RE: (Un)Official Prusa Enclosure "Smart Box" - Project
Looking forward to it! 👍
RE: (Un)Official Prusa Enclosure "Smart Box" - Project
First print was a short one, 2nd was longer, so I turned off the heater and went to bed, you can see that the heat from the bed keeps heating up the enclosure 😉
RE: (Un)Official Prusa Enclosure "Smart Box" - Project
I finished the first shot of the fritzing sketch, took longer than building it 😉

RE: (Un)Official Prusa Enclosure "Smart Box" - Project
Great project, Thomas. I'm waiting for my new Mk4 w/Enclosure kit to arrive after Christmas, and I'm excited to see how this progresses -- I'll certainly attempt a make!
RE: (Un)Official Prusa Enclosure "Smart Box" - Project
I started to create the github page for this project here:
https://github.com/tuct/smart-control-for-prusa-enclosure
Not perfect but should be better than this post!
I now plan to move the heater to the bottom of the enclosure and replace the flap with this iris:
https://www.printables.com/model/687126-servo-automated-iris-sg90-mod-for-esphome-home-ass
If i manage to do this, I will update the github repo!





















