Community project

Temperature-Controlled Fan Hub

ESP32
Photo of Temperature-Controlled Fan Hub
Generated with AI

Paulo Leonardes

Published October 5, 2026

This temperature-controlled fan hub uses an ESP32-S3 to monitor thermal conditions and automatically adjust fan speeds based on real-time temperature readings. The system reads analog voltage from NTC thermistors, converts those readings to temperature values, and drives up to three 12 V PC fans via PWM-controlled transistor switches. A 5 V linear regulator powers the ESP32 from the same 12 V supply that feeds the fans, keeping the entire system on a single power input.

This guide provides a complete wiring diagram, full parts list with sourcing guidance, ESPHome firmware ready to customize, and step-by-step assembly instructions. Builders will learn how to condition analog sensor signals, implement thermal control logic, and safely switch higher-voltage loads from a microcontroller. The project is suitable for cooling enclosures, server racks, or any application requiring temperature-responsive fan management.

Wiring diagram

Wiring diagram for Temperature-Controlled Fan Hub

Gather all the parts

QtyComponent
1

10 kΩ B3590 NTC thermistor

10 kΩ B3590

A two-wire temperature-sensitive resistor used to measure the first temperature point.

1

10 kΩ B3590 NTC thermistor

10 kΩ B3590

A two-wire temperature-sensitive resistor used to measure the second temperature point.

1

10 kΩ B3590 NTC thermistor

10 kΩ B3590

A two-wire temperature-sensitive resistor used to measure the third temperature point.

1

10 kΩ B3590 NTC thermistor

10 kΩ B3590

A two-wire temperature-sensitive resistor used to measure the fourth temperature point.

1

10 kΩ B3590 NTC thermistor

10 kΩ B3590

A two-wire temperature-sensitive resistor used to measure the fifth temperature point.

1

Pc Fan

Standard 4-wire PC/case fan (Intel 4-wire PWM spec): always-on +12V power, ground, a ~25 kHz PWM control input (TTL, pulled to 5V or 3.3V), and an open-collector tachometer output (2 pulses/rev, needs an external pull-up to read RPM). Unlike a brushed DC motor it is brushless with built-in commutation, so it is driven directly from an MCU PWM pin rather than through an H-bridge; do not wire it like a bidirectional dc-motor. With no PWM signal it runs at full RPM. 5V 4-wire variants exist but 12V is standard.

1

Pc Fan

Standard 4-wire PC/case fan (Intel 4-wire PWM spec): always-on +12V power, ground, a ~25 kHz PWM control input (TTL, pulled to 5V or 3.3V), and an open-collector tachometer output (2 pulses/rev, needs an external pull-up to read RPM). Unlike a brushed DC motor it is brushless with built-in commutation, so it is driven directly from an MCU PWM pin rather than through an H-bridge; do not wire it like a bidirectional dc-motor. With no PWM signal it runs at full RPM. 5V 4-wire variants exist but 12V is standard.

1

Pc Fan

Standard 4-wire PC/case fan (Intel 4-wire PWM spec): always-on +12V power, ground, a ~25 kHz PWM control input (TTL, pulled to 5V or 3.3V), and an open-collector tachometer output (2 pulses/rev, needs an external pull-up to read RPM). Unlike a brushed DC motor it is brushless with built-in commutation, so it is driven directly from an MCU PWM pin rather than through an H-bridge; do not wire it like a bidirectional dc-motor. With no PWM signal it runs at full RPM. 5V 4-wire variants exist but 12V is standard.

1

12 V DC fan power supply

12 V DC; current rating at least the combined fan-start current

An isolated 12 V DC supply that powers the three PC fans.

1

2N3904 NPN transistor

An electronic switch that lets the 3.3 V ESP32 safely control the first fan's 5 V PWM input.

1

2N3904 NPN transistor

An electronic switch that lets the 3.3 V ESP32 safely control the second fan's 5 V PWM input.

1

2N3904 NPN transistor

An electronic switch that lets the 3.3 V ESP32 safely control the third fan's 5 V PWM input.

1

1 kΩ resistor

1 kΩ

Limits control current into the first PWM transistor.

1

1 kΩ resistor

1 kΩ

Limits control current into the second PWM transistor.

1

1 kΩ resistor

1 kΩ

Limits control current into the third PWM transistor.

1

10 kΩ 1% resistor

10 kΩ, 1%

The fixed resistor that lets the first NTC thermistor be measured as a voltage.

1

10 kΩ 1% resistor

