QuietCool smart control ESP32

Same here , don’t want it flash the firmware , perhaps an esp32 that can capture data and relay over WiFi or a raspberry pi or something . Would be nice to have data trend

Has anyone with a newer board revision tried flashing this with ESPHome? If not, looks like I may be the first… hoping it works.

Anything I should be on the look out for or things I can check to confirm any changes to the board haven’t effected our use here?

The unit I just received came with board version REV:A 2022.05.28 and looks mostly the same from what I saw at first glance. Hoping maybe it was just a very minor change that will have no effect. Wish me luck I guess.

Building upon the earlier solution @TCecil the following configuration supports both a 2 and 3 speed fan configuration. Earlier solutions were for just 3 speed fans but I have a 2 speed (no Medium speed).

My solution also adds a switch that support automatic control of the fan state/speed based on the current temperature and a set of number components.

This allows you to set a High, Medium, Low, Off temperatures and enable “Auto Speed Management”. You can even bind them in Home Assistant to UI elements to make a nice management console.

The benefit of this solution over using Home Assistant automations is that the fan will continue to automatically control its state based on the current temperature even if it loses wifi connectivity completely.

esphome:
  name: attic-fan
  friendly_name: Attic # just name Attic and not Attic Fan so outputs like Temperature become "Attic Temperature" and not "Attic Fan Temperature"
  on_boot: #Display cascading LED's to show full boot
    priority: 800 #run after full boot
    then:
      - delay: 1s
      - light.turn_on: temp_hum_led_light
      - light.turn_on: speed_led_light
      - light.turn_on: bluetooth_led_light
      - delay: 1s
      - light.turn_off: bluetooth_led_light
      - delay: 0.5s
      - light.turn_off: speed_led_light
      - delay: 0.5s
      - light.turn_off: temp_hum_led_light
  on_loop:
    then:
      - lambda: |-
          //force fan component to sync with speed select component
          if (id(on_off_switch).state && id(attic_fan).speed != id(speed_mode) && id(speed_mode) > 0){
            auto call = id(attic_fan).turn_on();            
            call.set_speed(id(speed_mode));
            call.perform();
          }

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:
  - platform: esphome
    password: "1234"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Attic-Fan"
    password: "1234"

captive_portal:
  
globals:
  - id: speed_mode
    type: int
    initial_value: '1'
  - id: timer_offset
    type: long
    initial_value: '0'
  - id: timer_stop
    type: long
    initial_value: '0'
  - id: timer_en
    type: bool
    initial_value: 'false'
  - id: select_lock
    type: bool
    initial_value: 'false'
  - id: failsafe_temp
    type: float
    initial_value: '182'
  # set to 'true' if your Fan supports 3 speeds (also update lines below with comments as well)
  - id: three_speed_fan
    type: bool
    initial_value: 'false'

time:
  - platform: homeassistant
    id: homeassistant_time

number:
  - platform: template
    id: high_speed_above_temp
    device_class: temperature
    entity_category: config
    unit_of_measurement: "°F"
    name: "Fan High Speed On Temp"
    restore_value: True
    min_value: 77
    max_value: 125
    initial_value: 100
    optimistic: True
    step: 1
    set_action:
      - if:
          condition:
            lambda: 'return id(auto_temp_control).state && id(attic_temperature).state >= x && id(attic_temperature).state < id(failsafe_temp);'
          then:
            - fan.turn_on: 
                id: attic_fan
                speed: !lambda "return id(three_speed_fan) ? 3 : 2;"
      - if:
          condition:
            lambda: 'return id(auto_temp_control).state && id(attic_temperature).state < x && id(attic_temperature).state >= (id(three_speed_fan) ? id(medium_speed_above_temp).state : id(low_speed_above_temp).state) && id(attic_temperature).state < id(failsafe_temp);'
          then:
            - fan.turn_on: 
                id: attic_fan
                speed: !lambda "return id(three_speed_fan) ? 2 : 1;"
# Only supported on Fans that have 3 speeds (see the global 'three_speed_fan' for info)                
  - platform: template
    id: medium_speed_above_temp
    device_class: temperature
    entity_category: config
    unit_of_measurement: "°F"
    name: "Fan Medium Speed On Temp"
    restore_value: True
    min_value: 76
    max_value: 115
    initial_value: 95
    optimistic: True
    step: 1
    set_action:
      - if:
          condition:
            lambda: 'return id(three_speed_fan) && id(auto_temp_control).state && id(attic_temperature).state < id(high_speed_above_temp).state && id(attic_temperature).state >= x && id(attic_temperature).state < id(failsafe_temp);'
          then:
            - fan.turn_on: 
                id: attic_fan
                speed: 2
      - if:
          condition:
            lambda: 'return id(three_speed_fan) && id(auto_temp_control).state && id(attic_temperature).state < x && id(attic_temperature).state >= id(low_speed_above_temp).state && id(attic_temperature).state < id(failsafe_temp);'
          then:
            - fan.turn_on: 
                id: attic_fan
                speed: 1
  - platform: template
    id: low_speed_above_temp
    entity_category: config
    device_class: temperature
    unit_of_measurement: "°F"
    name: "Fan Low Speed On Temp"
    restore_value: True
    min_value: 75
    max_value: 110
    initial_value: 90
    optimistic: True
    step: 1
    set_action:
      - if:
          # Turn the Fan to Low Speed if the temp is above the low speed temp and below the high (or medium for 3 speed fans) speed temp (and auto controls are enabled)
          condition:
            lambda: 'return id(auto_temp_control).state && id(attic_temperature).state < (id(three_speed_fan) ? id(medium_speed_above_temp).state : id(high_speed_above_temp).state) && id(attic_temperature).state >= x && id(attic_temperature).state < id(failsafe_temp);'
          then:
            - fan.turn_on: 
                id: attic_fan
                speed: 1
      - if:
          # Turn off the Fan if it was previously on because its temp was above the previous low_speed_above_temp but is now below it
          condition:
            lambda: 'return id(auto_temp_control).state && id(attic_temperature).state < x && id(attic_temperature).state >= id(low_speed_above_temp).state;'
          then:
            - fan.turn_off: attic_fan
  - platform: template
    id: off_below_temp
    device_class: temperature
    unit_of_measurement: "°F"
    name: "Fan Off Temp"
    restore_value: True
    entity_category: config
    min_value: 65
    max_value: 100
    initial_value: 85
    optimistic: True
    step: 1
    set_action:
      - if:
          # Turns off the fan if the current temp is below the Fan Off Temp
          condition:
            lambda: 'return id(auto_temp_control).state && id(attic_temperature).state < x;'
          then:
            - fan.turn_off: attic_fan

