Hi everyone,
UPDATE (18 January): I have significantly updated the configuration to make it more robust and reliable.
Like many of you, I struggled with the Sonoff iFan03 on ESPHome. The main issues were the fan stalling/humming on “Low” speed (due to motor inertia) and the annoying buzzer.
I analyzed the hardware behavior and created a robust configuration that solves these issues completely.
Key Improvements:
- RF Reliability (Debounce): [NEW] Implemented a 300ms software debounce. This prevents the common issue where 433MHz remotes send multiple codes per press, fixing “stuttering” buzzer sounds or unintended rapid toggling of the light.
- Kickstart Logic (Boost): When turning on to Low or Medium, the fan momentarily powers HIGH (4s) to overcome friction, then drops to the target speed. No more humming motors!
- Safety Stop: It turns off all relays for 500ms before switching speeds to prevent arcing.
- Silent Mode: Added a simple option to disable the buzzer completely.
- Feedback: If the buzzer is on, it gives feedback even when controlled via Home Assistant (not just the remote).
Download & Documentation
This configuration is now officially part of the ESPHome Devices Repository!
You can find the documentation and the latest clean YAML code here:
Official Page: Sonoff iFan03 on ESPHome-Devices
(If you want to see the development history or contribute, check the GitHub repository)
YAML Configuration (Quick Copy)
For those who want to try it right now, here is the full configuration.
Click to expand the YAML code
substitutions:
name: "living-fan"
friendly_name: "Living Room Fan"
comment: "Sonoff Ifan03 Fan Controller (ESP8285)"
api_key: ""
ota_password: ""
wifi_ssid: !secret wifi_ssid
wifi_password: !secret wifi_password
on_boot_light: ALWAYS_OFF # or ALWAYS_ON
buzzer_enabled: "true"
globals:
- id: last_speed
type: int
restore_value: no
initial_value: '0' # 0=OFF, 1=LOW, 2=MED, 3=HIGH
- id: target_speed
type: int
restore_value: no
initial_value: '0'
- id: last_rf_ms
type: uint32_t
restore_value: no
initial_value: '0'
esphome:
name: $name
comment: $comment
friendly_name: $friendly_name
on_boot:
priority: -100
then:
- switch.turn_off: fan_relay1
- switch.turn_off: fan_relay2
- switch.turn_off: fan_relay3
- lambda: |-
id(last_speed) = 0;
id(target_speed) = 0;
esp8266:
board: esp8285
api:
encryption:
key: $api_key
ota:
- platform: esphome
password: $ota_password
wifi:
ssid: $wifi_ssid
password: $wifi_password
min_auth_mode: WPA2
ap:
ssid: "$name AP"
password: $wifi_password
captive_portal:
logger:
level: INFO
baud_rate: 0
logs:
remote_receiver: WARN
sensor:
- platform: wifi_signal
name: "WiFi Signal"
update_interval: 60s
entity_category: diagnostic
button:
- platform: restart
name: "restart"
entity_category: diagnostic
- platform: template
name: "Buzzer"
id: buzzer
on_press:
- switch.turn_on: buzzer_switch
- delay: 50ms
- switch.turn_off: buzzer_switch
text_sensor:
- platform: uptime
name: "Uptime"
entity_category: diagnostic
- platform: version
name: "ESPHome Version"
hide_timestamp: true
- platform: wifi_info
ip_address:
id: wifi_ip
name: "IP Address"
entity_category: diagnostic
output:
- platform: gpio
id: light_relay
pin: GPIO9
inverted: true
switch:
- platform: gpio
internal: True
id: buzzer_switch
name: "Buzzer"
pin:
number: GPIO10
inverted: true
- platform: gpio
internal: true
pin: GPIO14
id: fan_relay1
restore_mode: ALWAYS_OFF
- platform: gpio
internal: true
pin: GPIO12
id: fan_relay2
restore_mode: ALWAYS_OFF
- platform: gpio
internal: true
pin: GPIO15
id: fan_relay3
restore_mode: ALWAYS_OFF
light:
- platform: binary
name: "$friendly_name"
output: light_relay
id: ifan03_light
restore_mode: $on_boot_light
on_state:
then:
- script.execute: beep_feedback
script:
- id: fan_set_speed
mode: restart
then:
# 1. Turn off if it was on
- if:
condition:
lambda: 'return id(last_speed) != 0;'
then:
- switch.turn_off: fan_relay1
- switch.turn_off: fan_relay2
- switch.turn_off: fan_relay3
- delay: 500ms
# 2. BOOST only from OFF to LOW or MED
- if:
condition:
lambda: |-
return id(last_speed) == 0 &&
(id(target_speed) == 1 || id(target_speed) == 2);
then:
- switch.turn_on: fan_relay3
- delay: 4s
- switch.turn_off: fan_relay3
- delay: 500ms
# 3. Set final velocity
- if:
condition:
lambda: 'return id(target_speed) == 1;'
then:
- switch.turn_on: fan_relay1
- if:
condition:
lambda: 'return id(target_speed) == 2;'
then:
- switch.turn_on: fan_relay1
- switch.turn_on: fan_relay2
- if:
condition:
lambda: 'return id(target_speed) == 3;'
then:
- switch.turn_on: fan_relay3
# 4. Store real state
- lambda: |-
id(last_speed) = id(target_speed);
- id: beep_feedback
mode: restart
then:
- if:
condition:
lambda: 'return ${buzzer_enabled};'
then:
- button.press: buzzer
- id: rf_gate
mode: single
parameters:
action: int
then:
- lambda: |-
uint32_t now = millis();
if (now - id(last_rf_ms) < 300) {
return;
}
id(last_rf_ms) = now;
switch (action) {
case 0: {
auto call = id(ifan03_fan).turn_off();
call.perform();
break;
}
case 1: {
auto call = id(ifan03_fan).turn_on();
call.set_speed(1);
call.perform();
break;
}
case 2: {
auto call = id(ifan03_fan).turn_on();
call.set_speed(2);
call.perform();
break;
}
case 3: {
auto call = id(ifan03_fan).turn_on();
call.set_speed(3);
call.perform();
break;
}
case 4: {
auto call = id(ifan03_light).toggle();
call.perform();
break;
}
case 5: {
id(buzzer).press();
break;
}
}
fan:
- platform: template
id: ifan03_fan
name: "$friendly_name"
speed_count: 3
restore_mode: NO_RESTORE # important
on_turn_on:
- lambda: |-
// If for any reason the speed is 0, we force 1 (Low).
if (id(ifan03_fan).speed == 0) {
id(target_speed) = 1;
} else {
// If you already have speed, we use it.
id(target_speed) = id(ifan03_fan).speed;
}
- script.execute: fan_set_speed
- script.execute: beep_feedback
on_turn_off:
- lambda: |-
id(target_speed) = 0;
- script.execute: fan_set_speed
- script.execute: beep_feedback
on_speed_set:
- lambda: |-
id(target_speed) = id(ifan03_fan).speed;
- script.execute: fan_set_speed
- script.execute: beep_feedback
remote_receiver:
pin: GPIO3
binary_sensor:
# remote button row 3 button 1
- platform: remote_receiver
name: "Fan Off"
id: remote_0
raw:
code: [-207, 104, -103, 104, -104, 103, -104, 207, -104, 103, -104, 104,
-103, 104, -104, 103, -104, 105, -102, 104, -725, 104, -311, 103,
-518, 104, -933, 103, -104, 104, -725, 104, -932, 104, -207, 207, -519]
on_release:
- script.execute:
id: rf_gate
action: 0
internal: true
# remote button row 3 button 2
- platform: remote_receiver
name: "Fan Low"
id: remote_1
raw:
code: [-207, 104, -104, 103, -104, 104, -103, 207, -104, 104, -103, 104,
-104, 103, -104, 104, -103, 104, -104, 103, -726, 103, -312, 103,
-518, 104, -933, 103, -104, 104, -725, 104, -103, 104, -726, 103,
-104, 311, -518]
on_release:
- script.execute:
id: rf_gate
action: 1
internal: true
# remote button row 2 button 2
- platform: remote_receiver
name: "Fan Medium"
id: remote_2
raw:
code: [-208, 103, -104, 104, -103, 104, -103, 208, -103, 104, -104, 103,
-104, 104, -103, 104, -104, 103, -104, 103, -726, 104, -310, 104,
-518, 104, -933, 103, -104, 104, -725, 104, -207, 104, -622, 103,
-416, 102, -415]
on_release:
- script.execute:
id: rf_gate
action: 2
internal: true
# remote button row 2 button 1
- platform: remote_receiver
name: "Fan High"
id: remote_3
raw:
code: [-207, 104, -104, 103, -104, 104, -103, 208, -103, 104, -104, 103,
-104, 104, -103, 104, -104, 103, -104, 103, -726, 104, -311, 104,
-518, 103, -934, 103, -103, 104, -726, 103, -104, 207, -622, 104,
-103, 104, -207, 104, -415]
on_release:
- script.execute:
id: rf_gate
action: 3
internal: true
# remote button row 1 button 1
- platform: remote_receiver
name: "Fan Light"
id: remote_light
raw:
code: [-207, 104, -103, 104, -104, 103, -104, 207, -104, 103, -104, 104,
-103, 104, -103, 104, -104, 103, -104, 104, -725, 104, -311, 103,
-518, 104, -933, 103, -104, 103, -726, 103, -311, 104, -518, 104,
-207, 104, -103, 104, -414]
on_release:
- script.execute:
id: rf_gate
action: 4
internal: true
# remote button row 1 button 2
- platform: remote_receiver
name: "Spare Button"
id: remote_spare_button
filters:
- delayed_off: 200ms
raw:
code: [-207, 104, -103, 104, -104, 103, -104, 207, -104, 103, -104, 103,
-104, 104, -103, 104, -103, 104, -104, 107, -721, 105, -206, 207,
-518, 105, -931, 104, -104, 103, -725, 104, -104, 103, -725, 104,
-104, 103, -207, 104, -414]
on_release:
- script.execute:
id: rf_gate
action: 5
Credits
I want to thank everyone who worked on this device before. This configuration builds upon the ideas and testing done by the community in previous threads. I took those foundations as inspiration and refined the logic to create this stable version.