For context — a couple of years ago I built a PID Fan Controller on an Olimex EVB board (for POE support) to control a fan affixed to the top of my cabinet.
I haven’t touched it (or my other ESPHome devices) for over a year for the sake of “if it ain’t broke…”, but found myself rattling through them all yesterday to bump them all to the latest version, just to keep on top of things. For more context, with a newborn on the way, I’m not going to have time in a few months…
Everything else went through fine, however, my fan controller now appears to be stuck at 100% and cannot for the life of me figure out why. I’ve changed nothing in the configuration, other than a few errors regarding configuration changes and deprecations—a single line, here or there.
There is a warning that the fan PWM is configured on GPIO02, a strapping pin, but this wasn’t a problem in the previous build, again, unless framework tweaks…
Below is my entire config for the device, not sure if someone can spot anything glaringly obvious.
Apologies it’s a bit of a dump, I thought it would be easier to print out the entire validated config for context-awareness.
Summary
INFO ESPHome 2025.2.2
INFO Reading configuration olimex-server-cabinet.yaml...
INFO Detected timezone 'Europe/London'
WARNING GPIO2 is a strapping PIN and should only be used for I/O with care.
Attaching external pullup/down resistors to strapping pins can cause unexpected failures.
See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins
WARNING GPIO5 is a strapping PIN and should only be used for I/O with care.
Attaching external pullup/down resistors to strapping pins can cause unexpected failures.
See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins
WARNING GPIO2 is a strapping PIN and should only be used for I/O with care.
Attaching external pullup/down resistors to strapping pins can cause unexpected failures.
See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins
substitutions:
esphome_version: 2025.2.2
device: evb
device__friendly: EVB
pin__tx: '1'
pin__tx2: '10'
pin__rx: '3'
pin__rx2: '9'
pin__can_tx: '5'
pin__can_rx: '35'
pin__i2c_sda: '13'
pin__i2c_scl: '16'
pin__spi_cs: '17'
pin__ir_tx: '12'
pin__ir_rx: '39'
pin__relay_1: '32'
pin__relay_2: '33'
pin__ethernet_mdc: '23'
pin__ethernet_mdio: '18'
pin__hs2_clk: '14'
pin__hs2_cmd: '15'
pin__hs2_data: '2'
pin__button: '34'
manufacturer: olimex
manufacturer__friendly: Olimex
name: olimex-server-cabinet
name__friendly: Server Cabinet
description: Olimex EVB configured for monitoring temperature within the Server
Cabinet, and adjusting it's top-case fan.
version: 2025.3.4-1
fan_min: 5%
fan_max: 100%
temp_target: '27.5'
temp_min: '20'
temp_max: '35'
pid_kp: '0.90000'
pid_ki: '0.00135'
pid_kd: '0.00000'
cron_autotune: 0 0 0 * * 6
esphome:
name: olimex-server-cabinet
comment: Olimex EVB configured for monitoring temperature within the Server Cabinet,
and adjusting it's top-case fan. (2025.3.4-1)
project:
name: jamieshaw.olimex-server-cabinet
version: 2025.3.4-1
on_boot:
- priority: -100.0
then:
- delay: 1s
- binary_sensor.template.publish:
id: esphome_ready
state: true
- logger.log:
tag: -.ready.-
level: DEBUG
format: Server Cabinet has announced 'ready'
args: []
- lambda: !lambda |-
id(thermostat).set_kp(id(thermostat_kp).state);
id(thermostat).set_ki(id(thermostat_ki).state);
id(thermostat).set_kd(id(thermostat_kd).state);
min_version: 2025.2.2
build_path: build/olimex-server-cabinet
friendly_name: ''
area: ''
platformio_options: {}
includes: []
libraries: []
name_add_mac_suffix: false
esp32:
board: esp32-evb
framework:
version: 2.0.5
advanced:
ignore_efuse_custom_mac: false
source: ~3.20005.0
platform_version: platformio/[email protected]
type: arduino
flash_size: 4MB
variant: ESP32
logger:
baud_rate: 115200
tx_buffer_size: 512
deassert_rts_dtr: false
hardware_uart: UART0
level: DEBUG
logs: {}
ota:
- platform: esphome
password: !secret 'ota_password'
version: 2
port: 3232
button:
- platform: shutdown
name: Server Cabinet Shutdown
disabled_by_default: true
id: esphome_shutdown
icon: mdi:power
entity_category: config
- platform: restart
name: Server Cabinet Restart
id: esphome_restart
disabled_by_default: false
icon: mdi:restart
entity_category: config
device_class: restart
- platform: safe_mode
name: Server Cabinet Restart (Safe Mode)
id: esphome_restart_safe
disabled_by_default: false
icon: mdi:restart-alert
entity_category: config
device_class: restart
- platform: template
name: Server Cabinet Thermostat Autotune
entity_category: config
icon: mdi:car-cruise-control
on_press:
- then:
- script.execute:
id: autotune
disabled_by_default: false
api:
encryption:
key: !secret 'api_encryption'
port: 6053
password: ''
reboot_timeout: 15min
http_request:
useragent: olimex-server-cabinet/2025.3.4-1 (Olimex EVB; olimex; evb) ESPHome/2025.2.2
timeout: 10s
verify_ssl: false
follow_redirects: true
redirect_limit: 3
binary_sensor:
- platform: status
name: Server Cabinet Status
id: esphome_status
disabled_by_default: false
entity_category: diagnostic
device_class: connectivity
- platform: template
name: Server Cabinet Ready
device_class: running
entity_category: diagnostic
id: esphome_ready
disabled_by_default: false
- platform: template
name: Server Cabinet Overheat
device_class: heat
lambda: !lambda |-
if (std::isnan(id(thermostat).current_temperature))
return {};
else if (id(thermostat).current_temperature > id(temperature_max))
return true;
else
return false;
id: overheat
disabled_by_default: false
- platform: template
name: Server Cabinet Underheat
device_class: cold
lambda: !lambda |-
if (std::isnan(id(thermostat).current_temperature))
return {};
else if (id(thermostat).current_temperature < id(temperature_min))
return true;
else
return false;
id: underheat
disabled_by_default: false
text_sensor:
- platform: version
name: Server Cabinet ESPHome Version
hide_timestamp: true
icon: mdi:application-import
id: esphome_version
disabled_by_default: false
entity_category: diagnostic
web_server:
version: 2
port: !secret 'server_port'
auth:
username: !secret 'server_name'
password: !secret 'server_pass'
include_internal: true
ota: true
enable_private_network_access: true
log: true
css_url: ''
js_url: https://oi.esphome.io/v2/www.js
ethernet:
mdc_pin: 23
mdio_pin: 18
clk_mode: GPIO0_IN
domain: !secret 'wifi_domain_name'
phy_addr: 0
type: LAN8720
globals:
- id: temperature_min
type: float
restore_value: false
initial_value: '20'
- id: temperature_max
initial_value: '35'
type: float
restore_value: false
time:
- platform: sntp
on_time:
- then:
- if:
condition:
switch.is_on:
id: autotune_schedule
then:
- script.execute:
id: autotune
else:
- logger.log:
tag: debug.script.pid.autotune
level: INFO
format: 'Ignoring request for scheduled autotuning: schedule not enabled'
args: []
seconds:
- 0
minutes:
- 0
hours:
- 0
days_of_month:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
months:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
days_of_week:
- 6
timezone: GMT0BST,M3.5.0/1,M10.5.0
update_interval: 15min
servers:
- 0.pool.ntp.org
- 1.pool.ntp.org
- 2.pool.ntp.org
switch:
- platform: template
name: Server Cabinet Thermostat Autotune Schedule
entity_category: config
restore_mode: RESTORE_DEFAULT_ON
optimistic: true
icon: mdi:car-speed-limiter
id: autotune_schedule
disabled_by_default: false
assumed_state: false
number:
- platform: template
name: Server Cabinet Thermostat KP
entity_category: config
unit_of_measurement: '%'
mode: BOX
initial_value: 0.9
step: 0.001
min_value: 0.0
max_value: 1.0
restore_value: true
optimistic: true
update_interval: 5s
icon: mdi:chart-bell-curve
id: thermostat_kp
set_action:
then:
- lambda: !lambda |-
id(thermostat).set_kp(x);
disabled_by_default: false
- platform: template
name: Server Cabinet Thermostat KI
id: thermostat_ki
initial_value: 0.00135
set_action:
then:
- lambda: !lambda |-
id(thermostat).set_ki(x);
entity_category: config
unit_of_measurement: '%'
mode: BOX
step: 0.001
min_value: 0.0
max_value: 1.0
restore_value: true
optimistic: true
update_interval: 5s
icon: mdi:chart-bell-curve
disabled_by_default: false
- platform: template
name: Server Cabinet Thermostat KD
id: thermostat_kd
initial_value: 0.0
max_value: 20.0
set_action:
then:
- lambda: !lambda |-
id(thermostat).set_kd(x);
entity_category: config
unit_of_measurement: '%'
mode: BOX
step: 0.001
min_value: 0.0
restore_value: true
optimistic: true
update_interval: 5s
icon: mdi:chart-bell-curve
disabled_by_default: false
output:
- platform: ledc
pin:
number: 2
allow_other_uses: true
mode:
output: true
input: false
open_drain: false
pullup: false
pulldown: false
inverted: false
ignore_pin_validation_error: false
ignore_strapping_warning: false
drive_strength: 20.0
frequency: 25000.0
min_power: 0.05
max_power: 1.0
zero_means_zero: true
id: output_cool
climate:
- platform: pid
name: Server Cabinet Thermostat
sensor: temperature
cool_output: output_cool
default_target_temperature: 27.5
visual:
min_temperature: 20.0
max_temperature: 35.0
control_parameters:
kp: 0.9
ki: 0.00135
kd: 0.0
starting_integral_term: 0.0
min_integral: -1.0
max_integral: 1.0
derivative_averaging_samples: 1
output_averaging_samples: 1
id: thermostat
on_state:
- then:
- if:
condition:
binary_sensor.is_on:
id: esphome_ready
then:
- number.set:
id: thermostat_kp
value: !lambda |-
return id(thermostat).get_kp();
- number.set:
id: thermostat_ki
value: !lambda |-
return id(thermostat).get_ki();
- number.set:
id: thermostat_kd
value: !lambda |-
return id(thermostat).get_kd();
- lambda: !lambda |-
bool actv_thermo = id(thermostat).mode != 0;
bool actv_fan = id(fan_control).state != 0;
if (actv_thermo != actv_fan) {
auto call = (actv_thermo) ? id(fan_control).turn_on() : id(fan_control).turn_off();
call.perform();
}
disabled_by_default: false
fan:
- platform: binary
name: Server Cabinet Fan
output: output_cool
restore_mode: RESTORE_DEFAULT_ON
id: fan_control
on_turn_on:
- then:
- if:
then:
- climate.control:
id: thermostat
mode: COOL
condition:
binary_sensor.is_on:
id: esphome_ready
on_turn_off:
- then:
- if:
then:
- climate.control:
id: thermostat
mode: 'OFF'
condition:
binary_sensor.is_on:
id: esphome_ready
disabled_by_default: false
sensor:
- platform: pulse_counter
name: Server Cabinet Fan Speed
pin:
number: 5
mode:
input: true
pullup: true
output: false
open_drain: false
pulldown: false
inverted: false
ignore_pin_validation_error: false
ignore_strapping_warning: false
drive_strength: 20.0
state_class: measurement
entity_category: diagnostic
unit_of_measurement: RPM
accuracy_decimals: 0
update_interval: 1s
filters:
- multiply: 0.5
- exponential_moving_average:
alpha: 0.15
send_every: 1
send_first_at: 1
- or:
- delta:
value: 5.0
type: absolute
- heartbeat: 60s
icon: mdi:fan-clock
id: fan_speed
disabled_by_default: false
force_update: false
count_mode:
rising_edge: INCREMENT
falling_edge: DISABLE
use_pcnt: true
internal_filter: 13us
- platform: duty_cycle
name: Server Cabinet Fan Duty
pin:
number: 2
allow_other_uses: true
mode:
input: true
output: false
open_drain: false
pullup: false
pulldown: false
inverted: false
ignore_pin_validation_error: false
ignore_strapping_warning: false
drive_strength: 20.0
state_class: measurement
entity_category: diagnostic
accuracy_decimals: 0
update_interval: 1s
icon: mdi:gauge
id: fan_duty
disabled_by_default: false
force_update: false
unit_of_measurement: '%'
- platform: pid
name: Server Cabinet Thermostat P Term
state_class: measurement
entity_category: diagnostic
disabled_by_default: true
climate_id: thermostat
type: PROPORTIONAL
force_update: false
unit_of_measurement: '%'
icon: mdi:gauge
accuracy_decimals: 1
- platform: pid
name: Server Cabinet Thermostat I Term
type: INTEGRAL
state_class: measurement
entity_category: diagnostic
disabled_by_default: true
climate_id: thermostat
force_update: false
unit_of_measurement: '%'
icon: mdi:gauge
accuracy_decimals: 1
- platform: pid
name: Server Cabinet Thermostat D Term
type: DERIVATIVE
state_class: measurement
entity_category: diagnostic
disabled_by_default: true
climate_id: thermostat
force_update: false
unit_of_measurement: '%'
icon: mdi:gauge
accuracy_decimals: 1
- platform: pid
name: Server Cabinet Thermostat Result
type: RESULT
icon: mdi:arrow-collapse-vertical
state_class: measurement
entity_category: diagnostic
disabled_by_default: true
climate_id: thermostat
force_update: false
unit_of_measurement: '%'
accuracy_decimals: 1
- platform: pid
name: Server Cabinet Thermostat Error
type: ERROR
filters:
- multiply: -1.0
icon: mdi:sun-snowflake
state_class: measurement
entity_category: diagnostic
disabled_by_default: true
climate_id: thermostat
force_update: false
unit_of_measurement: '%'
accuracy_decimals: 1
- platform: am2320
i2c_id: climate_upper
address: 0x5C
update_interval: 5s
setup_priority: -100.0
temperature:
name: Server Cabinet Upper Temperature
id: climate_upper__temperature
disabled_by_default: false
force_update: false
unit_of_measurement: °C
accuracy_decimals: 1
device_class: temperature
state_class: measurement
humidity:
name: Server Cabinet Upper Humidity
id: climate_upper__humidity
disabled_by_default: false
force_update: false
unit_of_measurement: '%'
accuracy_decimals: 1
device_class: humidity
state_class: measurement
- platform: am2320
i2c_id: climate_lower
address: 0x5C
update_interval: 5s
setup_priority: -100.0
temperature:
name: Server Cabinet Lower Temperature
id: climate_lower__temperature
disabled_by_default: false
force_update: false
unit_of_measurement: °C
accuracy_decimals: 1
device_class: temperature
state_class: measurement
humidity:
name: Server Cabinet Lower Humidity
id: climate_lower__humidity
disabled_by_default: false
force_update: false
unit_of_measurement: '%'
accuracy_decimals: 1
device_class: humidity
state_class: measurement
- platform: template
name: Server Cabinet Center Temperature
state_class: measurement
device_class: temperature
unit_of_measurement: °C
update_interval: 5s
setup_priority: -400.0
lambda: !lambda |-
float sum = 0;
uint16_t cnt = 0;
if (!std::isnan(id(climate_upper__temperature).state)) {
sum += id(climate_upper__temperature).state;
cnt += 1;
}
if (!std::isnan(id(climate_lower__temperature).state)) {
sum += id(climate_lower__temperature).state;
cnt += 1;
}
if (cnt == 0) {
return NAN;
} else {
return ((sum / cnt) * 1.15);
}
id: climate_center__temperature
disabled_by_default: false
force_update: false
accuracy_decimals: 1
- platform: template
name: Server Cabinet Center Humidity
device_class: humidity
unit_of_measurement: '%'
lambda: !lambda |-
float sum = 0;
uint16_t cnt = 0;
if (!std::isnan(id(climate_upper__humidity).state)) {
sum += id(climate_upper__humidity).state;
cnt += 1;
}
if (!std::isnan(id(climate_lower__humidity).state)) {
sum += id(climate_lower__humidity).state;
cnt += 1;
}
if (cnt == 0) {
return NAN;
} else {
return ((sum / cnt) * 0.85);
}
id: climate_center__humidity
state_class: measurement
update_interval: 5s
setup_priority: -400.0
disabled_by_default: false
force_update: false
accuracy_decimals: 1
- platform: template
name: Server Cabinet Temperature
setup_priority: -500.0
filters:
- exponential_moving_average:
alpha: 0.075
send_every: 1
send_first_at: 1
lambda: !lambda |-
float sum = 0;
uint16_t cnt = 0;
if (!std::isnan(id(climate_upper__temperature).state)) {
sum += id(climate_upper__temperature).state;
cnt += 1;
}
if (!std::isnan(id(climate_center__temperature).state)) {
sum += id(climate_center__temperature).state;
cnt += 1;
}
if (!std::isnan(id(climate_lower__temperature).state)) {
sum += id(climate_lower__temperature).state;
cnt += 1;
}
if (cnt == 0) {
return NAN;
} else {
return (sum / cnt);
}
id: temperature
state_class: measurement
device_class: temperature
unit_of_measurement: °C
update_interval: 5s
disabled_by_default: false
force_update: false
accuracy_decimals: 1
- platform: template
name: Server Cabinet Humidity
setup_priority: -500.0
filters:
- exponential_moving_average:
alpha: 0.075
send_every: 1
send_first_at: 1
lambda: !lambda |-
float sum = 0;
uint16_t cnt = 0;
if (!std::isnan(id(climate_upper__humidity).state)) {
sum += id(climate_upper__humidity).state;
cnt += 1;
}
if (!std::isnan(id(climate_center__humidity).state)) {
sum += id(climate_center__humidity).state;
cnt += 1;
}
if (!std::isnan(id(climate_lower__humidity).state)) {
sum += id(climate_lower__humidity).state;
cnt += 1;
}
if (cnt == 0) {
return NAN;
} else {
return (sum / cnt);
}
id: humidity
device_class: humidity
unit_of_measurement: '%'
state_class: measurement
update_interval: 5s
disabled_by_default: false
force_update: false
accuracy_decimals: 1
script:
- id: autotune
then:
- logger.log:
format: Starting autotuning process…
tag: debug.script.pid.autotune
level: INFO
args: []
- climate.pid.autotune:
id: thermostat
positive_output: 1.0
noiseband: 0.25
negative_output: -1.0
mode: single
parameters: {}
i2c:
- scl: 16
sda: 13
scan: false
frequency: 50000.0
id: climate_upper
- scl: 1
sda: 3
id: climate_lower
scan: false
frequency: 50000.0