I am not a gamer so my computer doesn’t normally run very hot. Recently my son started playing video games together and we found that the heat coming from under the desk (office style with closed back) is unbearable after playing for a while. In the graph below you can see that the room is at about 25C (electronics warm it up!) and the computer exhaust was at 30C under normal office use. When the gaming started the exhaust spiked to 38C (>100F). The blower I automated with ESPHome turned up the fan speed and sitting at the desk was way more pleasant. Where the two temperatures match is when I put the computer to sleep. While it really doesn’t matter, it is likely that one of the sensors is a bit off as I would expect the two to match when the computer is off.
This is the assembly while I was testing it. The oversized white cable gland is there because there was a huge hole in the box and the cable gland fit well. It looks terrible but works and it was a quick and easy solution with what I had. I added a power connector right next to it. The 4 screws are what hold the PCB board I built.
The assembly shown in the picture is mounted under table behind my desk and currently just blows the warm exhaust into the room, but I can easily add a flexible conduit to outside the door if it gets too hot in the room.
With minimal testing I found that the fan runs at 12V and takes a PWM signal for speed control. The power electronics that control the motor are inside the motor so automating the fan was as simple as providing a 5V PWM signal straight from the ESP32. The power supply is 12V 2A as the fan requires around 12V 1A, so I added a small buck converter that brings the voltage down to 5V for the ESP32. I need to look into smaller variants as the one I used is overkill. Anyone have suggestions?
The only trouble I had with this build is that I connected the data line of the DS18B20 temperature sensor to D13 and it did not work… maybe the IO is broken or maybe I am missing something… anyhow connecting it to D27 worked perfectly.
D23 provides the PWM signal.
To make it all fit I had to use low profile standoffs and grind down some plastic parts inside the box.
This is a newer version of the blower I have… the controller shown is “dumb” and the LED backlight broke as in 2 previous ones I had… Hopefully AC Infinity figured their issues out on newer versions. Anyhow, it is way cooler now (pun intended) that I can control it via Home Assistant thanks to ESPHome!
Below is the YAML I used. It requires a few helpers to be created for it to interface to HA. I am certain there are better ways to control the fan speed so if anyone has suggestions I am glad to try them out!
EDIT (12-JUN-2023): YAML below is now replaced by what should be a better revision… see next code block.
substitutions:
devicename: computer-exhaust-fan
devicename_no_dashes: computer_exhaust_fan
friendly_devicename: "Computer Exhaust Fan"
device_description: "Computer Exhaust Fan"
update_interval_s: "5s"
update_interval_wifi: "60s"
pwm00_t: "12.0" #23.0
pwm05_t: "11.5" #23.5
pwm10_t: "11.0" #24.0
pwm20_t: "10.0" #25.0
pwm30_t: "9.0" #26.0
pwm40_t: "8.0" #27.0
pwm50_t: "7.0" #28.0
pwm60_t: "6.0" #29.0
pwm70_t: "5.0" #30.0
pwm80_t: "3.0" #32.0
pwm90_t: "1.0" #34.0
pwm100_t: "0.0" #35.0
esphome:
name: ${devicename}
comment: ${device_description}
platform: ESP32
board: esp32doit-devkit-v1
# Enable logging
logger:
# Enable Home Assistant API
api:
password: !secret api_pwd
ota:
password: !secret ota_pwd
wifi:
ssid: !secret iot_wifi_ssid
password: !secret iot_wifi_password
#Faster than DHCP. Also use if can't reach because of name change
# manual_ip:
# static_ip: 192.168.3.195
# gateway: 192.168.3.1
# subnet: 255.255.255.0
# dns1: 192.168.1.25
# dns2: 192.168.1.26
#Manually override what address to use to connect to the ESP.
#Defaults to auto-generated value. Example, if you have changed your
#static IP and want to flash OTA to the previously configured IP address.
use_address: 192.168.3.195
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "${devicename} Hotspot"
password: !secret iot_wifi_password
web_server:
port: 80
include_internal: true
captive_portal:
text_sensor:
- platform: wifi_info
ip_address:
name: "${friendly_devicename}: IP"
icon: "mdi:ip-outline"
update_interval: ${update_interval_wifi}
ssid:
name: "${friendly_devicename}: SSID"
icon: "mdi:wifi-settings"
update_interval: ${update_interval_wifi}
bssid:
name: "${friendly_devicename}: BSSID"
icon: "mdi:wifi-settings"
update_interval: ${update_interval_wifi}
mac_address:
name: "${friendly_devicename}: MAC"
icon: "mdi:network-outline"
scan_results:
name: "${friendly_devicename}: Wifi Scan"
icon: "mdi:wifi-refresh"
disabled_by_default: true
#https://esphome.io/guides/automations.html?highlight=restore_value#bonus-2-global-variables
globals: ##to set default reboot behavior
- id: computer_exhaust_fan
type: bool
restore_value: no
initial_value: "true"
- id: fan_speed
type: float
restore_value: no
initial_value: '0'
- id: fan_speed_or
type: float
restore_value: no
initial_value: '0.0'
- id: max_fan_speed_temperature
type: float
restore_value: yes
initial_value: '35.0'
switch:
- platform: restart
name: "${friendly_devicename}: Restart"
button:
- platform: safe_mode
name: "${friendly_devicename}: Restart (Safe Mode)"
binary_sensor: #pull in HA value
- platform: homeassistant
id: enable_fans
internal: true
entity_id: input_boolean.${devicename_no_dashes}
on_state:
- globals.set:
id: computer_exhaust_fan
value: !lambda 'return id(enable_fans).state;'
- platform: homeassistant
id: override_control
internal: true
entity_id: input_boolean.${devicename_no_dashes}_override_speed
output:
- platform: ledc
pin: 23
frequency: 19531 Hz
id: fan_pwm
#Configuration entry for 18B20 sensor
dallas:
- pin: 27
update_interval: ${update_interval_s}
sensor:
- platform: wifi_signal
name: "${friendly_devicename}: WiFi Signal"
update_interval: ${update_interval_wifi}
device_class: signal_strength
- platform: template
id: report_fan_speed
name: "${friendly_devicename}: Fan Speed"
icon: "mdi:fan"
lambda: return id(fan_speed) * 100;
unit_of_measurement: "%"
accuracy_decimals: 0
update_interval: ${update_interval_s}
- platform: homeassistant
id: fan_speed_override
internal: true
entity_id: input_number.${devicename_no_dashes}_speed
on_value:
- globals.set:
id: fan_speed_or
value: !lambda 'return id(fan_speed_override).state;'
- platform: homeassistant
id: max_fan_speed_temp
internal: true
entity_id: input_number.${devicename_no_dashes}_max_speed_temperature
on_value:
- globals.set:
id: max_fan_speed_temperature
value: !lambda 'return id(max_fan_speed_temp).state;'
- platform: dallas
address: 0xf2020292454de528
name: "${friendly_devicename}: Temperature"
id: computer_temp
device_class: "temperature"
state_class: "measurement"
on_value:
then:
lambda: |-
if (id(computer_exhaust_fan)) {
if (!id(override_control).state) {
if (id(computer_temp).state < id(max_fan_speed_temperature) - 12.0) {
id(fan_speed) = 0.0;
}
/*Adding 0.5C to prevent constant on/off when temp is right at 23*/
else if ((id(computer_temp).state >= id(max_fan_speed_temperature) - 12.0) and (id(computer_temp).state <= id(max_fan_speed_temperature) - 11.5)) {
ESP_LOGD("PWM", "Leave PWM as is. Dead band.");
}
else if ((id(computer_temp).state >= id(max_fan_speed_temperature) - 11.5) and (id(computer_temp).state <= id(max_fan_speed_temperature) - 11.0)) {
id(fan_speed) = 0.05;
}
else if ((id(computer_temp).state > id(max_fan_speed_temperature) - 11.0) and (id(computer_temp).state <= id(max_fan_speed_temperature) - 10.0)) {
id(fan_speed) = 0.1;
}
else if ((id(computer_temp).state > id(max_fan_speed_temperature) - 10.0) and (id(computer_temp).state <= id(max_fan_speed_temperature) - 9.0)) {
id(fan_speed) = 0.2;
}
else if ((id(computer_temp).state > id(max_fan_speed_temperature) - 9.0) and (id(computer_temp).state <= id(max_fan_speed_temperature) - 8.0)) {
id(fan_speed) = 0.3;
}
else if ((id(computer_temp).state > id(max_fan_speed_temperature) - 8.0) and (id(computer_temp).state <= id(max_fan_speed_temperature) - 7.0)) {
id(fan_speed) = 0.4;
}
else if ((id(computer_temp).state > id(max_fan_speed_temperature) - 7.0) and (id(computer_temp).state <= id(max_fan_speed_temperature) - 6.0)) {
id(fan_speed) = 0.5;
}
else if ((id(computer_temp).state > id(max_fan_speed_temperature) - 6.0) and (id(computer_temp).state <= id(max_fan_speed_temperature) - 5.0)) {
id(fan_speed) = 0.6;
}
else if ((id(computer_temp).state > id(max_fan_speed_temperature) - 5.0) and (id(computer_temp).state <= id(max_fan_speed_temperature) - 3.0)) {
id(fan_speed) = 0.7;
}
else if ((id(computer_temp).state > id(max_fan_speed_temperature) - 3.0) and (id(computer_temp).state <= id(max_fan_speed_temperature) - 1.0)) {
id(fan_speed) = 0.8;
}
else if ((id(computer_temp).state > id(max_fan_speed_temperature) - 1.0) and (id(computer_temp).state <= id(max_fan_speed_temperature) - 0.0)) {
id(fan_speed) = 0.9;
}
else {
id(fan_speed) = 1.0;
ESP_LOGD("ALERT", "OVER int(id(max_fan_speed_temperature))C! Setting fan to 100");
}
ESP_LOGD("ALERT", "PWM: TEMPERATURE CONTROL");
} else {
id(fan_speed) = id(fan_speed_or) / 100;
ESP_LOGD("ALERT", "PWM: OVERRIDE : ON @ %d%%", int(id(fan_speed_or)));
//ESP_LOGD("ALERT", "PWM OVERRIDE: ON - SETPOINT: %d%%", int(id(fan_speed) * 100));
//ESP_LOGD("PWM", "PWM OVERRIDE: %d%%" , int(id(fan_speed_or)));
}
} else {
id(fan_speed) = 0.0;
ESP_LOGD("ALERT", "FANS TURNED OFF");
}
id(fan_pwm).set_level(id(fan_speed));
ESP_LOGD("PWM", "PWM: %d%%" , int(id(fan_speed) * 100));
ESP_LOGD("PWM", "TEMP THRESHOLD: %d" , int(id(max_fan_speed_temperature)));
UPDATED YAML FOR COMPUTER FAN CONTROLLER (12-JUN-2023):
substitutions:
devicename: computer-exhaust-fan
devicename_no_dashes: computer_exhaust_fan
friendly_devicename: "Computer Exhaust Fan"
device_description: "Computer Exhaust Fan"
update_interval_s: "2s"
update_interval_wifi: "120s"
#temp_calibration: "-0.7" #The DS18B20 measures about 0.7C too high
esphome:
name: ${devicename}
friendly_name: "${friendly_devicename}"
comment: ${device_description}
platform: ESP32
board: esp32doit-devkit-v1
on_boot:
then:
# Using multiple lambdas as the compiler was complaining otherwise
- lambda: |-
auto call = id(max_fan_speed_temperature_ha).make_call();
call.set_value(id(max_fan_speed_temperature));
call.perform();
- lambda: |-
auto call = id(fan_speed_override_ha).make_call();
call.set_value(id(fan_speed_override));
call.perform();
# Push values to HA for initital display
- lambda: |-
id(override_fan_speed_ha).publish_state(id(override_fan_speed));
id(computer_exhaust_fan_ha).publish_state(id(computer_exhaust_fan));
id(room_temperature_ha).publish_state(id(room_temperature_sensor).state); // Report Room Temperature from Sensor directly
id(report_room_humidity).publish_state(id(room_humidity_sensor).state); // Report Room Humidity from Sensor directly
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: JTPbDiE5vCSb2bF6MbVewjgz6PSOSbuDUCQXFNGcVMQ=
ota:
password: !secret ota_pwd
wifi:
ssid: !secret iot_wifi_ssid
password: !secret iot_wifi_password
#Faster than DHCP. Also use if can't reach because of name change
# manual_ip:
# static_ip: 192.168.3.195
# gateway: 192.168.3.1
# subnet: 255.255.255.0
# dns1: 192.168.1.25
# dns2: 192.168.1.26
#Manually override what address to use to connect to the ESP.
#Defaults to auto-generated value. Example, if you have changed your
#static IP and want to flash OTA to the previously configured IP address.
use_address: 192.168.3.195
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "${devicename}"
password: !secret iot_wifi_password
web_server:
port: 80
include_internal: true
captive_portal:
# Sync time with Home Assistant
time:
- platform: homeassistant
id: ha_time
i2c:
- id: bus_a
sda: 32
scl: 33
scan: true
#Configuration entry for 18B20 sensor
dallas:
- pin: 27
update_interval: ${update_interval_s}
text_sensor:
- platform: wifi_info
ip_address:
name: "IP"
icon: "mdi:ip-outline"
update_interval: ${update_interval_wifi}
ssid:
name: "SSID"
icon: "mdi:wifi-settings"
update_interval: ${update_interval_wifi}
bssid:
name: "BSSID"
icon: "mdi:wifi-settings"
update_interval: ${update_interval_wifi}
mac_address:
name: "MAC"
icon: "mdi:network-outline"
scan_results:
name: "Wifi Scan"
icon: "mdi:wifi-refresh"
disabled_by_default: true
#https://esphome.io/guides/automations.html?highlight=restore_value#bonus-2-global-variables
globals: ##to set default reboot behavior
# Variables to recall (saves them after 1 minute by default, can be changed)
- id: computer_exhaust_fan
type: bool
restore_value: yes
initial_value: "false"
- id: override_fan_speed
type: bool
restore_value: yes
initial_value: "false"
- id: fan_speed_override
type: float
restore_value: yes
initial_value: '0.0'
- id: max_fan_speed_temperature
type: float
restore_value: yes
initial_value: '35.0'
- id: fan_speed
type: float
restore_value: no
initial_value: '0'
- id: computer_exhaust_temperature
type: float
restore_value: no
initial_value: '0.0'
- id: room_temperature
type: float
restore_value: no
initial_value: '0.0'
- id: room_humidity
type: float
restore_value: no
initial_value: '0.0'
switch:
- platform: restart
name: "Restart"
- platform: template
name: ""
id: computer_exhaust_fan_ha
icon: "mdi:fan"
optimistic: true
turn_on_action:
- globals.set:
id: computer_exhaust_fan
value: 'true'
turn_off_action:
- globals.set:
id: computer_exhaust_fan
value: 'false'
- platform: template
name: "Override Fan Speed"
id: override_fan_speed_ha
icon: "mdi:fan"
optimistic: true
turn_on_action:
- globals.set:
id: override_fan_speed
value: 'true'
turn_off_action:
- globals.set:
id: override_fan_speed
value: 'false'
button:
- platform: safe_mode
name: "Restart (Safe Mode)"
output:
- platform: ledc
pin: 23
frequency: 19531 Hz
id: fan_pwm
number:
- platform: template
name: "Max Fan Speed Temperature"
id: max_fan_speed_temperature_ha
unit_of_measurement: "°C"
optimistic: true #Not sure what this does, research. leave? remove?
min_value: 25
max_value: 40
step: 1
on_value:
then:
- lambda: |-
id(max_fan_speed_temperature) = id(max_fan_speed_temperature_ha).state;
- platform: template
name: "Fan Speed Override"
icon: "mdi:speedometer"
id: fan_speed_override_ha
unit_of_measurement: "%"
optimistic: true #Not sure what this does, research. leave? remove?
min_value: 0
max_value: 100
step: 1
on_value:
then:
- lambda: |-
id(fan_speed_override) = id(fan_speed_override_ha).state;
sensor:
- platform: wifi_signal
name: "WiFi Signal"
update_interval: ${update_interval_wifi}
device_class: signal_strength
- platform: template
id: fan_speed_ha
name: "Fan Speed"
icon: "mdi:fan"
#lambda: return id(fan_speed) * 100;
unit_of_measurement: "%"
accuracy_decimals: 0
update_interval: never
# - platform: qmp6988
# i2c_id: bus_a
# temperature:
# name: "Temperature 2"
# oversampling: 16x
# pressure:
# name: "Pressure"
# oversampling: 16x
# address: 0x70
# update_interval: 5s
# iir_filter: 2x
- platform: sht3xd
i2c_id: bus_a
address: 0x44
update_interval: 1s
humidity:
id: room_humidity_sensor
internal: true
force_update: true
device_class: "humidity"
state_class: "measurement"
on_value:
- globals.set:
id: room_humidity
value: !lambda 'return id(room_humidity_sensor).state;'
temperature:
id: room_temperature_sensor
internal: true
force_update: true
device_class: "temperature"
state_class: "measurement"
on_value:
- globals.set:
id: room_temperature
value: !lambda 'return id(room_temperature_sensor).state;'
- platform: template
name: "Room Temperature"
id: room_temperature_ha
device_class: "temperature"
state_class: "measurement"
unit_of_measurement: "°C"
update_interval: never
filters:
# Map MEASURED -> TRUTH
- calibrate_linear:
- 0.0 -> 0.0
- 25.0 -> 24.2
- 40.0 -> 39.2
- lambda: return x;
- platform: template
name: "Room Humidity"
id: report_room_humidity
device_class: "humidity"
state_class: "measurement"
unit_of_measurement: "%"
update_interval: never
- platform: dallas
address: 0xf2020292454de528
id: computer_exhaust_temperature_sensor
device_class: "temperature"
state_class: "measurement"
unit_of_measurement: "°C"
filters:
- sliding_window_moving_average:
window_size: 10
send_every: 10
# Map MEASURED -> TRUTH
- calibrate_linear:
- 0.0 -> 0.0
- 20.0 -> 19.1
- 25.0 -> 24.1
- 40.0 -> 39.1
- lambda: return x;
on_value:
- globals.set:
id: computer_exhaust_temperature
value: !lambda 'return id(computer_exhaust_temperature_sensor).state;'
- platform: template
name: "Exhaust Temperature"
device_class: "temperature"
state_class: "measurement"
unit_of_measurement: "°C"
id: computer_exhaust_temperature_ha
update_interval: never
interval:
- interval: 2s
then:
lambda: |-
if (id(computer_exhaust_fan)) {
if (!id(override_fan_speed)) {
if (id(computer_exhaust_temperature) > id(max_fan_speed_temperature)) {
id(fan_speed) = 1.0;
ESP_LOGD("ALERT", "OVER int(id(max_fan_speed_temperature))C! Setting fan to 100");
}
else if (id(computer_exhaust_temperature) <= id(room_temperature_sensor).state + 1.0) {
id(fan_speed) = 0.00;
ESP_LOGD("PWM", "Minimum fan speed as exhaust is <= ambient.");
}
else {
id(fan_speed) = (1.0 - 0.00) * ((id(computer_exhaust_temperature) - id(room_temperature_sensor).state)/(id(max_fan_speed_temperature) - id(room_temperature_sensor).state));
}
ESP_LOGD("PWM", "TEMPERATURE CONTROL - Max Speed @ %dC - CURRENT PWM: %d%%" , int(id(max_fan_speed_temperature)), int(id(fan_speed) * 100));
} else {
id(fan_speed) = id(fan_speed_override) / 100;
ESP_LOGD("ALERT", "PWM OVERRIDE: %d%% - CURRENT PWM: %d%%", int(id(fan_speed_override)), int(id(fan_speed) * 100));
}
} else {
id(fan_speed) = 0.0;
ESP_LOGD("ALERT", "FANS TURNED OFF");
}
id(fan_pwm).set_level(id(fan_speed));
- interval: 10s
then:
lambda: |-
id(computer_exhaust_temperature_ha).publish_state(id(computer_exhaust_temperature_sensor).state); // Report Computer Exhaust Temperature from Sensor directly
id(fan_speed_ha).publish_state(id(fan_speed) * 100);
- interval: 60s
then:
lambda: |-
id(room_temperature_ha).publish_state(id(room_temperature)); // Report Room Temperature Sensor
id(report_room_humidity).publish_state(id(room_humidity)); // Report Room Humidity Sensor
script:
- id: setup_controls
then:
- lambda: |-
auto call = id(max_fan_speed_temperature_ha).make_call();
call.set_value(id(max_fan_speed_temperature));
call.perform();
- lambda: |-
auto call = id(fan_speed_override_ha).make_call();
call.set_value(id(fan_speed_override));
call.perform();
- lambda: |-
id(override_fan_speed_ha).publish_state(id(override_fan_speed));
id(computer_exhaust_fan_ha).publish_state(id(computer_exhaust_fan));
- id: setup_sensors
then:
- lambda: |-
id(room_temperature_ha).publish_state(id(room_temperature_sensor).state); // Report Room Temperature Sensor
id(report_room_humidity).publish_state(id(room_humidity_sensor).state); // Report Room Humidity Sensor