select:
  - platform: template
    name: "Fan Speed Select"
    id: speed_select
    update_interval: 6s
    options:
      - "Low"
      # Uncomment for 3 speed fans
      # - "Medium"
      - "High"
    set_action:
      - lambda: |-
          int prev_mode = id(speed_mode);
          if (x == "Low") {
            id(speed_mode) = 1;
          } else if (x == "High" && id(three_speed_fan)) {
            id(speed_mode) = 3;
          } else {
            id(speed_mode) = 2;
          }
          
          if (id(speed_mode) != prev_mode && id(on_off_switch).state){
            if (id(speed_mode) == 1){
              id(Speed_Low).turn_on();
            } else if (id(speed_mode) == 2) {
              if (id(three_speed_fan))
                id(Speed_Medium).turn_on();
              else
                id(Speed_High).turn_on();
            } else if (id(speed_mode) == 3) {
              id(Speed_High).turn_on();
            }
          }
          
          //if speed is changed in the middle of operation change fan component output to match
          //select_lock will prevent excessive commands sent to fan compnent in the turn on process
          if (id(on_off_switch).state && (id(attic_fan).speed != id(speed_mode) || id(speed_mode) != prev_mode) && !id(select_lock)){        
            auto call = id(attic_fan).turn_on();
            call.set_speed(id(speed_mode));
            call.perform();            
          } else if (id(select_lock)){
            //clear select_lock if it sets to true
            id(select_lock) = false;
          }
          
    lambda: |-
      if (id(Speed_Low).state && id(speed_select).state != "Low") {
        return {"Low"}; 
      } else if (id(Speed_Medium).state && id(speed_select).state != "Medium") {
        return {"Medium"};
      } else if (id(Speed_High).state && id(speed_select).state != "High") {
        return {"High"};
      } 
      if(id(speed_select).state == "") {
        return {"Low"};
      }
      
      return{};
      
  - platform: template
    name: "Fan Timer"
    id: timer
    update_interval: 20s
    options:
      - "off"
      - "1 hour"
      - "2 hours"
      - "4 hours"
      - "8 hours"
      - "12 hours"
    set_action:
      - lambda: |-
        
          if (x == "1 hour"){
            id(timer_offset) = 3600;
            id(timer_en) = true;
          } else if (x == "2 hours"){
            id(timer_offset) = 7200;
            id(timer_en) = true;
          } else if (x == "4 hours"){
            id(timer_offset) = 14400;
            id(timer_en) = true;
          } else if (x == "8 hours"){
            id(timer_offset) = 28800;
            id(timer_en) = true;
          } else if (x == "12 hours"){
            id(timer_offset) = 43200;
            id(timer_en) = true;
          } else if (x == "off") {
            id(timer_offset) = 0;
            id(timer_en) = false;
          }
          
          if (id(timer_en) && id(on_off_switch).state){
            id(timer_stop) = id(homeassistant_time).now().timestamp + id(timer_offset); // when the fan should turn off
          }
          
    lambda: |-
    
      if (id(timer_en) && id(on_off_switch).state && id(timer_stop) != 0) {
        auto time_remaining = id(timer_stop) - id(homeassistant_time).now().timestamp;
        
        if (time_remaining <= 0) {
          id(on_off_switch).turn_off();
          id(timer_en) = false;
          id(time_left).publish_state(0);
          return {"off"};
        } else if (time_remaining <= 3600 && id(timer).state != "1 hour"){
          return {"1 hour"};
        } else if (time_remaining > 3600 && time_remaining <= 7200 && id(timer).state != "2 hours"){
          return {"2 hours"};
        } else if (time_remaining > 7200 && time_remaining <= 14400 && id(timer).state != "4 hours"){
          return {"4 hours"};
        } else if (time_remaining > 14400 && time_remaining <= 28800 && id(timer).state != "8 hours"){
          return {"8 hours"};
        } else if (time_remaining > 28800 && time_remaining <= 43200 && id(timer).state != "12 hours"){
          return {"12 hours"};
        }
      } 
      if (id(timer).state == ""){
        return {"off"};
      }    
      
      return {};

#Front Buttons
binary_sensor:
  - platform: gpio
    name: "Fan Button 2 - Pair"
    pin:
      number: 26
      inverted: true
      mode:
        input: true
        pullup: true
    internal: true
    filters:
      - delayed_on_off: 100ms
  - platform: gpio
    name: "Fan Button 1 - Test"
    pin:
      number: 27
      inverted: true
      mode:
        input: true
        pullup: true
    internal: true
    filters:
      - delayed_on_off: 100ms
    on_press:
      then:
        - fan.cycle_speed: attic_fan
light:
#Note: There are 4 LEDs the far left Red LED is tied directly to power and not controllable.
  - platform: binary
    name: "Fan Right LED - Bluetooth"
    output: bluetooth_led
    id: bluetooth_led_light
    restore_mode: ALWAYS_OFF
    internal: true
    
  - platform: binary
    name: "Fan Middle LED - Speed"
    output: speed_led
    id: speed_led_light
    restore_mode: ALWAYS_OFF
    internal: true
    effects: #Setup strobe blinking for speed led to indicate current speed
      - strobe:
          name: Speed Low
          colors:
            - state: true
              brightness: 100%
              duration: 150ms
            - state: false
              duration: 1000ms
      - strobe:
          name: Speed Medium
          colors:
            - state: true
              brightness: 100%
              duration: 150ms
            - state: false
              duration: 150ms
            - state: true
              brightness: 100%
              duration: 150ms
            - state: false
              duration: 150ms
      - strobe:
          name: Speed High
          colors:
            - state: true
              brightness: 100%
              duration: 150ms
            - state: false
              duration: 150ms
            - state: true
              brightness: 100%
              duration: 150ms
            - state: false
              duration: 150ms
            - state: true
              brightness: 100%
              duration: 150ms
            - state: false
              duration: 1000ms

    
  - platform: binary
    name: "Fan Left LED - Temp & Hum Timer"
    output: temp_hum_led
    id: temp_hum_led_light
    internal: true
    restore_mode: ALWAYS_OFF

output:
  - id: bluetooth_led
    platform: gpio
    pin: GPIO32
    inverted: true
    
  - id: speed_led
    platform: gpio
    pin: GPIO33
    inverted: true
    
  - id: temp_hum_led
    platform: gpio
    pin: GPIO25
    inverted: true
    
  - platform: template
    id: fan_output_control
    type: float
    write_action:
      - lambda: |-
          auto call = id(speed_select).make_call();
          std::string option = "";
          
          if (state > 0.67) {
            if (id(speed_select).state != "High")
              option = "High";
          } else if (id(three_speed_fan) && state > 0.34) {
            if (id(speed_select).state != "Medium")
              option = "Medium";
          } else if (state > 0.1 && id(speed_select).state != "Low") {
            option = "Low";
          }        
          
          if (option != "") {
            call.set_option(option);
            call.perform();
          }
          
fan:
  - platform: speed
    output: fan_output_control
    name: "Fan"
    id: attic_fan
    speed_count: 2 # Change to 3 on a 3 speed fan
    restore_mode: ALWAYS_OFF
    on_turn_on:
      - switch.turn_on: on_off_switch
    on_turn_off:
      - switch.turn_off: on_off_switch

switch:
  - platform: gpio
    pin: 5
    name: "Fan Speed Low"
    id: Speed_Low
    restore_mode: ALWAYS_OFF
    interlock: &interlock_group [Speed_Low, Speed_Medium, Speed_High] #Interlock prevents multiple on relays 
    interlock_wait_time: 2s #2 seconds of all relays off before enabling a new speed
    internal: true
    on_turn_on:
      - light.turn_on: 
          id: speed_led_light
          effect: "Speed Low"
    on_turn_off:
      - light.turn_off: 
          id: speed_led_light

  # This switch does not function on 2 speed fans
  - platform: gpio
    pin: 22
    name: "Fan Speed Medium"
    id: Speed_Medium
    restore_mode: ALWAYS_OFF
    interlock: *interlock_group
    interlock_wait_time: 2s
    internal: true
    on_turn_on:
      - light.turn_on: 
          id: speed_led_light
          effect: "Speed Medium"
    on_turn_off:
      - light.turn_off: 
          id: speed_led_light

  - platform: gpio
    pin: 23
    name: "Fan Speed High"
    id: Speed_High
    restore_mode: ALWAYS_OFF
    interlock: *interlock_group
    interlock_wait_time: 2s
    internal: true
    on_turn_on:
      - light.turn_on: 
          id: speed_led_light
          effect: "Speed High"
    on_turn_off:
      - light.turn_off: 
          id: speed_led_light
   
  - platform: template
    name: "Fan On/Off"
    id: on_off_switch
    turn_on_action: 
      lambda: |-
        if (id(speed_mode) == 1) {
          id(Speed_Low).turn_on();      
        } else if (id(three_speed_fan) && id(speed_mode) == 2) {
          id(Speed_Medium).turn_on();
        } else {
          id(Speed_High).turn_on();
        }
        if (id(timer_en)){ //start the timer if its set
          id(timer_stop) = id(homeassistant_time).now().timestamp + id(timer_offset);
          id(time_left).publish_state((double)id(timer_offset) / 60.0);
        }
        
        //turn on fan component if it's not already on and set it to the current speed mode
        if (!id(attic_fan).state && !id(select_lock)) {
          id(select_lock) = true;
          auto call = id(attic_fan).turn_on();
          call.set_speed(id(speed_mode));
          call.perform();        
        }
        
    turn_off_action:
      lambda: |-
        id(Speed_Low).turn_off();
        id(Speed_Medium).turn_off();
        id(Speed_High).turn_off();
        id(timer_stop) = 0;
        id(time_left).publish_state(0);
        
        //turn off fan component if it's not already off
        if(id(attic_fan).state) {
          id(timer).publish_state("off");
          auto call = id(attic_fan).turn_off();
          call.perform();
        }
    lambda: |-
      return (id(Speed_Low).state | id(Speed_Medium).state | id(Speed_High).state);
  - platform: template
    name: "Fan Auto Temp Control On/Off"
    id: auto_temp_control
    restore_mode: RESTORE_DEFAULT_OFF
    optimistic: True
    turn_on_action: 
      then:
        - if:
            condition:
              lambda: "return id(attic_temperature).state >= id(high_speed_above_temp).state && id(attic_temperature).state < id(failsafe_temp);"
            then:
              - fan.turn_on:
                  id: attic_fan                  
                  speed: !lambda "return id(three_speed_fan) ? 3 : 2;"
        - if:
            condition:
              lambda: "return id(three_speed_fan) && id(attic_temperature).state >= id(medium_speed_above_temp).state && id(attic_temperature).state < id(high_speed_above_temp).state && id(attic_temperature).state < id(failsafe_temp);"
            then:
              - fan.turn_on:
                  id: attic_fan
                  speed: 2
        - if:
            condition:
              lambda: "return id(attic_temperature).state >= id(low_speed_above_temp).state && id(attic_temperature).state < (id(three_speed_fan) ? id(medium_speed_above_temp).state : id(high_speed_above_temp).state) && id(attic_temperature).state < id(failsafe_temp);"
            then:
              - fan.turn_on:
                  id: attic_fan
                  speed: 1
        - if:
            condition:
              lambda: "return id(attic_temperature).state < id(low_speed_above_temp).state;"
            then:
              - fan.turn_off: attic_fan

i2c: #setup i2c for temp/humidity sensor
  sda: 18
  scl: 19
  scan: true
  id: bus_a

sensor:
  - platform: wifi_signal
    name: "Fan WiFi Strength"
    update_interval: 60s
  - platform: uptime
    name: "Fan Uptime"
  - platform: template
    name: "Fan Time Left"
    id: time_left
    update_interval: 20s
    unit_of_measurement: "minutes"
    lambda: |-
      if (id(timer_en)) {
        auto time_remaining = id(timer_stop) - id(homeassistant_time).now().timestamp;
        
        return (time_remaining >=0) ? ((float)time_remaining / 60.0):0;
      } else if (id(time_left).state != 0) {
        return 0;
      }
      return {};
      
  - platform: sht3xd
    temperature:
      id: attic_temperature
      device_class: temperature
      state_class: measurement
      name: "Temperature"
      filters:
        - lambda: return x * (9.0/5.0) + 32.0; #Change to Freedom Units
      unit_of_measurement: "°F"
      
      
      on_value_range:
        # Fire Protection Automation
        - above: !lambda return id(failsafe_temp);
          then:
            - fan.turn_off: attic_fan        
        # Automated High Speed Trigger
        - above: !lambda return id(high_speed_above_temp).state;
          below: !lambda return id(failsafe_temp);        
          then:
            - if:
                condition:
                  switch.is_on: auto_temp_control
                then:
                  - fan.turn_on:
                      id: attic_fan
                      speed: !lambda "return id(three_speed_fan) ? 3 : 2;"
        # Automated Medium/Low Speed Trigger
        - above: !lambda "return id(three_speed_fan) ? id(medium_speed_above_temp).state : id(low_speed_above_temp).state;"
          below: !lambda return id(high_speed_above_temp).state;
          then:
            - if:
                condition:
                  switch.is_on: auto_temp_control
                then:
                  - fan.turn_on:
                      id: attic_fan
                      speed: !lambda "return id(three_speed_fan) ? 2 : 1;"
        # Automated Low Speed Trigger (only used for 3 speed fans)
        - above: !lambda return id(low_speed_above_temp).state;
          below: !lambda "return id(three_speed_fan) ? id(medium_speed_above_temp).state : id(low_speed_above_temp).state;"
          then:
            - if:
                condition:
                  lambda: "return id(three_speed_fan) && id(auto_temp_control).state;"
                then:
                  - fan.turn_on:
                      id: attic_fan
                      speed: 1
        # Automated Off Trigger
        - below: !lambda return id(off_below_temp).state;
          then:
            - if:
                condition:
                  switch.is_on: auto_temp_control
                then:
                  - fan.turn_off: attic_fan
          
    humidity:
      name: "Humidity"
    address: 0x44
    update_interval: 60s

text_sensor:
  - platform: wifi_info
    ip_address:
      name: "Fan IP Address"