10 kΩ, 1%

The fixed resistor that lets the second NTC thermistor be measured as a voltage.

1

10 kΩ 1% resistor

10 kΩ, 1%

The fixed resistor that lets the third NTC thermistor be measured as a voltage.

1

10 kΩ 1% resistor

10 kΩ, 1%

The fixed resistor that lets the fourth NTC thermistor be measured as a voltage.

1

10 kΩ 1% resistor

10 kΩ, 1%

The fixed resistor that lets the fifth NTC thermistor be measured as a voltage.

1

10 kΩ resistor

10 kΩ

A 3.3 V pull-up resistor that makes the first fan's speed pulses safe for the ESP32.

1

10 kΩ resistor

10 kΩ

A 3.3 V pull-up resistor that makes the second fan's speed pulses safe for the ESP32.

1

10 kΩ resistor

10 kΩ

A 3.3 V pull-up resistor that makes the third fan's speed pulses safe for the ESP32.

1

LM7805 / 7805 positive 5 V linear regulator

TO-220 package

A three-pin regulator that turns the existing 12 V supply into 5 V for the ESP32.

1

10 µF 25 V electrolytic capacitor

10 µF, ≥25 V

An input smoothing capacitor fitted across the 7805 regulator input and ground.

1

100 nF ceramic capacitor

100 nF, ≥10 V

A small output bypass capacitor fitted across the 7805 regulator output and ground.

1

TO-220 bolt-on heatsink

TO-220 heatsink, rated for at least 3 W

A metal heatsink that carries away heat from the 7805 while it changes 12 V into 5 V.

Assemble it in 7 steps

1. Place the 5 V regulator parts

With the 12 V supply unplugged, place the 7805 with its labelled face toward you. For the usual TO-220 7805 package, the left pin is IN, middle pin is GND, and right pin is OUT; check the printing or datasheet for your exact part before connecting it. Bolt regulator_heatsink to regulator_7805’s metal tab so it can release heat.

  • Keep the regulator and its heatsink clear of wires and plastic.
  • The 7805 must be a positive 7805/LM7805, not the negative 7905 originally shown.
  • Do not power the circuit if the regulator pins are uncertain — swapping IN and OUT can damage the regulator and ESP32.

2. Add the regulator capacitors

Connect c1_input’s positive lead to the regulator IN pin and its negative striped lead to GND (input smoothing). Connect c2_output across the regulator output: one lead to OUT and the other to GND (output stability). Keep both capacitors physically close to the regulator pins.

  • The 10 µF capacitor has polarity: its stripe marks the negative lead.
  • The 100 nF ceramic capacitor has no polarity.
  • Putting the 10 µF capacitor in backwards can make it overheat or burst.

3. Connect the shared 12 V supply

Connect the 12 V supply positive output to regulator_7805 IN and to the 12V wire on fan_1, fan_2, and fan_3 (power). Connect the supply negative output to the shared GND line (ground). This one supply feeds the fans directly and feeds the regulator separately.

  • Use a 12 V supply rated for at least the three fans’ combined start-up current plus 0.5 A for the ESP32 regulator.
  • Use thicker wire for the fan 12 V and GND connections than for the signal wires.
  • Never connect the 12 V positive wire to an ESP32 pin, including 5V, 3V3, or any GPIO — this can permanently damage the board.

4. Power the ESP32 from the regulator

Connect regulator_7805 OUT to the ESP32-S3 Zero 5V/VIN pin (power). Connect regulator_7805 GND to an ESP32 GND pin (ground). The fan-supply negative, all fan grounds, transistor emitters, and ESP32 GND must be one shared ground connection.

  • Measure between OUT and GND with a multimeter before attaching the ESP32; it should read close to +5 V.
  • The 7805 heatsink may become warm in normal use.
  • Do not connect the regulator OUT wire to the ESP32 3V3 pin — 5 V on that pin can permanently damage the board.

5. Build the five temperature probe junctions

For each probe, connect one NTC lead to ESP32 3V3 (power). Connect its other lead to its matching 10 kΩ resistor and the assigned GPIO (signal); connect the resistor’s other lead to GND (ground). Use GPIO1 for ntc_1, GPIO2 for ntc_2, GPIO3 for ntc_3, GPIO4 for ntc_4, and GPIO5 for ntc_5.

  • The NTC probe leads have no polarity.
  • Keep each probe’s signal junction away from the 12 V wiring.
  • Do not connect an NTC signal junction to 12 V — ESP32 inputs accept only low-voltage signals.

