ESP32 Fan Control

Turns an ESP32-C3 into a web-based controller for up to eight 4-pin PWM fans. It hosts its own control panel — open the board's address on your network and set each fan's speed from a phone or laptop, with live RPM readback on up to four fans. No cloud service, no app to install, nothing to keep running on a computer.

Per-fan speed control

Each fan gets its own 0–100 % slider, driven at the 25 kHz the 4-pin fan spec calls for. Give them names that mean something — "Radiator", "Intake" — instead of numbers.

Live RPM per fan

Reads each fan's tachometer and pushes the actual measured speed to the browser once a second — immediate confirmation a fan is really spinning, and how fast. Available with up to four fans; see below.

Remembers its settings

Speeds, names, fan count and the board's own network name are stored on the board. After a power cut it comes back exactly as you left it, without waiting for anything to reconnect.

Wi-Fi set up in the browser

This page hands the board your network credentials over USB right after flashing. Nothing to recompile, and the same firmware works on every board you flash.

What you need

  • An ESP32-C3 board — not a classic ESP32; the two aren't interchangeable
  • One to eight 4-pin PWM fans (Noctua-style). Four or fewer also gives live RPM
  • A 5 V supply for the fans — they draw power from it directly, not through the ESP32
  • Wires from each fan's PWM and tach pins to the board, and a shared ground. No MOSFETs, level shifters or driver boards (wiring below)

Install

  1. Plug the ESP32 into this computer via USB.
  2. Click install, then pick the board's serial port in the browser prompt.
  3. When flashing finishes, the page will offer to set up Wi-Fi — enter your network name and password there.
  4. Open the address shown on screen — the board announces itself as fan-control-xxxxxx.local, which you can rename under Settings. If your network doesn't resolve .local names, use the IP address instead.
Your browser can't do this — use Chrome, Edge, or Opera on desktop. This page needs to be served over HTTPS (or from localhost) for the browser to allow serial access.

Wiring

Fan power does not go through the ESP32

Each fan's +5 V and GND go straight to your 5 V supply. The ESP32 only touches the PWM and tach pins — it carries none of the fans' current.

The ESP32's ground must still be tied to the supply's ground. Without that shared reference the PWM and tach signals have nothing to measure against and won't read correctly.

Signals

PWM is driven at 25 kHz per the 4-pin fan spec, as a normal push-pull output — no pull-up needed.

Tach is open-collector on the fan side, so it needs a pull-up; the firmware enables the ESP32's internal one, so no external resistor is required. Fans emit 2 pulses per revolution, which the RPM calculation already accounts for.

Pins

The C3 only has eight freely usable GPIOs — the rest are taken by flash, USB, UART and boot straps. That isn't enough to give eight fans both a PWM and a tach line, so the second group changes role with the fan count. Set it in the control panel; nothing needs reflashing.

Fan PWM Tach (1–4 fans) PWM (5–8 fans)
1GPIO 0GPIO 5GPIO 0
2GPIO 1GPIO 6GPIO 1
3GPIO 3GPIO 7GPIO 3
4GPIO 4GPIO 10GPIO 4
5——GPIO 5
6——GPIO 6
7——GPIO 7
8——GPIO 10

Above four fans the tach lines are dropped for every fan, not just the extra ones — those pins are carrying PWM instead. If live RPM matters more to you than fan count, stay at four or fewer. Wire only as many fans as you have; unused pins are never initialised.

Home Assistant

Untested — written from the firmware's API, not yet run against real hardware. Treat it as a starting point rather than a recipe.

The board speaks plain HTTP, so Home Assistant can drive it with its built-in RESTful Switch. No MQTT broker, no add-on, nothing to discover. Once the switch exists you can pass it on to HomeKit via the HomeKit Bridge integration.

1. Give the board a stable address

Name it under Settings in the control panel, then check Home Assistant can actually reach it — run this from the HA host:

curl http://fan-control-office.local/api/fans

If that returns JSON, use the hostname below. If it doesn't, don't fight it: .local resolution from inside HA's container is unreliable. Set a DHCP reservation on your router and use the IP address instead — it's arguably the more robust option anyway.

2. Add to configuration.yaml

switch:
  - platform: rest
    name: Office fans
    resource: http://fan-control-office.local/api/fan
    state_resource: http://fan-control-office.local/api/fans
    body_on: '{"id": 0, "speed": 70}'
    body_off: '{"id": 0, "speed": 0}'
    is_on_template: "{{ value_json[0].speed > 0 }}"
    headers:
      Content-Type: application/json

One block per fan, changing both the id in the bodies and the value_json[N] index to match — they have to agree, or the switch reports a different fan's state.

speed: 70 is what "on" means; pick what suits. Avoid going much below 20 %: 4-pin fans generally stall at low duty, so Home Assistant would report the fan on while it sits still.

3. Restart, then hand it to HomeKit

After restarting Home Assistant the entity appears under Developer tools → States. To reach the Home app, add the HomeKit Bridge integration, include the entity, and scan the pairing code.

Two things to expect. State is polled roughly every 30 seconds, so a change made in the board's own panel takes a moment to show up in Home Assistant — fine for on/off, but the two can briefly disagree. And "off" is persistent: it writes 0 % to the board's stored settings, so the fan stays off through a power cut rather than being a momentary override.

Note: installing erases any fan names/speeds and Wi-Fi credentials already saved on the board (you'll be asked to confirm).