(Un)Official Prusa Enclosure "Smart Box" - Project
 
Notifications
Clear all

(Un)Official Prusa Enclosure "Smart Box" - Project  

Page 1 / 2
  RSS
Tobias Stanzel
(@tobias-stanzel)
Eminent Member
(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! 

 

Posted : 09/10/2023 11:25 pm
CanisVolans, Christopher72, jseyfert3 and 2 people liked
Tobias Stanzel
(@tobias-stanzel)
Eminent Member
Topic starter answered:
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

Custom / hand made PCB 

AC / Traget temp control

Lid / Flap test

Posted : 20/10/2023 6:05 pm
JMH714
(@jmh714-2)
Trusted Member
RE: (Un)Official Prusa Enclosure "Smart Box" - Project

After seeing those wires connecting to board - that's wayyyyyyy above my pay grade. 😱 😱 

Posted : 20/10/2023 6:13 pm
Walter Layher
(@walter-layher)
Honorable Member
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.

Posted : 20/10/2023 9:15 pm
jseyfert3
(@jseyfert3)
Reputable Member
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... 🤣

Posted : 21/10/2023 9:59 pm
_KaszpiR_
(@_kaszpir_)
Honorable Member
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.

Posted : 22/10/2023 8:48 am
Tobias Stanzel
(@tobias-stanzel)
Eminent Member
Topic starter answered:
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 😂

 

Posted : 23/10/2023 5:48 pm
Tobias Stanzel
(@tobias-stanzel)
Eminent Member
Topic starter answered:
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

  

Posted : 23/10/2023 11:36 pm
_KaszpiR_ liked
Tobias Stanzel
(@tobias-stanzel)
Eminent Member
Topic starter answered:
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

 

Posted : 23/10/2023 11:48 pm
_KaszpiR_
(@_kaszpir_)
Honorable Member
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.

Posted : 24/10/2023 6:10 am
Tobias Stanzel
(@tobias-stanzel)
Eminent Member
Topic starter answered:
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 😉

Posted : 30/10/2023 11:24 pm
Tobias Stanzel
(@tobias-stanzel)
Eminent Member
Topic starter answered:
RE: (Un)Official Prusa Enclosure "Smart Box" - Project

Finally up and running, more details soon, here are some pics:

Posted : 09/11/2023 10:00 pm
LiokoDev and nhand42 liked
LiokoDev
(@liokodev)
Member
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!

Posted : 26/11/2023 6:08 pm
Tobias Stanzel
(@tobias-stanzel)
Eminent Member
Topic starter answered:
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. 

Posted : 09/12/2023 2:54 pm
LiokoDev liked
JMH714
(@jmh714-2)
Trusted Member
RE: (Un)Official Prusa Enclosure "Smart Box" - Project

Looking forward to it! 👍 

Posted : 10/12/2023 6:30 am
LiokoDev liked
Tobias Stanzel
(@tobias-stanzel)
Eminent Member
Topic starter answered:
RE: (Un)Official Prusa Enclosure "Smart Box" - Project

Here you can see a heating for abs with heater on and heatbed at 100c, took 25min to heat up. 

 

Posted : 10/12/2023 8:39 pm
LiokoDev liked
Tobias Stanzel
(@tobias-stanzel)
Eminent Member
Topic starter answered:
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 😉 

Posted : 10/12/2023 8:41 pm
LiokoDev liked
Tobias Stanzel
(@tobias-stanzel)
Eminent Member
Topic starter answered:
RE: (Un)Official Prusa Enclosure "Smart Box" - Project

I finished the first shot of the fritzing sketch, took longer than building it 😉 

 

Posted : 11/12/2023 12:36 am
LiokoDev liked
Christopher72
(@christopher72)
Eminent Member
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!

Posted : 18/12/2023 4:23 am
Tobias Stanzel
(@tobias-stanzel)
Eminent Member
Topic starter answered:
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! 

 

Posted : 02/01/2024 12:17 am
Page 1 / 2
Share: