PWM Fan Controller

For my first esphome project I’m creating a fan controller for my rack with an D1 Mini (8266). So far everything seems to work. Any tips or suggestions are welcome.

  friendly_name: Rack Fan

  name: rack-fan

  - id: dhttemp
    type: float
    restore_value: yes
    initial_value: '0'

# ESP32 AND NETWORK SETUP                                                                                                                                                                                                                

  board: d1_mini

# pid climate log update is noisy, dial it back to warn                                                                                                                                                                                  
  level: DEBUG
    dht: DEBUG

# default HA integration, OTA updater and backup http web portal                                                                                                                                                                         
  password: !secret ota_password
  password: !secret ota_password
      - logger.log: "OTA start"
      - logger.log:
          format: "OTA progress %0.1f%%"
          args: ["x"]
      - logger.log: "OTA end"


  # Read the wifi/pass from secrets.yaml:                                                                                                                                                                                                
  # wifi_ssid: "My Wifi XX"                                                                                                                                                                                                              
  # wifi_password: "XXXXXXX"                                                                                                                                                                                                             
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: true

  # Enable fallback hotspot (captive portal) in case wifi connection fails                                                                                                                                                               
    ssid: !secret wifi_ssid_backup
    password: !secret wifi_password


  # Send IP Address                                                                                                                                                                                                                      
  - platform: wifi_info
      name: $friendly_name IP Address

  # Send Uptime in raw seconds                                                                                                                                                                                                           
  - platform: template
    name: $friendly_name Uptime
    id: uptime_human
    icon: mdi:clock-start


  # Send WiFi signal strength & uptime to HA                                                                                                                                                                                             
  - platform: wifi_signal
    name: $friendly_name WiFi Strength
    update_interval: 60s

  # RPM Signal from Fan                                                                                                                                                                                                                  
  - platform: pulse_counter
      number: D2
    name: ${friendly_name} Fan Speed
    id: fan_pulse
    unit_of_measurement: 'RPM'
      - multiply: 0.5
      rising_edge: INCREMENT
      falling_edge: DISABLE
    update_interval: 30s

  # This is a bit of overkill. It sends a human readable                                                                                                                                                                                 
  # uptime string                                                                                                                                                                                                                        
  # 1h 41m 32s instead of 6092 seconds                                                                                                                                                                                                   
  - platform: uptime
    name: $friendly_name Uptime
    id: uptime_sensor
    update_interval: 60s
        - text_sensor.template.publish:
            id: uptime_human
            # Custom C++ code to generate the result                                                                                                                                                                                     
            state: !lambda |-
              int seconds = round(id(uptime_sensor).raw_state);
              int days = seconds / (24 * 3600);                                                                                                                                                                                          
              seconds = seconds % (24 * 3600);                                                                                                                                                                                           
              int hours = seconds / 3600;                                                                                                                                                                                                
              seconds = seconds % 3600;                                                                                                                                                                                                  
              int minutes = seconds /  60;                                                                                                                                                                                               
              seconds = seconds % 60;                                                                                                                                                                                                    
              return (                                                                                                                                                                                                                   
                (days ? to_string(days) + "d " : "") +
                (hours ? to_string(hours) + "h " : "") +
                (minutes ? to_string(minutes) + "m " : "") +
                (to_string(seconds) + "s")


  # GET TEMP/HUMIDITY FROM DHT11                                                                                                                                                                                                         
  - platform: dht
    pin: D0
    model: DHT11
      name: "Temperature"
      id: rack_fan_temperature
      accuracy_decimals: 3
        - above: 50.0
                fan.is_off: rack_fan_toggle
              - logger.log: "Set fan level 100 over 50C"
              - output.set_level:
                  id: rack_fan_speed
                  level: 100%
        - above: 40.0
                fan.is_off: rack_fan_toggle
              - logger.log: "Set fan level 66 over 40C"
              - output.set_level:
                  id: rack_fan_speed
                  level: 66%
        - above: 30.0
                fan.is_off: rack_fan_toggle
              - logger.log: "Set fan level 33 over 30C"
              - output.set_level:
                  id: rack_fan_speed
                  level: 33%
        - below: 30.0
                fan.is_off: rack_fan_toggle
              - logger.log: "Set fan level 0 under 30C"
              - output.set_level:
                  id: rack_fan_speed
                  level: 0%

      name: "Humidity"
      id: rack_fan_humidity
    update_interval: 2s

  # Wire this pin (15) into the PWM pin of your 12v fan                                                                                                                                                                                  
  # ledc is the name of the pwm output system on an esp32                                                                                                                                                                                
  - platform: esp8266_pwm
    id: rack_fan_speed
    pin: D1

    # 25KHz is standard PC fan frequency, minimises buzzing                                                                                                                                                                              
    frequency: "25000 Hz"

    # my fans stop working below 13% powerful.                                                                                                                                                                                           
    # also they're  powerful and loud, cap their max speed to 80%                                                                                                                                                                        
    min_power: 1%
    max_power: 100%

  - platform: speed
    output: rack_fan_speed
    name: ${friendly_name} Override Enable
    id: rack_fan_toggle
     - output.set_level:
          id: rack_fan_speed
          level: !lambda |-
             return id(fan_speed_override).state/100.0;
      - logger.log: "Power of Fan turned OFF"
      - delay: 1s
      - output.set_level:
          id: rack_fan_speed
          level: !lambda |-
            if (id(rack_fan_temperature).raw_state > 50){
              return 1;}
            else if (id(rack_fan_temperature).raw_state > 40){
              return 0.66;}
            else if (id(rack_fan_temperature).raw_state > 30){
              return 0.33;}
              return 0;
  - platform: template
    name: "Fan Speed Override"
    id: fan_speed_override
    internal: false
    max_value: 100.0
    min_value: 0.0
    step: 33.3
    optimistic: true
    mode: slider
            fan.is_on: rack_fan_toggle
          - output.set_level:
              id: rack_fan_speed
              level: !lambda "return x/100;"

  # Expose an ESP32 restart button to HA                                                                                                                                                                                                 
  - platform: restart
    name: "Rack Fan D1 Pro Mini Restart"

Screenshots from HA reading temp:

Override Enabled:

Circuit diagram:



I am trying something similar based on

That is using esp32 while like you I am using an esp8266 d1 mini

Does your max power work? I am trying to set it to a max of 80% but that seems to be ignored. I can’t see that the software esp8266 pwm supports max power. Maybe we need a lambda for when it’s set above 80 it’s put back down to 80?

1 Like

I haven’t tried setting max power to another other than 100%. I’ll play with it this weekend and get back to you.

I have been playing around with the settings, and it seems, the setting is used but isn’t displayed how I expected.

The pwm speed will still ramp up to 100%, but that 100% is less with the setting on. I managed to get the rpm speed recorded and see that with the max set to 70%, it maxes out at 980rpm, but without the setting it was the full 1200.

Edit: and this mistake is my first clue. 1200rpm isn’t the max, 1500rpm is. 80% of 1500 is 1200 :grin:

1 Like

It looks like the reported RPM isn’t correct. Here are my results based on testing. I need to look further into the issue.

Calc RPM=1700 * (MAX/100)*(OVRD_PWR/100)

Min Max Override Power Reported RPM Calc RPM
1 100 100 1700 1700
1 100 66 1784 1122
1 100 33 773 561
1 75 100 2053 1275
1 75 66 1204 842
1 75 23 496 293
1 50 100 1200 850
1 50 66 692 561
1 50 33 330 281
1 25 100 494 425
1 25 66 330 281
1 25 33 203 140

@wardy277 I finally got it figured out. I looked at the docs for the fan again here. It mentions a pull up resistor and it hit me that I needed to use a 12V pull and not the 3.3V going to the D1. Previously I was using the internal pull up resistor in code. I used an oscope to verify the frequency of the wave on D2 and plugged it into the formula from the Noctua docs. fan speed [rpm] = frequency [Hz] × 60 ÷ 2. Now that I added a 10K pull up resistor to 12V my measured values are close enough to the calculate values. :partying_face:
I’ll update the OP with the latest code and wiring diagram. Notice the new 10k resistor between 12V and the yellow RPM signal from the fan.

With 10K Pull Up to 12V
Input Speed Freq at o-scope (Hz) Reported Speed (RPM) Counter from ESP LOGS Pulse/min Count/2 Calculated Speed (1700*input speed)
33.00% 23 694 1388 694 690
66.00% 41 1245 2490 1245 1,230
100.00% 56 1700 3400 1700 1,680

Thanks for your work looking into this. I don’t have enough knowledge to have worked that out

Great job

1 Like

@Stu1811 I do not think the GPIO pin will survive 12v for very long. The tachometer is an open collector and is pulled down to the ground on pulse and floating otherwise, hence the required pullup at GPIO side.
With 3.3v, the internal pullup is a lot safer.

I’m building this myself and curious to understand your comment about the 12v a bit more. Are you referring to the voltage the Tach is sending in pulses?

I just tried to test this and never saw anything over a couple of hundred millivolts, although I’m not 100% sure my testing was correct.

I cant quite get the RPM sensor working as well which is confusing me.

My recommendation about the project though would be to strip out any automation from the controller. I move all my automation into NodeRed via HA. I have complete control that way and the programs need little tweaking if ever.

Tried the multi sensor doing lots of things and found it unreliable and full of bugs. Simple is best!

I’m about to build this and was a bit baffled by your comment. Could you explain it further for those of us on the learning curve.

I would be especialy interested in a proposed solution if there is a problem,.

Thanks for any help offered

1 Like

Why is there a 10k resistor between the tacho and 12V?

Uummm…I just can’t get over this. someone made this and it has the documentation, configuration , instructions and whatever else but you can’t get it working. You then make a public service announcement that you aren’t sure why this sensor wasn’t working , but I Checked whatever this here PWM pin thing is and my meter… ya, it ain’t working right now, it’s showing a couple milivolts but, I’m not sure what I’m measuring anyway. So clearly this is because of esphome, I’m gonna quit so should everyone else and let’s go to Node Red and rebuild it from scratch!
I’m sorry man it just cracked me up. I can’t stress this enough! You need to look at data sheets (aka the instruction manual). You would have know that sensor wasn’t going to work untill you added the reccomend pullup resistor ( pullup/down resistors they don’t have anything to do with voltage or reducing voltage. They limit how much current (a) can pass through it. All of this was in that data sheet, it even tells you how to calibrate the sensors and speeds, it tells you the fan logic is inverted, so if you wanted fan speed at HIGH(100%) the gpio (inverted)needs to be OFF(0.0v) so it explains what PWM is and how it changes the speed all that fun stuff to. Read those data sheets, .man. When you understand how something works or if there’s specific details you need to be aware of , I promise it will change your whole game and cut your troubleshooting times and irritation factor way down.

I think my comment maybe slightly unclear/incomplete so I will try to do better.

ESPHome is working great for me. In this instance I used what @Stu1811 has created as a good base (Thanks again for that!) and modified it to fit my needs/wants. I had a few questions going into it though and I was looking for some help to understand some weirdness I was seeing. My testing wasn’t that in-depth and felt understanding the comments about the voltages, pull up/down resistors etc may help me to see what and how I should testing.

Now I still get unusual readings for RPM with only two exceptions. 100% and 0%. They read correctly as per the fans documented speeds but anywhere in between I get readings higher than 100% or that don’t register at all when they should be. For what its worth its a small problem and I rarely run the fan at anything less than 100% anyway.

My suggestion with NodeRed is to remove only the automation from the device. Let ESPHome read sensors and control things when told to and allow NodeRed/HA to control when to do things based on those sensors. Minimizes code and means you can edit parameters without re-programming it each time you want to change it.

With regards to this project I refer to the section that says when temperature is above X turn fan on, and when below X turn fan off. Wouldn’t it be good to be able to edit those temperature limits from the same place you can monitor the fan from? Tis merely a suggestion. Each to their own. My preference is to let the software built for automation do the automation.

Thanks again for your comment and to @Stu1811 for putting in the work to share their hard work :slight_smile:

Sharing what your using, a link to the project, your fan config, how your getting RPM, is it a sensor or is somehow calculated from somewhere else? Your question was a little confusing but that’s not unusual. Posting actual data, links, config, etc that’s how confusion is minimized and someone can actually help. No details then people are just going to take guesses or ask you for the information that should have been attached to the initial post.

I’m not picking on you or anything, I’m just saying for future reference. Adding one or a couple of these things, pictures, a config, a link and then a to the point description of the problem would have been more helpful than a 3 or 4 paragraph thing. All that extra stuff over multiple paragraphs, it makes it more confusing a lot of the time. I really mean no offense but, you put some time and a few minutes into that post and it’s obvious you meant well and you tried but, it’s all mostly not important, not useful and now confusing.

Would you be talking about a server rack ran controller with rpm sensor?

Thanks for this code, it’s just what i need, but…i’d like it to work with a dallas sensor instead. I’ve tried but just cannot get it to work. Would anyone be so kind as to post up the code, but for a dallas sensor instead of a dht ?

Thank you in advance

Go look at the documentation , it explains how to do it. You gotta be a big boy and make an effort.

I already done it. I’m sorry but I forgot to post up to say so.
Thank you so much for your reply, it means the world to me

This is ace.
Am going to do this.
I have three NF-F12 Industrial 3000RPM fans.
If I were to extend this circuit for controlling the three fans individually, I assume:

  • Add PWM & RPM for Fan 2 to D3 and D4
  • Add PWM & RPM for Fan 3 to D5 and D6
  • Would need 2 extra 10k Resistors
  • would keep the single DHT11

Rather old thread but I still like to add, that based on what I read here and elsewhere I also did my fan controller to upgrade my deye inverter (remove the noise). You can find my code here: GitHub - sker65/esphome-fan-controller: Yet another esphome based fan controller. Can be controlled either manually or by temperature sensor.

From my point of view the code is cleaner (simple linear temperatur characteristics), simple switch between manual and temp controlled, optional MOSFET to turn the fans off. See some more comments on the githubwiring

1 Like