Fume Extractor upgraded with Particulate Matter sensor and now running ESPHome

I recently purchased a fume extractor but was completely underwhelmed by how poorly the fan was controlled. The speed varied between 0 and 30% and would then stay at max until 100%.

This was the original product:

The fan was a regular 24V fan without PWM control (even though the part number of the fan took me to a datasheet that showed specifications of an identical fan with power + pwm + tach. Anyway, even trying to control the fan with an ESP32 did not make a difference so… I put in a better fan that takes PWM!

I tore out the 80s looking PWM motor controller (meant for regular motors) and built my own using the outer casing of the original one.

(Sorry for the horrible pictures…)


Installed

View of bottom with particulate sensor (PMS5003)

Now my question… looking for suggestions…

Is there a better, less bulky sensor that you recommend? I know there are many and I read about a lot of them but do not want to start buying them all to figure it out if possible.

I am guessing that the ideal placement is in a hood at the entrance of the pipe (see amazon picture). I don’t have a 3D printer though… and this came without a hood.

I thought about putting it inside the box where the filter (before the filter of course) is but there is no room.

I am at the stage where I need to make a hole for a panel mount connector to connect the sensor… all I could find was an aviation style connector, also commonly found on ham radios. It is a bit big but would work.

Before I make the huge hole, I was hoping to get suggestions, ideas, on sensor, connector and placement of the sensor.

On the software side… of course it is controllable via HA…

This is my code:

substitutions:
  devicename: fume-extractor
  devicename_no_dashes: fume_extractor
  friendly_devicename: "Fume Extractor"
  device_description: "Fume Extractor"
  update_interval_s: "1s"
  update_interval_wifi: "60s"
  low_speed: "20"
  medium_speed: "60"
  high_speed: "100"
  manual_fan_speed_step: "5"
    
esphome:
  name: ${devicename}
  comment: ${device_description}
  on_boot:
    then:
      - lambda: |-
          id(device_on_off_ha).publish_state(true);
          id(set_max_particulate_ha).execute();

esp32:
  board: nodemcu-32s
  framework:
    type: arduino

#Investigate globals to see if they always get written on only if marked as restore.
#This setting preserves the flash by limiting writes
preferences:
  flash_write_interval: 5min

# Enable logging
logger:
  baud_rate: 0 #Disable logger on UART since it is being used for the sensor. Helps not overload.

# Enable Home Assistant API
api: 
  password: !secret api_pwd

ota:
  password: !secret ota_pwd

wifi:
  ssid: !secret iot_wifi_ssid
  password: !secret iot_wifi_password

#Faster than DHCP. Also use if can't reach because of name change
  manual_ip:
    static_ip: 192.168.3.208
    gateway: 192.168.3.1
    subnet: 255.255.255.0
    dns1: 192.168.1.25
    dns2: 192.168.1.26

  #fast_connect: true

#Manually override what address to use to connect to the ESP.
#Defaults to auto-generated value. Example, if you have changed your
#static IP and want to flash OTA to the previously configured IP address.
  use_address: 192.168.3.208
  
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "${devicename}"
    password: !secret iot_wifi_password

web_server:
  port: 80
  include_internal: true

captive_portal:

# Sync time with Home Assistant
time:
  - platform: homeassistant
    id: ha_time

text_sensor:
  - platform: wifi_info
    ip_address:
      name: "${friendly_devicename}: IP"
      id: device_ip
      icon: "mdi:ip-outline"
      update_interval: ${update_interval_wifi}
    ssid:
      name: "${friendly_devicename}: SSID"
      icon: "mdi:wifi-settings"
      update_interval: ${update_interval_wifi}
    bssid:
      name: "${friendly_devicename}: BSSID"
      icon: "mdi:wifi-settings"
      update_interval: ${update_interval_wifi}
    mac_address:
      name: "${friendly_devicename}: MAC"
      icon: "mdi:network-outline"
    scan_results:
      name: "${friendly_devicename}: Wifi Scan"
      icon: "mdi:wifi-refresh"
      disabled_by_default: true

#https://esphome.io/guides/automations.html?highlight=restore_value#bonus-2-global-variables
globals: ##to set default reboot behavior
  # Wifi variables
  - id: wifi_connection
    type: bool
    restore_value: no
    initial_value: "false"

  # Menu variables   
  - id: menu_index
    type: int
    restore_value: no
    initial_value: '0'

  - id: device_on_off
    type: bool
    restore_value: no
    initial_value: "true"
  
  - id: display_on_off
    type: bool
    restore_value: no
    initial_value: "true"

  # Speed variables
  - id: fan_speed
    type: float
    restore_value: no
    initial_value: '0.0'
  - id: fan_rpm
    type: float
    restore_value: no
    initial_value: '0.0'
  - id: pwm_man_speed
    type: int
    restore_value: no
    initial_value: '0'
  - id: man_speed
    type: int
    restore_value: no
    initial_value: '0'
  - id: speed_index
    type: int
    restore_value: no
    initial_value: '0'
  - id: speed_name
    type: std::string
    restore_value: no # Strings cannot be saved/restored
    initial_value: '"Low"'

  # Sensor variables
  - id: particulate_1
    type: int
    restore_value: no
    initial_value: '0'
  - id: particulate_2_5
    type: int
    restore_value: no
    initial_value: '0'
  - id: particulate_10
    type: int
    restore_value: no
    initial_value: '0'
  - id: max_particulate
    type: int
    restore_value: yes
    initial_value: '250'

font:
  # gfonts://family[@weight]
  - file: "gfonts://Roboto"
    id: roboto
    size: 12

  - file: "gfonts://Roboto"
    id: roboto_large
    size: 32
 
  - file: "fonts/materialdesignicons-webfont.ttf"
    id: icon_font
    size: 12
    glyphs: [
      "\U000F05A9", #wifi
      "\U000F05AA", #no wifi
      ]

uart:
  rx_pin: 16
  tx_pin: 17
  baud_rate: 9600

i2c:
  - id: bus_a
    sda: 19
    scl: 18
    scan: true

display:
  - platform: ssd1306_i2c
    id: device_display
    model: 'SH1106 128x64'
    address: 0x3C
    rotation: 180
    flip_x: false
    flip_y: false
    offset_y: 0
    offset_x: 0
    external_vcc: true
    update_interval: 150ms
    pages:
      - id: standby_page
        lambda: |-
          it.rectangle(0, 0, 128, 64);
          it.print(4, 1, id(roboto), "FUME EXTRACTOR");
          it.printf(112, 2, id(icon_font), "%s", id(wifi_connection) ? "\U000F05A9" : "\U000F05AA");
          it.line(0, 16, 128, 16);
          it.printf(4, 18, id(roboto), "Mode: Standby");
          it.printf(4, 32, id(roboto), "IP: %s", id(device_ip).state.c_str());
          it.printf(4, 46, id(roboto), "Press knob to turn on.");
      - id: auto_0_100_page
        lambda: |-
          it.rectangle(0, 0, 128, 64);
          it.print(4, 1, id(roboto), "FUME EXTRACTOR");
          it.printf(112, 2, id(icon_font), "%s", id(wifi_connection) ? "\U000F05A9" : "\U000F05AA");
          it.line(0, 16, 128, 16);
          it.printf(4, 18, id(roboto), "Mode: Auto 0-100%%");
          it.printf(4, 32, id(roboto), "Fan: %d%%", int(id(fan_speed) * 100));
          it.printf(4, 46, id(roboto), "PM 2.5: %d ug", id(particulate_2_5));
      - id: auto_l_m_h_page
        lambda: |-
          it.rectangle(0, 0, 128, 64);
          it.print(4, 1, id(roboto), "FUME EXTRACTOR");
          it.printf(112, 2, id(icon_font), "%s", id(wifi_connection) ? "\U000F05A9" : "\U000F05AA");
          it.line(0, 16, 128, 16);
          it.printf(4, 18, id(roboto), "Mode: Auto L-M-H");
          it.printf(4, 32, id(roboto), "Fan: %s (%d%%)", id(speed_name).c_str(), int(id(fan_speed) * 100));
          it.printf(4, 46, id(roboto), "PM 2.5: %d ug", id(particulate_2_5));
      - id: manual_0_100_page
        lambda: |-
          it.rectangle(0, 0, 128, 64);
          it.print(4, 1, id(roboto), "FUME EXTRACTOR");
          it.printf(112, 2, id(icon_font), "%s", id(wifi_connection) ? "\U000F05A9" : "\U000F05AA");
          it.line(0, 16, 128, 16);
          it.printf(4, 18, id(roboto), "Mode: Manual 0-100%%");
          it.printf(4, 32, id(roboto), "Fan: %d%%", int(id(fan_speed) * 100));
          it.printf(4, 46, id(roboto), "PM 2.5: %d ug", id(particulate_2_5));
      - id: manual_l_m_h_page
        lambda: |-
          it.rectangle(0, 0, 128, 64);
          it.print(4, 1, id(roboto), "FUME EXTRACTOR");
          it.printf(112, 2, id(icon_font), "%s", id(wifi_connection) ? "\U000F05A9" : "\U000F05AA");
          it.line(0, 16, 128, 16);
          it.printf(4, 18, id(roboto), "Mode: Manual L-M-H");
          it.printf(4, 32, id(roboto), "Fan: %s (%d%%)", id(speed_name).c_str(), int(id(fan_speed) * 100));
          it.printf(4, 46, id(roboto), "PM 2.5: %d ug", id(particulate_2_5));
      - id: off_page
        lambda: |-
          it.print(32, 1, id(roboto_large), "OFF");
          it.printf(2, 32, id(roboto), "Long press knob to");
          it.printf(2, 46, id(roboto), "turn on.");
select:
  - platform: template
    name: "${friendly_devicename}: Menu"
    id: menu_option_ha
    optimistic: true
    options:
      - "Standby"
      - "Auto 0-100%"
      - "Auto L-M-H"
      - "Manual 0-100%"
      - "Manual L-M-H"
    initial_option: "Standby"
    on_value:
      then:
        - lambda: |-
            if (id(device_on_off)) {
              // Get active menu index in HA and set it on device
              auto index = id(menu_option_ha).active_index();
              id(menu_index) = index.value() - 1; //Subtract 1 as select index in HA starts from 1
              id(menu_change).execute();
            }

number:
  - platform: template
    name: "${friendly_devicename}: Fan Speed"
    id: fan_speed_ha
    icon: "mdi:speedometer"
    optimistic: true #Not sure what this does, research. leave? remove?
    min_value: 0
    max_value: 100
    step: ${manual_fan_speed_step}
    on_value:
      then:
        - lambda: |-
            if (id(menu_index) == 3) {
              id(pwm_man_speed) = int(id(fan_speed_ha).state);
              id(report_fan_speed).publish_state(int(id(fan_speed_ha).state));
            };

  - platform: template
    name: "${friendly_devicename}: Max Particulate"
    id: max_particulate_ha
    optimistic: true #Not sure what this does, research. leave? remove?
    min_value: 50
    max_value: 500
    step: 5
    on_value:
      then:
        - lambda: |-
            id(max_particulate) = int(id(max_particulate_ha).state);

switch:
  - platform: restart
    name: "${friendly_devicename}: Restart"

  - platform: template
    name: "${friendly_devicename}"
    id: device_on_off_ha
    optimistic: true
    turn_on_action:
      - globals.set:
          id: device_on_off
          value: 'true'
      - script.execute: device_turn_on
    turn_off_action:
      - globals.set:
          id: device_on_off
          value: 'false'
      - script.execute: device_turn_off

button:
  - platform: safe_mode
    name: "${friendly_devicename}: Restart (Safe Mode)"

binary_sensor:
  - platform: gpio
    id: device_button
    internal: true
    pin:
      number: 27
      inverted: true
      mode:
        input: true
        pullup: true
# This debounce filter causes many button presses to be missed ?!?!
#    filters:
#      - delayed_on: 10ms
    on_multi_click:
    # Single Long Click
    - timing:
        - ON for 1.0s to 3.0s
        - OFF for at least 0.2s
      then:
        - logger.log: "Single Long Click"
        - switch.toggle: device_on_off_ha
        #- script.execute: display_on
    # Single Short Click
    - timing:
        - ON for at most 0.8s
        - OFF for at least 0.1s
      then:
        - logger.log: "Single Short Click"
        - lambda: |-
            if (id(device_on_off)) {
              id(menu_change).execute();
              id(update_menu_ha).execute();
            } else {
              id(display_on).execute();
              id(display_off_again).execute();
            }
            

output:
  - platform: ledc
    pin: 26
    frequency: 19531 Hz
    id: fan_pwm

  - platform: gpio
    id: pms5003_sensor
    pin:
      number: 4
      mode:
        output: true

sensor:
  - platform: wifi_signal
    name: "${friendly_devicename}: WiFi Signal"
    update_interval: ${update_interval_wifi}
    device_class: signal_strength

  - platform: pulse_counter
    id: rpm
    internal: true
    pin: 23
    accuracy_decimals: 0
    update_interval: "5s"
    on_value:
        - globals.set:
            id: fan_rpm
            value: !lambda 'return id(rpm).state / 2;'

  - platform: template
    id: report_fan_speed
    name: "${friendly_devicename}: Fan Speed"
    icon: "mdi:speedometer"
    unit_of_measurement: "%"
    accuracy_decimals: 0
    update_interval: never
    #update_interval: ${update_interval_s}
    #lambda: return id(fan_speed) * 100;

  - platform: template
    id: report_fan_rpm
    name: "${friendly_devicename}: Fan RPM"
    icon: "mdi:speedometer"
    accuracy_decimals: 0
    update_interval: never
    #update_interval: ${update_interval_s}
    #lambda: return id(fan_rpm).state;

  - platform: rotary_encoder
    id: encoder
    internal: true
    pin_a:
      number: 32
      mode:
        input: true
        pullup: true
    pin_b:
      number: 25
      mode:
        input: true
        pullup: true
    on_clockwise:
      #- logger.log: "Manual Speed Increase"
      - lambda: |-
          if (id(device_on_off)) {
            if (id(menu_index) == 3) {
              if (id(pwm_man_speed) >= 100) {
                id(pwm_man_speed) = 100;
              } else {
                id(pwm_man_speed) += ${manual_fan_speed_step};
              }
              // Set fan_speed_ha to pwm_man_speed
              id(update_fan_speed_ha).execute();
            }
            if (id(menu_index) == 4) {
              if (id(speed_index) >= 2) {
                id(speed_index) = 2;
              } else {
                id(speed_index) += 1;
              }
              id(set_speed_level).execute();
            }
          } else {
            id(display_on).execute();
            id(display_off_again).execute();
          } 
    on_anticlockwise:
      #- logger.log: "Manual Speed Decrease"
      - lambda: |-
          if (id(device_on_off)) {
            if (id(menu_index) == 3) {
              if (id(pwm_man_speed) <= 0) {
                id(pwm_man_speed) = 0;
              } else {
                id(pwm_man_speed) -= ${manual_fan_speed_step};
              }
              // Set fan_speed_ha to pwm_man_speed
              id(update_fan_speed_ha).execute();
            }
            if (id(menu_index) == 4) {
              if (id(speed_index) <= 0) {
                id(speed_index) = 0;
              } else {
                id(speed_index) -= 1;
              }
              id(set_speed_level).execute();
            }
          } else {
            id(display_on).execute();
            id(display_off_again).execute();
          }      
    filters:
      - delta: 1.0

  - platform: pmsx003
    type: PMSX003
    pm_1_0:
      id: fumes_particulate_1
      on_value:
        - globals.set:
            id: particulate_1
            value: !lambda 'return id(fumes_particulate_1).state;'
    pm_2_5:
      id: fumes_particulate_2_5
      on_value:
        - globals.set:
            id: particulate_2_5
            value: !lambda 'return id(fumes_particulate_2_5).state;'
    pm_10_0:
      id: fumes_particulate_10
      on_value:
        - globals.set:
            id: particulate_10
            value: !lambda 'return id(fumes_particulate_10).state;'

  - platform: template
    id: report_particulate_1
    name: "${friendly_devicename}: Particulate <1.0µm"
    lambda: return id(particulate_1);
    device_class: pm1
    unit_of_measurement: µg/m³
    accuracy_decimals: 0
    update_interval: never

  - platform: template
    id: report_particulate_2_5
    name: "${friendly_devicename}: Particulate <2.5µm"
    lambda: return id(particulate_2_5);
    device_class: pm25
    unit_of_measurement: µg/m³
    accuracy_decimals: 0
    update_interval: never

  - platform: template
    id: report_particulate_10
    name: "${friendly_devicename}: Particulate <10.0µm"
    device_class: pm10
    unit_of_measurement: µg/m³
    lambda: return id(particulate_10);
    accuracy_decimals: 0
    update_interval: never
    

interval:
  - interval: 10s
    then:
      - lambda: |-
          if (WiFi.status() == WL_CONNECTED) {
            id(wifi_connection) = true;
          } else {
            id(wifi_connection) = false;
          }

  - interval: 1s
    then:
          lambda: |-
            switch (id(menu_index)) {
              case 0:
                id(fan_speed) = 0.0;
                break;
              case 1:
                if (id(fumes_particulate_2_5).state < 12.0) {
                    id(fan_speed) = 0.1;
                }
                else if ((id(fumes_particulate_2_5).state > 12.0) and (id(fumes_particulate_2_5).state <= 35.0)) {
                    id(fan_speed) = 0.2;
                }
                else if ((id(fumes_particulate_2_5).state > 35.0) and (id(fumes_particulate_2_5).state <= 55.0)) {
                    id(fan_speed) = 0.3; 
                }
                else if ((id(fumes_particulate_2_5).state > 55.0) and (id(fumes_particulate_2_5).state <= 150.0)) {
                    id(fan_speed) = 0.4; 
                }
                else if ((id(fumes_particulate_2_5).state > 150.0) and (id(fumes_particulate_2_5).state <= 250.0)) {
                    id(fan_speed) = 0.6; 
                }
                else if ((id(fumes_particulate_2_5).state > 250.0) and (id(fumes_particulate_2_5).state <= 350.0)) {
                    id(fan_speed) = 0.8;
                }
                else if ((id(fumes_particulate_2_5).state > 350.0)) {
                    id(fan_speed) = 1.0;
                    ESP_LOGD("ALERT", "OVER 350µg/m³! Setting fan to 100");
                }
                break;
              case 2:
                if (id(fumes_particulate_2_5).state <= 55.0) {
                    id(fan_speed) = float(id(${low_speed}) / 100.0);
                }
                else if ((id(fumes_particulate_2_5).state > 55.0) and (id(fumes_particulate_2_5).state <= 250.0)) {
                    id(fan_speed) = float(id(${medium_speed}) / 100.0); 
                }
                else if ((id(fumes_particulate_2_5).state > 250.0)) {
                    id(fan_speed) = float(id(${high_speed}) / 100.0);
                    ESP_LOGD("ALERT", "OVER 250µg/m³! Setting fan to full speed!");
                }
                break;
              case 3:
                id(fan_speed) = id(pwm_man_speed) / 100.0;
                break;
              case 4:
                id(fan_speed) = id(man_speed) / 100.0;
                break;
              default:
                break;
                }
            
            if(id(menu_index) != 0) {
              id(fan_pwm).set_level(id(fan_speed));
              id(report_fan_speed).publish_state(int(id(fan_speed) * 100));
              id(report_fan_rpm).publish_state(int(id(fan_rpm)));
              id(report_particulate_1).publish_state(id(particulate_1));      // Report Particulate Sensor
              id(report_particulate_2_5).publish_state(id(particulate_2_5));  // Report Particulate Sensor
              id(report_particulate_10).publish_state(id(particulate_10));    // Report Particulate Sensor
              ESP_LOGD("PWM", "PWM: %d%%" , int(id(fan_speed) * 100));
            //ESP_LOGD("PWM", "PARTICULATE THRESHOLD: %d" , int(id(max_fan_speed_particulate)));
            }
            //ESP_LOGD("DEBUG", "Device ON_OFF? %d" , id(device_on_off));
            //ESP_LOGD("DEBUG", "Active Menu: %d", id(menu_index));

script:
  - id: menu_change
    then:
      - lambda: |-
          if (id(device_on_off)) {
            if (id(menu_index) == 4) {
              id(menu_index) = 0;
            } else {
              id(menu_index) += 1;
            }
            switch (id(menu_index)) {
              case 0:
                //Standby
                id(reset_everything_to_default).execute();
                break;
              case 1:
                //Auto 0-100%
                id(pms5003_sensor).turn_on();
                break;
              case 2:
                //Auto L-M-H
                id(speed_index) = 0;
                id(set_speed_level).execute();
                id(pms5003_sensor).turn_on();
                break;
              case 3:
                //Manual 0-100%
                id(pwm_man_speed) = int(id(fan_speed_ha).state);  // Set speed to what it is in HA
                id(pms5003_sensor).turn_on();
                break;
              case 4:
                //Manual L-M-H
                id(speed_index) = 0;
                id(set_speed_level).execute();
                id(pms5003_sensor).turn_on();
                break;
              default:
                //Undefined
                id(pms5003_sensor).turn_off();
                break;
            }
            ESP_LOGD("DEBUG", "Button Press: Menu: %d", id(menu_index));
          }
      - display.page.show: !lambda |-
          switch (id(menu_index)) {
            case 0:
              //Standby
              return id(standby_page);
              break;
            case 1:
              //Auto 0-100%
              return id(auto_0_100_page);
              break;
            case 2:
              //Auto L-M-H
              return id(auto_l_m_h_page);
              break;
            case 3:
              //Manual 0-100%
              return id(manual_0_100_page);
              break;
            case 4:
              //Manual L-M-H
              return id(manual_l_m_h_page);
              break;
            default:
              //Undefined
              return id(standby_page);
              break;
          }

  # Updates the HA menu to match the device menu
  - id: update_menu_ha
    then:
      - lambda: |-
          auto call = id(menu_option_ha).make_call();
          call.set_index(id(menu_index));
          call.perform();

  # Updates the HA fan speed to match the device fan speed
  - id: update_fan_speed_ha
    then:
      - lambda: |-
          auto call = id(fan_speed_ha).make_call();
          call.set_value(id(pwm_man_speed));
          call.perform();

  # Updates max particulate in HA to setting (gvar) that is saved to flash
  - id: set_max_particulate_ha
    then:
      - lambda: |-
          auto call = id(max_particulate_ha).make_call();
          call.set_value(id(max_particulate));
          call.perform();

  - id: set_speed_level
    then:
      - lambda: |-
          switch (id(speed_index)) {
            case 0:
              id(speed_name) = "Low";
              id(man_speed) = id(${low_speed});
              break;
            case 1:
              id(speed_name) = "Medium";
              id(man_speed) = id(${medium_speed});
              break;
            case 2:
              id(speed_name) = "High";
              id(man_speed) = id(${high_speed});
              break;
            default:
              id(speed_name) = "Error";
              break;
          }

  - id: reset_everything_to_default
    then:
      - lambda: |-
          id(fan_pwm).set_level(0.0);                   // Turn off fan - only needed when switching modes after fan was on
          id(report_fan_speed).publish_state(0);        // Reset Fan Speed
          id(report_fan_rpm).publish_state(0);          // Reset Fan RPM
          id(particulate_1) = 0;                        // Reset Global Variables
          id(particulate_2_5) = 0;                      // Reset Global Variables
          id(particulate_10) = 0;                       // Reset Global Variables
          id(report_particulate_1).publish_state(0);    // Report Particulate Sensor
          id(report_particulate_2_5).publish_state(0);  // Report Particulate Sensor
          id(report_particulate_10).publish_state(0);   // Report Particulate Sensor
          id(pms5003_sensor).turn_off();                // Put sensor to sleep to preserve its life

  - id: device_turn_on
    then:
      - lambda: |-
          auto index = id(menu_option_ha).active_index();  // Get active menu index in HA
          id(menu_index) = index.value() - 1;              //Subtract 1 as select index start from 1
          id(menu_change).execute();
          id(device_display).turn_on();  

  - id: device_turn_off
    then:
      - lambda: |-
          id(menu_index) = 0;             // Default back to Standby menu on device
          id(update_menu_ha).execute();   // Update menu in HA to reflect device
          id(reset_everything_to_default).execute();
      - display.page.show: off_page
      - delay: 10s
      - lambda: |-
          if (!id(device_on_off)) {
            id(device_display).turn_off();
          }

  - id: display_on
    then:
      - lambda: |-
          if (!id(device_on_off)) {
            id(device_display).turn_on();
            id(display_on_off) = true;
            id(show_off_page).execute();
            //ESP_LOGD("DEBUG", "display_on");
          }

  - id: display_off_again
    then:
      - delay: 10s
      - lambda: |-
          if (!id(device_on_off)) {
            id(device_display).turn_off();
            id(display_on_off) = false;
            //ESP_LOGD("DEBUG", "display_off_again - device off");
          }

  - id: show_off_page
    then:
      - display.page.show: off_page

The code is likely very far from perfect as I am not a programmer. I was trying to be able to change any setting from both HA and the device with the changes being reflected immediately. The extractor has multiple modes including a standby mode and a soft off mode. It is still work in progress and there is a bug I just introduced that causes the menu to get out of sync so if you use this code, know that there is an annoying bug. On top of that, the button timing is pretty annoying… by making it faster response it got pickier too. Anyway that is for another thread.

EDIT: Updated YAML to fix the bugs. I think it works well now.

1 Like

Nice!

Couple of comments:

  1. I think you have the right sensor. Yes they’re a little bulky.
  2. Be thoughtful about the inlet and outlet of the pms sensor. I think they’re designed to have a certain flow rate of air through them (they have an internal fan), so I would avoid interference with that due to the extractor fan air flow…
  3. Might not be so important for you as I guess the sensor isn’t on that large a % of the time, but consider life extension strategies.
  4. You could consider having this as just a stand alone sensor that is always on in the room. Throw some other sensors on there too…

Great feedback @Mahko_Mahko !

When considering where to place the sensor, I was wondering what the suction would do to the flow inside the sensor. The close to 7000rpm fan inside the extractor would certainly overpower the little fan inside the sensor even at low rpm not letting it get much flow…

While I was developing it I actually used an IKEA Vindriktning Air Quality Sensor and noticed that just having it on the table close to where I was making smoke worked quite well however I am hoping to have a faster response by having the sensor close to the hood (one I am planning on adding). I’ll have to do some tests with duct tape :wink:

The PMS5003 has a pin that can be used to put the sensor to sleep (stops fan too) so I implemented that already but the sensor is on all the time the extractor is in a suction mode because of the response time. In standby or soft off, the sensor never activates.

As for #4, I am building that which is why I had the sensor that I then used for the fume extractor. The only issue with having a separate device is that I wanted to make the fume extractor work 100% with or without HA. There is no way to do ESP32 to ESP32 link in ESPHome, right?

EDIT: I saw the other thread and the multi sensor you made. Nice work!

1 Like

There’s this for esp to esp communication. I don’t know much about it except that it exists;).

You might find that affecting the fan flow may not even matter too much for your use case. Or you might be able to calibrate it away. Not sure …

Maybe just try a few things and take a look at some trending charts of the measurements and see what works (I like Grafana for that kind of thing).

I think my first instinct would be to mount it on the outside about here…

ESP-NOW Two-Way Communication Between ESP32 Boards | Random Nerd Tutorials)%20between%20ESP32%20boards.