Sonoff Ifan04 - ESPHome working code

Note the change in ESPHome - we can no longer use slashes “/” in names.

WARNING 'RC button RF/WiFi' contains '/' which is reserved as a URL path separator. Automatically replacing with 'RC button RF⁄WiFi' (Unicode FRACTION SLASH). Please update your configuration. This will become an error in ESPHome 2026.7.0.

I changed this line:

name: "RC button RF/WiFi"

to:

name: "RC button RF+WiFi"

The 2026.1 ESPHome lists breaking changes for fan lambdas, but no broken lambdas are present - you can safely upgrade.

Here’s a currently working device file; I keep everything in one file for simplicity:

substitutions:
  devicename: fan-bedroom-s
  friendly_devicename: Fan Bedroom (S)
  ota_password: !secret ota_password_fan-bedroom-s
  api_key: !secret api_key_fan-bedroom-s
  
  wifi_ssid: !secret wifi_ssid
  wifi_password: !secret wifi_password
  wifi_fallback_password: !secret wifi_fallback_password

  ###################################
  # Don't edit anything beyond here #
  ###################################
  fan_startup_boost_time: 5s

  # Morse codes for the different buzzer sounds
  code_buzzer_on:  "E"    # .
  code_buzzer_off: "I"    # ..
  code_fan_off:    "T"    # -
  code_fan_1:      "N"    # -.
  code_fan_2:      "D"    # -..
  code_fan_3:      "B"    # -...
  code_rc_unpair:  "O"    # ---
  code_short:      "E"    # .
  code_long:       "T"    # -

external_components:
  - source:
      type: git
      url: https://github.com/rh1rich/esphome-ifan-remote
  - source:
      type: git
      url: https://github.com/rh1rich/esphome-morse-code

esphome:
  project:
    name: "rh1rich.iFan04"
    version: "1.0.0"
  name: $devicename
  friendly_name: $friendly_devicename
  on_boot:
    priority: 600
    then:
      - output.turn_off: buzzer

esp8266:
  board: esp8285
  framework:
    version: latest
  early_pin_init: false
  restore_from_flash: true

preferences:
  flash_write_interval: 5min

# Enable logging
# But disable it via UART0, because UART0 is used by the RF receiver
logger:
  baud_rate: 0
#  level: VERBOSE

# Enable Home Assistant API
api:
  encryption:
    key: "$api_key"
  # Setting reboot timeout to 0 seconds to disable the automatic reboot, if no api connection (Home Assistant server).
  # This is essential for offline usage.
  reboot_timeout: 0s
  actions:
    - action: play_morse_code
      variables:
        text_string: string
      then:
        - morse_code.start:
            text: !lambda 'return text_string;'

ota:
  - platform: esphome
    password: "$ota_password"

wifi:
  ssid: "$wifi_ssid"
  password: "$wifi_password"
  fast_connect: yes

  # Set minimum authentication mode
  min_auth_mode: wpa2

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "$devicename"
    password: "$wifi_fallback_password"

captive_portal:

web_server:
  port: 80
  version: 3

# GPIO00 ... Button
# GPIO01 ... UART0 TX
# GPIO03 ... UART0 RX (RF Remote Control)
# GPIO04 ... I2C SDA (TP11 D_RX on PCB backside)
# GPIO05 ... I2C SCL (TP10 D_TX on PCB backside)
# GPIO09 ... Light Relay (D)
# GPIO10 ... Buzzer
# GPIO12 ... Fan Relay 2 (D11, 3uF cap on IFAN04-H)
# GPIO13 ... LED
# GPIO14 ... Fan Relay 1 (D5, 2.5uF cap on IFAN04-H)
# GPIO15 ... Fan Relay 3 (D13, no cap)

status_led:
  pin:
    number: GPIO13
    inverted: true

# UART0 (GPIO03) is used by the RF-receiver to send the received RF-commands
# to the ESP.
uart:
  rx_pin: GPIO03
  baud_rate: 9600
  id: uart_bus

# I2C via the test points TP10 (= GPIO05) and TP11 (= GPIO04) on the PCB.
# i2c:
#   sda: GPIO04
#   scl: GPIO05
#   frequency: 200kHz

output:
  - platform: gpio
    id: light_relay
    pin:
      number: GPIO09
      inverted: true
  - platform: gpio
    id: buzzer
    pin:
      number: GPIO10
      inverted: true
  - platform: gpio
    id: fan_relay_1
    pin: GPIO14
  - platform: gpio
    id: fan_relay_2
    pin: GPIO12
  - platform: gpio
    id: fan_relay_3
    pin: GPIO15
  - platform: template
    id: fan_relays
    type: float
    write_action:
      - if:
          condition:
            lambda: return (state < 0.3);
          then:
            - if:
                condition:
                  lambda: return id(buzzer_enabled).state;
                then:
                  - morse_code.start: $code_fan_off
            - script.execute:
                id: fan_speed_set
                speed: 0
      - if:
          condition:
            lambda: return ((state >= 0.3) && (state < 0.6));
          then:
            - if:
                condition:
                  lambda: return id(buzzer_enabled).state;
                then:
                  - morse_code.start: $code_fan_1
            - script.execute:
                id: fan_speed_set
                speed: 1
      - if:
          condition:
            lambda: return ((state >= 0.6) && (state < 0.9));
          then:
            - if:
                condition:
                  lambda: return id(buzzer_enabled).state;
                then:
                  - morse_code.start: $code_fan_2
            - script.execute:
                id: fan_speed_set
                speed: 2
      - if:
          condition:
            lambda: return (state >= 0.9);
          then:
            - if:
                condition:
                  lambda: return id(buzzer_enabled).state;
                then:
                  - morse_code.start: $code_fan_3
            - script.execute:
                id: fan_speed_set
                speed: 3

morse_code:
  output: buzzer
  dit_duration: 50

light:
  - platform: binary
    name: 'Light'
    id: light_comp
    output: light_relay

fan:
  - platform: speed
    name: 'Fan'
    id: fan_comp
    output: fan_relays
    speed_count: 3

switch:
  - platform: template
    name: 'Buzzer enabled'
    id: buzzer_enabled
    entity_category: config
    optimistic: True
    restore_mode: RESTORE_DEFAULT_ON
    on_turn_on:
      - morse_code.start: $code_buzzer_on
    on_turn_off:
      - morse_code.start: $code_buzzer_off
  - platform: template
    name: 'Light enabled'
    id: light_enabled
    entity_category: config
    optimistic: True
    restore_mode: RESTORE_DEFAULT_ON
  - platform: template
    name: 'Fan startup-boost enabled'
    id: fan_boost_enabled
    entity_category: config
    optimistic: True
    restore_mode: RESTORE_DEFAULT_OFF
    
button:
  - platform: restart
    name: Restart
    entity_category: diagnostic

sensor:
  - platform: uptime
    name: 'Uptime'
    entity_category: diagnostic
    unit_of_measurement: s
  - platform: wifi_signal
    id: wifi_signal_db
    name: 'WiFi Signal dB'
    entity_category: diagnostic
# Sensor for the WiFi signal strength in percent (%)
  - platform: copy
    id: wifi_signal_rel
    source_id: wifi_signal_db
    name: "WiFi Signal"
    entity_category: diagnostic
    filters:
      - lambda: |-
          return min(max(2 * (x + 100.0), 0.0), 100.0);
    unit_of_measurement: "%"
    device_class: 'signal_strength'
# Expample for using a SHT3x sensor which is connected via the test pins TP10 (I2C SCL) and TP11 (I2C SDA) on the PCB.
# Also enable i2c component!
  # - platform: sht3xd
  #   temperature:
  #     name: 'Temperature'
  #   humidity:
  #     name: 'Humidity'

binary_sensor:
  - platform: gpio
    name: "Button"
    pin:
      number: GPIO00
      inverted: true
  - platform: template
    name: "RC button light"
    id: rc_button_light
    filters:
      - delayed_off: 300ms
  - platform: template
    name: "RC button RF+WiFi"
    id: rc_button_rfwifi
    filters:
      - delayed_off: 300ms
  - platform: template
    name: "RC button WiFi (long)"
    id: rc_button_wifi_long
    filters:
      - delayed_off: 300ms
  - platform: template
    name: "RC button RF (long)"
    id: rc_button_rf_long
    filters:
      - delayed_off: 300ms

# Remote commands via UART0
#  RX: GPIO03
#
# The numbering of the following remote control buttons
# starts with the top left button (1) and ends with the
# bottom right button (8).
#
# 1 (Light)           = (AA55) 0104 000104 (0A)
# 2 (Mute)            = (AA55) 0106 000101 (09)
# 3 (Fan high)        = (AA55) 0104 000103 (09)
# 4 (Fan mid)         = (AA55) 0104 000102 (08)
# 5 (Fan off)         = (AA55) 0104 000100 (06)
# 6 (Fan low)         = (AA55) 0104 000101 (07)
# 7 (RF Clearing)     = (AA55) 0101 000102 (05)
# 7 (RF Clearing 5s)  = (AA55) 0107 000101 (0A) clears the connection between remote control and receiver!!
# 8 (WiFi Pairing)    = (AA55) 0101 000102 (05)
# 8 (WiFi Pairing 5s) = (AA55) 0101 000101 (04)

ifan_remote:
  on_command:
    then:
      # - logger.log:
      #     format: "RC command '%04X:%06X' received."
      #     args: [ 'command.high','command.low' ]
      - if:
          condition:
            lambda: return (command.high == 0x0104 && command.low == 0x000100);
          then:
            - fan.turn_off: fan_comp
      - if:
          condition:
            lambda: return (command.high == 0x0104 && command.low == 0x000101);
          then:
            - fan.turn_on:
                id: fan_comp
                speed: 1
      - if:
          condition:
            lambda: return (command.high == 0x0104 && command.low == 0x000102);
          then:
            - fan.turn_on:
                id: fan_comp
                speed: 2
      - if:
          condition:
            lambda: return (command.high == 0x0104 && command.low == 0x000103);
          then:
            - fan.turn_on:
                id: fan_comp
                speed: 3
      - if:
          condition:
            lambda: return (command.high == 0x0104 && command.low == 0x000104);
          then:
            - if:
                condition:
                  lambda: return id(light_enabled).state;
                then:
                  - light.toggle: light_comp
                else:
                  - if:
                      condition:
                        lambda: return id(buzzer_enabled).state;
                      then:
                        - morse_code.start: $code_short
                  - binary_sensor.template.publish:
                      id: rc_button_light
                      state: ON
                  - binary_sensor.template.publish:
                      id: rc_button_light
                      state: OFF
      - if:
          condition:
            lambda: return (command.high == 0x0106 && command.low == 0x000101);
          then:
            - switch.toggle: buzzer_enabled
      - if:
          condition:
            lambda: return (command.high == 0x0101 && command.low == 0x000102);
          then:
            - if:
                condition:
                  lambda: return id(buzzer_enabled).state;
                then:
                  - morse_code.start: $code_short
            - binary_sensor.template.publish:
                id: rc_button_rfwifi
                state: ON
            - binary_sensor.template.publish:
                id: rc_button_rfwifi
                state: OFF
      - if:
          condition:
            lambda: return (command.high == 0x0101 && command.low == 0x000101);
          then:
            - if:
                condition:
                  lambda: return id(buzzer_enabled).state;
                then:
                  - morse_code.start: $code_long
            - binary_sensor.template.publish:
                id: rc_button_wifi_long
                state: ON
            - binary_sensor.template.publish:
                id: rc_button_wifi_long
                state: OFF
      - if:
          condition:
            lambda: return (command.high == 0x0107 && command.low == 0x000101);
          then:
            - morse_code.start: $code_rc_unpair
            - binary_sensor.template.publish:
                id: rc_button_rf_long
                state: ON
            - binary_sensor.template.publish:
                id: rc_button_rf_long
                state: OFF

text_sensor:
  - platform: version
    name: 'ESPHome Version'
    entity_category: diagnostic
  - platform: wifi_info
    ip_address:
      name: WiFi IP Address
      entity_category: diagnostic
    ssid:
      name: WiFi SSID
      entity_category: diagnostic
    mac_address:
      name: WiFi MAC Address
      entity_category: diagnostic
    dns_address:
      name: WiFi DNS Address
      entity_category: diagnostic

globals:
  - id: last_fan_speed
    type: int
    restore_value: no
    initial_value: "0"

script:
  - id: fan_speed_set
    mode: restart
    parameters:
      speed: int
    then:
      - if:
          condition:
            lambda: return (speed == 0);
          then:
            - output.turn_off: fan_relay_1
            - output.turn_off: fan_relay_2
            - output.turn_off: fan_relay_3
          else:
            - if:
                condition:
                  lambda: return (id(fan_boost_enabled).state && (id(last_fan_speed) == 0));
                then:
                  - output.turn_off: fan_relay_1
                  - output.turn_off: fan_relay_2
                  - output.turn_on: fan_relay_3
                  - delay: $fan_startup_boost_time
            - if:    # enable cap 1
                condition:
                  lambda: return (speed == 1);
                then:
                  - output.turn_on: fan_relay_1
                  - output.turn_off: fan_relay_2
                  - output.turn_off: fan_relay_3
            - if:    # enable cap 1 & 2
                condition:
                  lambda: return (speed == 2);
                then:
                  - output.turn_on: fan_relay_1
                  - output.turn_on: fan_relay_2
                  - output.turn_off: fan_relay_3
            - if:    # no caps
                condition:
                  lambda: return (speed == 3);
                then:
                  - output.turn_off: fan_relay_1
                  - output.turn_off: fan_relay_2
                  - output.turn_on: fan_relay_3
      - lambda: |-
          id(last_fan_speed) = speed;