ESPHome3D printingHome Assistant

Printer enclosure fan controller

An ESP32 in the enclosure reads a DHT22 and ramps a 4-wire exhaust fan when the chamber crosses a temperature threshold, runs an optional carbon-filter fan during a print and for a run-on after, then steps the fans back down once the air is stable.

Difficulty · IntermediateSetup · 1–2 hoursHA · current stable (native API)ESPHome · current stable
Get the pack · from $19Have us set it up

Some hardware links on this page are affiliate links. As an Amazon Associate I earn from qualifying purchases. We may earn a commission if you buy through them, at no extra cost to you. See the affiliate disclosure.

A printer enclosure that only has a manual fan either cooks the electronics or runs full speed through the whole job. This pack puts an ESP32 in the box with a DHT22. Chamber temperature drives a 4-wire PWM exhaust fan: off below a threshold, a middle speed once you cross it, full speed when it is actually hot, and a step down every sensor update once the air is back under the line. An optional second fan recirculates a carbon filter while a print is running and for a few minutes after, then stops.

Wi-Fi credentials stay in secrets.yaml. The print flag is a Home Assistant entity id you already have (binary_sensor.printer_printing in the snippet). Never paste SSIDs, PSKs, or API keys into a published YAML.

3D printer watchdog is the print-failed, filament, and dashboard pack. This page only moves air. Use both if you want a watchdog and an enclosure climate loop. They do not share automations.

What problem it solves

You want the chamber exhausted when it is hot, a filter loop that follows the print instead of a switch you forget, and fans that ramp down after the box cools instead of slamming off or running all night.

How it works

  1. Sense — DHT22 temperature and humidity every 30 seconds. Humidity is published for the dashboard. The fan decisions use temperature only, with a short filter so a single spike does not change speed.
  2. Exhaust — a 4-wire fan on a 25 kHz LEDC output, exposed as a speed fan with five levels. Under exhaust_on_c (pack default 35 °C) the fan is not turned up. If it is already on, each temperature update drops one level until it is off. Between the on-threshold and exhaust_full_c (pack default 45 °C) it holds a middle speed. At or above the full threshold it goes to the top level. That is the ramp: up in two steps as the chamber climbs, down one level per half-minute once it is stable again.
  3. Filter fan — optional second PWM fan. A Home Assistant binary that is on during a job starts it. When the binary goes off, the same script (mode restart) waits out filter_runon (pack default 10 minutes) and then turns it off. A new print during the run-on restarts the script, so the fan does not stop mid-job. If that binary does not exist, leave the sensor out and the exhaust path still runs.
  4. Power — the fan’s 12 V leads stay on the 12 V supply. The ESP32 only drives the PWM wire (or a MOSFET gate). Common ground between the supply and the board, or the PWM signal has no reference and the fan ignores it.

Hardware notes

  • Board. 30-pin ESP32. DHT data on GPIO4. Exhaust PWM on GPIO18. Filter PWM on GPIO19. Those are outputs. Do not use the input-only ADC pins (GPIO34–GPIO39) for PWM.
  • Sensor. A DHT22 / AM2302 module. Power it from 3V3 if the module is happy there; do not feed a 5 V data line into the ESP32. Modules usually include the data pull-up. Give it a second after boot before the first real sample.
  • 4-wire fan. Arctic P12 PWM or any 12 V fan with a separate PWM wire. Standard coloring is black ground, red 12 V, yellow PWM, green tachometer. Tach is optional and unused here. PWM is 25 kHz. A 3.3 V GPIO is enough for most PC fans. Do not put 12 V on any ESP32 pin.
  • 2-wire fan. No PWM wire. Use the IRLZ44N on the low side of the 12 V lead (drain to the fan negative, source to ground, gate to the LEDC pin). Put a diode across the fan, cathode to 12 V. Drop that output’s frequency to about 1000 Hz. A non-logic-level MOSFET will not turn on from 3.3 V. The 4-wire path does not need the MOSFET.
  • 12 V supply. Sized for the fans, not taken from the ESP32’s 5 V pin. Tie the grounds together once.

Wiring

4-wire fan Connection
Black (ground) 12 V supply ground and ESP32 GND
Red (12 V) 12 V supply positive
Yellow (PWM) GPIO18 (exhaust) or GPIO19 (filter)
Green (tach) Leave unconnected
DHT22 module ESP32
VCC 3V3
GND GND
DATA GPIO4

Snippet

substitutions:
  device_name: enclosure-fans
  friendly_name: Enclosure fans
  dht_pin: GPIO4
  exhaust_pin: GPIO18
  filter_pin: GPIO19
  exhaust_on_c: "35"
  exhaust_full_c: "45"
  filter_runon: 10min
  print_entity: binary_sensor.printer_printing

esphome:
  name: ${device_name}
  friendly_name: ${friendly_name}

esp32:
  board: esp32dev
  framework:
    type: esp-idf

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

api:
  encryption:
    key: !secret api_encryption_key

ota:
  - platform: esphome
    password: !secret ota_password