6. Add the three fan control connections

For each fan, connect GPIO6 through base_r1 to pwm_q1 base, GPIO7 through base_r2 to pwm_q2 base, and GPIO8 through base_r3 to pwm_q3 base (control signals). Connect every transistor emitter to GND and each collector to its fan’s PWM wire. Connect fan_1 TACH to GPIO14 and tach_pullup_r1 to 3V3; fan_2 TACH to GPIO15 and tach_pullup_r2 to 3V3; and fan_3 TACH to GPIO17 and tach_pullup_r3 to 3V3 (speed signals).

  • PWM is commonly the blue fan wire and TACH commonly green, but confirm with the fan’s label or datasheet.
  • The onboard RGB status LED remains on GPIO16; do not connect a fan wire to GPIO16.
  • Do not connect a fan PWM or TACH wire straight to the ESP32; the transistor and 3.3 V pull-up parts protect the ESP32 from higher fan signal voltages.

7. Inspect and power safely

Before switching on, verify that only fan wires and regulator IN receive +12 V, while regulator OUT alone goes to the ESP32 5V/VIN pin. Confirm every ground wire is joined. Turn on the 12 V supply, check the regulator output remains near 5 V, then power the firmware from the same board connection only after this check.

  • If the 7805 becomes too hot to touch briefly, switch off immediately and use a larger heatsink or a 5 V buck converter.
  • Use Schematik’s Deploy button to flash the firmware after the wiring is complete.
  • Disconnect power immediately if a wire heats up, you smell hot plastic, or the regulator output is not close to 5 V.

Review all connections

1. Connections between "ntc_1" and "ESP32"

Functionntc_1ESP32
powerN13V3
analogN2GPIO 1

2. Connections between "ntc_ref_r1" and "ESP32"

Functionntc_ref_r1ESP32
groundBGND
analogA → 10 kΩ B3590 NTC thermistor N2EXT

3. Connections between "ntc_2" and "ESP32"

Functionntc_2ESP32
powerN13V3
analogN2GPIO 2

4. Connections between "ntc_ref_r2" and "ESP32"

Functionntc_ref_r2ESP32
groundBGND
analogA → 10 kΩ B3590 NTC thermistor N2EXT

5. Connections between "ntc_3" and "ESP32"

Functionntc_3ESP32
powerN13V3
analogN2GPIO 3

6. Connections between "ntc_ref_r3" and "ESP32"

Functionntc_ref_r3ESP32
groundBGND
analogA → 10 kΩ B3590 NTC thermistor N2EXT

7. Connections between "ntc_4" and "ESP32"

Functionntc_4ESP32
powerN13V3
analogN2GPIO 4

8. Connections between "ntc_ref_r4" and "ESP32"

Functionntc_ref_r4ESP32
groundBGND
analogA → 10 kΩ B3590 NTC thermistor N2EXT

9. Connections between "ntc_5" and "ESP32"

Functionntc_5ESP32
powerN13V3
analogN2GPIO 5

10. Connections between "ntc_ref_r5" and "ESP32"

Functionntc_ref_r5ESP32
groundBGND
analogA → 10 kΩ B3590 NTC thermistor N2EXT

11. Connections between "base_r1" and "ESP32"

Functionbase_r1ESP32
digitalAGPIO 6
digitalB → 2N3904 NPN transistor BEXT

12. Connections between "pwm_q1" and "ESP32"

Functionpwm_q1ESP32
digitalC → Pc Fan PWMEXT
groundEGND

13. Connections between "base_r2" and "ESP32"

Functionbase_r2ESP32
digitalAGPIO 7
digitalB → 2N3904 NPN transistor BEXT

14. Connections between "pwm_q2" and "ESP32"

Functionpwm_q2ESP32
digitalC → Pc Fan PWMEXT
groundEGND

15. Connections between "base_r3" and "ESP32"

Functionbase_r3ESP32
digitalAGPIO 8
digitalB → 2N3904 NPN transistor BEXT

16. Connections between "pwm_q3" and "ESP32"

Functionpwm_q3ESP32
digitalC → Pc Fan PWMEXT
groundEGND

17. Connections between "fan_supply" and "ESP32"

Functionfan_supplyESP32
groundGND → Pc Fan GNDEXT
groundGND → Pc Fan GNDEXT
groundGND → Pc Fan GNDEXT
groundGNDGND
power+12V → LM7805 / 7805 positive 5 V linear regulator INEXT

18. Connections between "fan_1" and "ESP32"

