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
- 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.
- Exhaust — a 4-wire fan on a 25 kHz LEDC output, exposed as a
speedfan with five levels. Underexhaust_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 andexhaust_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. - Filter fan — optional second PWM fan. A Home Assistant binary that is
onduring a job starts it. When the binary goesoff, the same script (moderestart) waits outfilter_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. - 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
frequencyto 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
- 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.
- 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_cfor the test. Let it cool and confirm it steps down to off. Put the real thresholds back. - Adopt the device in Home Assistant. Point
print_entityat 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. - 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_cis 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_onand the temperature is under the on-threshold. Aturn_offin that branch skips the ramp. - Filter fan never tracks the print.
binary_sensor.printer_printingis the wrong id, or it staysonwhen idle. Watch that entity during a job before you change the fan. - Filter fan stops as soon as the print starts.
wait_untilis matching anoffthat is still the idle state becauseon_pressand the binary raced. The script ismode: restartandon_releasealso 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.