sensor:
  - platform: dht
    pin: ${dht_pin}
    model: DHT22
    update_interval: 30s
    temperature:
      name: "Enclosure temperature"
      id: chamber_temp
      filters:
        - sliding_window_moving_average:
            window_size: 3
            send_every: 1
      on_value:
        then:
          - script.execute: exhaust_from_temp
    humidity:
      name: "Enclosure humidity"

binary_sensor:
  - platform: homeassistant
    id: printing
    entity_id: ${print_entity}
    on_press:
      - script.execute: filter_cycle
    on_release:
      - script.execute: filter_cycle

output:
  - platform: ledc
    id: exhaust_pwm
    pin: ${exhaust_pin}
    frequency: 25000Hz
  - platform: ledc
    id: filter_pwm
    pin: ${filter_pin}
    frequency: 25000Hz

fan:
  - platform: speed
    id: exhaust_fan
    output: exhaust_pwm
    name: "Enclosure exhaust"
    speed_count: 5
  - platform: speed
    id: filter_fan
    output: filter_pwm
    name: "Enclosure filter"
    speed_count: 5

script:
  - id: exhaust_from_temp
    mode: restart
    then:
      - if:
          condition:
            lambda: 'return !isnan(id(chamber_temp).state) && id(chamber_temp).state >= ${exhaust_full_c};'
          then:
            - fan.turn_on:
                id: exhaust_fan
                speed: 5
          else:
            - if:
                condition:
                  lambda: 'return !isnan(id(chamber_temp).state) && id(chamber_temp).state >= ${exhaust_on_c};'
                then:
                  - fan.turn_on:
                      id: exhaust_fan
                      speed: 3
                else:
                  - if:
                      condition:
                        fan.is_on: exhaust_fan
                      then:
                        - lambda: |-
                            int next = id(exhaust_fan).speed - 1;
                            if (next <= 0) {
                              auto call = id(exhaust_fan).turn_off();
                              call.perform();
                            } else {
                              auto call = id(exhaust_fan).turn_on();
                              call.set_speed(next);
                              call.perform();
                            }
  - id: filter_cycle
    mode: restart
    then:
      - fan.turn_on:
          id: filter_fan
          speed: 3
      - wait_until:
          condition:
            binary_sensor.is_off: printing
      - delay: ${filter_runon}
      - fan.turn_off: filter_fan

Speed levels are 1 through 5 (20 percent steps). Level 3 is the cruise speed. Level 5 is the hot speed. Below the on-threshold the lambda sheds one level per temperature update, so a fan that was at full takes a couple of minutes to stop instead of cutting the moment the sensor dips.

Delete the filter binary_sensor, the filter output / fan, and filter_cycle if you only have an exhaust fan. For a 2-wire exhaust fan, change that LEDC frequency to 1000Hz and wire the MOSFET as in the hardware notes. Leave 25 kHz in place for a real 4-wire PWM pin.

Setup outline

  1. Wire the DHT22 and one fan on the bench, with the 12 V supply and a shared ground. Do not connect fan positive to the ESP32.
  2. Flash the YAML. Warm the DHT22 with your hand and confirm the exhaust fan steps to speed 3, then 5 if you lower exhaust_full_c for the test. Let it cool and confirm it steps down to off. Put the real thresholds back.
  3. Adopt the device in Home Assistant. Point print_entity at a binary that is on only while a job is running (a template over the printer’s status is fine). Start a print: the filter fan should start. Cancel or finish: it should run on, then stop.
  4. Mount the sensor in the chamber air path, not on the ESP32 and not on the hotend. Mount the exhaust so it actually leaves the box.

Troubleshooting

  • Fan never spins. No common ground, PWM on the tach wire, or the fan is 2-wire and you are feeding 25 kHz into a power lead with no MOSFET. Check the wire colors and the frequency note.
  • Fan stuck at full speed. exhaust_full_c is below room temperature, or another controller (the printer board) is also driving the same PWM wire. One owner.
  • It slams off instead of ramping. The else-branch turns the fan off directly. The step-down only runs when fan.is_on and the temperature is under the on-threshold. A turn_off in that branch skips the ramp.
  • Filter fan never tracks the print. binary_sensor.printer_printing is the wrong id, or it stays on when idle. Watch that entity during a job before you change the fan.
  • Filter fan stops as soon as the print starts. wait_until is matching an off that is still the idle state because on_press and the binary raced. The script is mode: restart and on_release also runs it; a contact-bounce on the binary will end the run-on early. Lengthen nothing — fix the binary so it has one clean on-period per job.
  • DHT reads 0 or nan. Data and VCC swapped, or the pin is not GPIO4. Power the module from 3V3.
  • You wanted filament runout and a failed-print ping. That belongs on 3D printer watchdog, not on this fan node.

Credits

Based on the enclosure controller in Stealthmax v2 ESPHome Controller, which ties chamber air and an exhaust fan to a print. This write-up is an original ESPHome loop: DHT22, a 25 kHz PWM exhaust that steps with temperature and ramps down when the chamber is stable, and an optional filter fan with a run-on after the print. It does not use that project’s servo vent or printer-board fan headers.

Changelog

v1.0 · Oct 20, 2026: First publish.