Functionfan_1ESP32
dataTACHGPIO 14
power12V → LM7805 / 7805 positive 5 V linear regulator INEXT

19. Connections between "tach_pullup_r1" and "ESP32"

Functiontach_pullup_r1ESP32
powerB3V3
digitalA → Pc Fan TACHEXT

20. Connections between "fan_2" and "ESP32"

Functionfan_2ESP32
dataTACHGPIO 15
power12V → LM7805 / 7805 positive 5 V linear regulator INEXT

21. Connections between "tach_pullup_r2" and "ESP32"

Functiontach_pullup_r2ESP32
powerB3V3
digitalA → Pc Fan TACHEXT

22. Connections between "tach_pullup_r3" and "ESP32"

Functiontach_pullup_r3ESP32
powerB3V3
digitalA → Pc Fan TACHEXT

23. Connections between "fan_3" and "ESP32"

Functionfan_3ESP32
dataTACHGPIO 17
power12V → LM7805 / 7805 positive 5 V linear regulator INEXT

24. Connections between "regulator_7805" and "ESP32"

Functionregulator_7805ESP32
groundGNDGND
powerOUTVIN

25. Connections between "c1_input" and "ESP32"

Functionc1_inputESP32
power+ → LM7805 / 7805 positive 5 V linear regulator INEXT
ground-GND

26. Connections between "c2_output" and "ESP32"

Functionc2_outputESP32
power1 → LM7805 / 7805 positive 5 V linear regulator OUTEXT
ground2GND

Deploy the firmware

# GPIO16 drives the Waveshare ESP32-S3-Zero onboard WS2812 status LED.
esphome:
  name: s3-zero-fan-controller
  friendly_name: S3 Zero Fan Controller
  on_boot:
    priority: -100
    then:
      - light.turn_on:
          id: status_led
          red: 0%
          green: 0%
          blue: 100%
          brightness: 25%

esp32:
  variant: esp32s3
  framework:
    type: arduino

logger:

api:

wifi:
  ap:
    ssid: "S3 Fan Controller Setup"
    password: "fancontrol"

captive_portal:

light:
  - platform: esp32_rmt_led_strip
    id: status_led
    name: "Controller Status LED"
    pin: GPIO16
    num_leds: 1
    chipset: WS2812
    rgb_order: GRB
    restore_mode: ALWAYS_OFF

binary_sensor:
  - platform: status
    name: "Controller Connection"
    id: controller_connection
    on_press:
      then:
        - light.turn_on:
            id: status_led
            red: 0%
            green: 100%
            blue: 0%
            brightness: 20%
    on_release:
      then:
        - light.turn_on:
            id: status_led
            red: 100%
            green: 0%
            blue: 0%
            brightness: 20%

output:
  - platform: ledc
    id: fan1_pwm
    pin: GPIO6
    frequency: 25000 Hz
  - platform: ledc
    id: fan2_pwm
    pin: GPIO7
    frequency: 25000 Hz
  - platform: ledc
    id: fan3_pwm
    pin: GPIO8
    frequency: 25000 Hz

sensor:
  - platform: adc
    id: ntc1_voltage
    pin: GPIO1
    attenuation: 12db
    update_interval: 5s
  - platform: resistance
    id: ntc1_resistance
    sensor: ntc1_voltage
    configuration: DOWNSTREAM
    resistor: 10kOhm
  - platform: ntc
    name: "Temperature 1"
    id: temperature_1
    sensor: ntc1_resistance
    calibration:
      b_constant: 3590
      reference_temperature: 25°C
      reference_resistance: 10kOhm

  - platform: adc
    id: ntc2_voltage
    pin: GPIO2
    attenuation: 12db
    update_interval: 5s
  - platform: resistance
    id: ntc2_resistance
    sensor: ntc2_voltage
    configuration: DOWNSTREAM
    resistor: 10kOhm
  - platform: ntc
    name: "Temperature 2"
    id: temperature_2
    sensor: ntc2_resistance
    calibration:
      b_constant: 3590
      reference_temperature: 25°C
      reference_resistance: 10kOhm

  - platform: adc
    id: ntc3_voltage
    pin: GPIO3
    attenuation: 12db
    update_interval: 5s
  - platform: resistance
    id: ntc3_resistance
    sensor: ntc3_voltage
    configuration: DOWNSTREAM
    resistor: 10kOhm
  - platform: ntc
    name: "Temperature 3"
    id: temperature_3
    sensor: ntc3_resistance
    calibration:
      b_constant: 3590
      reference_temperature: 25°C
      reference_resistance: 10kOhm

  - platform: adc
    id: ntc4_voltage
    pin: GPIO4
    attenuation: 12db
    update_interval: 5s
  - platform: resistance
    id: ntc4_resistance
    sensor: ntc4_voltage
    configuration: DOWNSTREAM
    resistor: 10kOhm
  - platform: ntc
    name: "Temperature 4"
    id: temperature_4
    sensor: ntc4_resistance
    calibration:
      b_constant: 3590
      reference_temperature: 25°C
      reference_resistance: 10kOhm

  - platform: adc
    id: ntc5_voltage
    pin: GPIO5
    attenuation: 12db
    update_interval: 5s
  - platform: resistance
    id: ntc5_resistance
    sensor: ntc5_voltage
    configuration: DOWNSTREAM
    resistor: 10kOhm
  - platform: ntc
    name: "Temperature 5"
    id: temperature_5
    sensor: ntc5_resistance
    calibration:
      b_constant: 3590
      reference_temperature: 25°C
      reference_resistance: 10kOhm

  - platform: pulse_counter
    name: "Fan 1 Speed"
    pin:
      number: GPIO14
      mode:
        input: true
        pullup: false
    unit_of_measurement: "RPM"
    accuracy_decimals: 0
    count_mode:
      rising_edge: INCREMENT
      falling_edge: DISABLE
    filters:
      - multiply: 0.5

  - platform: pulse_counter
    name: "Fan 2 Speed"
    pin:
      number: GPIO15
      mode:
        input: true
        pullup: false
    unit_of_measurement: "RPM"
    accuracy_decimals: 0
    count_mode:
      rising_edge: INCREMENT
      falling_edge: DISABLE
    filters:
      - multiply: 0.5

  - platform: pulse_counter
    name: "Fan 3 Speed"
    pin:
      number: GPIO17
      mode:
        input: true
        pullup: false
    unit_of_measurement: "RPM"
    accuracy_decimals: 0
    count_mode:
      rising_edge: INCREMENT
      falling_edge: DISABLE
    filters:
      - multiply: 0.5

  - platform: template
    name: "Hottest Temperature"
    id: hottest_temperature
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 1
    update_interval: 5s
    lambda: |-
      float hottest = -100.0f;
      if (!isnan(id(temperature_1).state)) hottest = max(hottest, id(temperature_1).state);
      if (!isnan(id(temperature_2).state)) hottest = max(hottest, id(temperature_2).state);
      if (!isnan(id(temperature_3).state)) hottest = max(hottest, id(temperature_3).state);
      if (!isnan(id(temperature_4).state)) hottest = max(hottest, id(temperature_4).state);
      if (!isnan(id(temperature_5).state)) hottest = max(hottest, id(temperature_5).state);
      return hottest > -100.0f ? hottest : NAN;

number:
  - platform: template
    name: "Manual Fan Speed"
    id: manual_fan_speed
    unit_of_measurement: "%"
    icon: "mdi:fan"
    min_value: 30
    max_value: 100
    step: 1
    optimistic: true
    restore_value: true
    initial_value: 50
    set_action:
      - switch.turn_off: automatic_fan_control
      - lambda: |-
          float fan_duty = x / 100.0f;
          // The transistor inverts the 4-wire fan PWM signal.
          float transistor_duty = 1.0f - fan_duty;
          id(fan1_pwm).set_level(transistor_duty);
          id(fan2_pwm).set_level(transistor_duty);
          id(fan3_pwm).set_level(transistor_duty);

switch:
  - platform: template
    name: "Automatic Fan Control"
    id: automatic_fan_control
    optimistic: true
    restore_mode: RESTORE_DEFAULT_ON

interval:
  - interval: 5s
    then:
      - lambda: |-
          if (!id(automatic_fan_control).state) return;
          float hottest = id(hottest_temperature).state;
          float fan_duty = 1.0f;
          if (!isnan(hottest)) {
            if (hottest <= 30.0f) fan_duty = 0.30f;
            else if (hottest >= 60.0f) fan_duty = 1.0f;
            else fan_duty = 0.30f + (hottest - 30.0f) * 0.70f / 30.0f;
          }
          float transistor_duty = 1.0f - fan_duty;
          id(fan1_pwm).set_level(transistor_duty);
          id(fan2_pwm).set_level(transistor_duty);
          id(fan3_pwm).set_level(transistor_duty);

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik