Sonoff Ifan04 - ESPHome working code

@devjklein: Here is a modified version that when the fan is off, will start the fan on high for 5 seconds then switch to the target speed. I also modified the relay sequence to speed 1 is relay 1 only, speed 2 is relay 2 only and speed 3 is relay 3 only.

yaml

substitutions:
  name:  living-room-fan
  device_description: Sonoff iFan04-L
  friendly_name: Living Room Fan/Light

globals:
  - id: target_fan_speed
    type: int
  - id: start_time_offset
    type: int

esphome:
  name: ${name}
  comment: ${device_description}
  platform: ESP8266
  board: esp01_1m
  on_boot:
    priority: 225
    # turn off the light as early as possible
    then:
      - light.turn_off: fan_light
  on_loop:
    lambda: |-
      if (id(start_time_offset) && (millis() > id(start_time_offset))) {
        ESP_LOGD("IFAN04", "Delayed wakeup to select final target speed: %d", id(target_fan_speed));
        auto call = id(the_fan).turn_on();
        call.set_speed(id(target_fan_speed));
        call.perform();
        id(start_time_offset) = 0;
      }

# Disable logging on serial as it is used by the remote
logger:
  baud_rate: 0

# Enable Home Assistant API
api:

ota:
  password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: on

web_server:
  port: 80
  auth:
    username: !secret web_server_username
    password: !secret web_server_password
  
external_components:
  - source: github://ssieb/custom_components
    components: [ ifan04 ]

time:
  - platform: homeassistant
    id: time_homeassistant
    on_time_sync:
      - component.update: sensor_uptime_timestamp

uart:
  tx_pin: GPIO01
  rx_pin: GPIO03
  baud_rate: 9600
#  debug:
#    sequence:
#      - lambda: UARTDebug::log_hex(direction, bytes, ',');

ifan04:
  on_fan:
    - lambda: |-
        if (speed) {
          if (!id(target_fan_speed)) {
            ESP_LOGD("IFAN04", "Fan currently off, starting on max reducing to %d in 5 seconds", speed);
            id(target_fan_speed) = speed;
            speed = 3;
            id(start_time_offset) = millis() + 5000;
          } else {
            ESP_LOGD("IFAN04", "Fan already running, current speed (%d) new speed: (%d)", id(target_fan_speed), speed);
            id(start_time_offset) = 0;
            id(target_fan_speed) = speed;
          }
          auto call = id(the_fan).turn_on();
          call.set_speed(speed);
          call.perform();
          if (id(buzzer_dummy).state) {
            switch(id(target_fan_speed)) {
              case 3:
                id(buzzer_pin).turn_on();
                delay(50);
                id(buzzer_pin).turn_off();
                delay(50);
              case 2:
                id(buzzer_pin).turn_on();
                delay(50);
                id(buzzer_pin).turn_off();
                delay(50);
              case 1:
                id(buzzer_pin).turn_on();
                delay(50);
                id(buzzer_pin).turn_off();
            }
          }
        } else {
          id(target_fan_speed) = 0;
          id(start_time_offset) = 0;
          auto call = id(the_fan).turn_off();
          call.perform();
        }
  on_light:
    - light.toggle: fan_light
  on_buzzer:
    - switch.toggle: buzzer_dummy

sensor:
  - platform: uptime
    id: sensor_uptime

  - platform: template
    id: sensor_uptime_timestamp
    name: "${friendly_name} Uptime"
    device_class: "timestamp"
    accuracy_decimals: 0
    update_interval: never
    lambda: |-
      static float timestamp = (
        id(time_homeassistant).utcnow().timestamp - id(sensor_uptime).state
      );
      return timestamp;

  - platform: wifi_signal
    name: ${friendly_name} Signal
    update_interval: 60s

binary_sensor:
  - platform: gpio
    id: button
    pin: GPIO0
    on_press:
      then:
        - light.toggle: fan_light

interval:
  - interval: 500ms
    then:
      - if:
          condition:
            not:
              wifi.connected:
          then:
            - light.turn_on:
                id: led1
                brightness: 100%
                transition_length: 0s
            - delay: 250ms
            - light.turn_off:
                id: led1
                transition_length: 250ms

output:
  - platform: template
    id: fanoutput
    type: float
    write_action:
      - if:
          condition:
            lambda: return (state < 0.3);
          then:
            # OFF
            - switch.turn_off: fan_relay1
            - switch.turn_off: fan_relay2
            - switch.turn_off: fan_relay3
          else:
            if:
              condition:
                lambda: return (state < 0.6);
              then:
                # low speed
                - switch.turn_on: fan_relay1
                - switch.turn_off: fan_relay2
                - switch.turn_off: fan_relay3
              else:
                if:
                  condition:
                    lambda: return (state < 0.9);
                  then:
                    # medium speed
                    - switch.turn_off: fan_relay1
                    - switch.turn_on: fan_relay2
                    - switch.turn_off: fan_relay3

                  else:
                    # high speed
                    - switch.turn_off: fan_relay1
                    - switch.turn_off: fan_relay2
                    - switch.turn_on: fan_relay3

  - platform: gpio
    id: light_relay
    pin: GPIO9
    inverted: true

  - platform: gpio
    id: buzzer_pin
    pin: GPIO10
    inverted: true

  - platform: esp8266_pwm
    id: led_pin
    pin: GPIO13
    inverted: true

light:
  - platform: binary
    id: fan_light
    name: "${friendly_name} Light"
    output: light_relay

  - platform: monochromatic
    id: led1
    output: led_pin
    default_transition_length: 0s
    restore_mode: always off

switch:
  - platform: restart
    name: "${friendly_name} Restart"
    
  - platform: template
    id: buzzer_dummy
    name: "Buzzer"
    optimistic: True

  - platform: gpio
    id: fan_relay1
    pin: GPIO14

  - platform: gpio
    id: fan_relay2
    pin: GPIO12

  - platform: gpio
    id: fan_relay3
    pin: GPIO15

fan:
  - platform: speed
    id: the_fan
    name: "${friendly_name} Fan"
    output: fanoutput
    speed_count: 3
2 Likes

wow thanks for this!

I got mine all installed, it works, greatā€¦ I just now realized the light is on/off and not a dimmer - crud.

fwiw - iā€™m up and running now too. I had a difficult time getting the device to burn properly. After initially not even being able to connect, my second issue was esphome would write the firmware, but itā€™d never start and connect to my wifi.

I found that starting with tasmotta, i was able to write the firmware and get on the wifi immidiatelyā€¦ tasmotta has an extra option that ā€œerasesā€ the device during firmware write. This seems to have resolved by issues.

I had a small secondary issue with not enough space to upload my esphome firmeware to the tasmotta burned deviceā€¦ until i backed down tasmotta to the ā€œliteā€ / ā€œminimalā€ version firstā€¦ then i could swap over to my esp image no problem.

Just got around to uploading your code to my fan. It works great! I modified the start delay from 5 to 3 seconds, so it wouldnā€™t reach such a high speed on startup (fan is directly over seating, so max speed can be overwhelming :wink:)

I guess your fan starts spinning a lot faster than mine :slightly_smiling_face:. Glad I could help, you provided the motivation to do something to improve the longevity of the iFan04-L device and the fan in this type of installation.

I did just notice an issue, if I start the fan from a stop with the remote, it operates as expected and toggles all relays on for a few seconds. However, if I set the fan speed in homeassistant, it appears the fan module bypasses the speed logic and goes directly to toggling on a single relay. Any thoughts on this?

The logic that is currently in the on_fan lambda needs to move to the output template. The on_fan handler is only used by the remote. I will think about how that needs to be reworked over the next few days.

@devjklein: Reworked so that the use of Home-Assistant utilizes the boost at startup as well when using the remote. I think there is a bug in the HA front-end, in that if you slide the fan speed to where you want and release it sends one set speed command and works perfectly, but if you tap the speed bar at a new speed level when the unit is off it will send the speed command twice as it powers the device on defeating the speed boost at startup.

yaml

substitutions:
  name:  living-room-fan
  device_description: Sonoff iFan04-L
  friendly_name: Living Room Fan/Light

globals:
  - id: target_fan_speed
    type: int
  - id: start_time_offset
    type: int

esphome:
  name: ${name}
  comment: ${device_description}
  platform: ESP8266
  board: esp01_1m
  on_boot:
    priority: 225
    # turn off the light as early as possible
    then:
      - light.turn_off: fan_light
  on_loop:
    lambda: |-
      if (id(start_time_offset) && (millis() > id(start_time_offset))) {
        ESP_LOGD("IFAN04", "Setting target speed: %d", id(target_fan_speed));
        auto call = id(the_fan).turn_on();
        call.set_speed(id(target_fan_speed));
        call.perform();
        id(start_time_offset) = 0;
      }

# Disable logging on serial as it is used by the remote
logger:
  baud_rate: 0

# Enable Home Assistant API
api:

ota:
  password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: on

web_server:
  port: 80
  auth:
    username: !secret web_server_username
    password: !secret web_server_password
  
external_components:
  - source: github://ssieb/custom_components
    components: [ ifan04 ]

time:
  - platform: homeassistant
    id: time_homeassistant
    on_time_sync:
      - component.update: sensor_uptime_timestamp

uart:
  tx_pin: GPIO01
  rx_pin: GPIO03
  baud_rate: 9600
#  debug:
#    sequence:
#      - lambda: UARTDebug::log_hex(direction, bytes, ',');

ifan04:
  on_fan:
    - lambda: |-
        if (speed) {
          auto call = id(the_fan).turn_on();
          call.set_speed(speed);
          call.perform();
          if (id(buzzer_dummy).state) {
            switch(id(target_fan_speed)) {
              case 3:
                id(buzzer_pin).turn_on();
                delay(50);
                id(buzzer_pin).turn_off();
                delay(50);
              case 2:
                id(buzzer_pin).turn_on();
                delay(50);
                id(buzzer_pin).turn_off();
                delay(50);
              case 1:
                id(buzzer_pin).turn_on();
                delay(50);
                id(buzzer_pin).turn_off();
            }
          }
        } else {
          id(target_fan_speed) = 0;
          id(start_time_offset) = 0;
          auto call = id(the_fan).turn_off();
          call.perform();
        }
  on_light:
    - light.toggle: fan_light
  on_buzzer:
    - switch.toggle: buzzer_dummy

sensor:
  - platform: uptime
    id: sensor_uptime

  - platform: template
    id: sensor_uptime_timestamp
    name: "${friendly_name} Uptime"
    device_class: "timestamp"
    accuracy_decimals: 0
    update_interval: never
    lambda: |-
      static float timestamp = (
        id(time_homeassistant).utcnow().timestamp - id(sensor_uptime).state
      );
      return timestamp;

  - platform: wifi_signal
    name: ${friendly_name} Signal
    update_interval: 60s

binary_sensor:
  - platform: gpio
    id: button
    pin: GPIO0
    on_press:
      then:
        - light.toggle: fan_light

interval:
  - interval: 500ms
    then:
      - if:
          condition:
            not:
              wifi.connected:
          then:
            - light.turn_on:
                id: led1
                brightness: 100%
                transition_length: 0s
            - delay: 250ms
            - light.turn_off:
                id: led1
                transition_length: 250ms

output:
  - platform: template
    id: fanoutput
    type: float
    write_action:
      - lambda: |-
          if (state) {
            int speed = int(state / 0.33);
            if (!id(target_fan_speed) && (speed < 3)) {
              ESP_LOGD("IFAN04", "Fan currently off, boosting speed");
              id(target_fan_speed) = speed;
              state = 1.0;
              id(start_time_offset) = millis() + 5000; // 5 second delay
            } else {
              id(start_time_offset) = 0;
              id(target_fan_speed) = speed;
            }
          }
          if (state < 0.3) {
            // OFF
            id(target_fan_speed) = 0;
            id(start_time_offset) = 0;
            id(fan_relay1).turn_off();
            id(fan_relay2).turn_off();
            id(fan_relay3).turn_off();
          } else if (state < 0.6) {
            // low speed
            id(fan_relay1).turn_on();
            id(fan_relay2).turn_off();
            id(fan_relay3).turn_off();
          } else if (state < 0.9) {
            // medium speed
            id(fan_relay1).turn_off();
            id(fan_relay2).turn_on();
            id(fan_relay3).turn_off();
          } else {
            // high speed
            id(fan_relay1).turn_off();
            id(fan_relay2).turn_off();
            id(fan_relay3).turn_on();
          }

  - platform: gpio
    id: light_relay
    pin: GPIO9
    inverted: true

  - platform: gpio
    id: buzzer_pin
    pin: GPIO10
    inverted: true

  - platform: esp8266_pwm
    id: led_pin
    pin: GPIO13
    inverted: true

light:
  - platform: binary
    id: fan_light
    name: "${friendly_name} Light"
    output: light_relay

  - platform: monochromatic
    id: led1
    output: led_pin
    default_transition_length: 0s
    restore_mode: always off

switch:
  - platform: restart
    name: "${friendly_name} Restart"
    
  - platform: template
    id: buzzer_dummy
    name: "Buzzer"
    optimistic: True

  - platform: gpio
    id: fan_relay1
    pin: GPIO14

  - platform: gpio
    id: fan_relay2
    pin: GPIO12

  - platform: gpio
    id: fan_relay3
    pin: GPIO15

fan:
  - platform: speed
    id: the_fan
    name: "${friendly_name} Fan"
    output: fanoutput
    speed_count: 3
3 Likes

@NigelHA Thank you for your work on this. I installed four ifan04 devices yesterday and they are working great in HA. My fans old remote control modules had a feature that I would like to check if it is possible to implement on the ifan04. When power to the fans is removed by using a wall switch or because of a power outage, the fans would remember and go back to their previous state when the power is restored. For example if a fan was running on medium when it lost power, it will start back at medium speed when the power is restored. Can that be done with the ifan04 and ESPHome?

I have been working on an external component for the ifan04, that puts all of the c++ code where it belongs rather than in lambdas.

If anyone is interested

external_components:
  - source: github://cpyarger/custom_components

if you want to check the code, it is in components/ifan
The next steps are to integrate ssiebā€™s ifan04, so that itā€™s all handled within the component code.

1 Like

I canā€™t wait to try your code. Everything is working almost perfectly now, but I just noticed that my fan low and medium speeds are the same, so only low and high are really working.

Ill push my latest updates.

if you set your external components as such;

external_components:
  - source: github://cpyarger/custom_components@master
    refresh: 0s

Then whenever I push an update, you should get the latest on a fresh install.

I think the default refresh is 1 day

If you have any issues, or suggestions, please let me know

1 Like

Saving state is controlled through the use of restore_from_flash in the esphome section. You should also read about flash_write_interval.

I intentionally used lambdas to keep all the code together in the yaml file for easy manipulation, especially as the fan speed part of the configuration has been a point of contention for many on both the ifan03 and 04. But if you are going to submit this has an official component with speed issues addressed that would be great.

Thanks, I will look that up. I found that my fans require relays 1 and 2 on for medium speed to work.

Yeah the plan is eventually to allow for you to choose between the various models.

Here is my working yaml code for my ifan04, including my custom component.

substitutions:
  name: dining-room
  friendly_name: Dining Room
external_components:
  - source: github://cpyarger/custom_components@master
    refresh: 0s
esphome:
  name: ${name}
  comment: Sonoff iFan04-L
  # This will allow for (future) project identification,
  # configuration and updates.
  project:
    name: cpyarger.sonoff-ifan04-l
    version: "1.0"
esp8266:
  board: esp01_1m
# Disable logging on serial as it is used by the remote
logger:
  baud_rate: 0
# Enable Home Assistant API
api:
  services:
    - service: fan_cycle
      then:
        - fan.cycle_speed: the_fan
ota:
uart:
  tx_pin: GPIO01
  rx_pin: GPIO03
  baud_rate: 9600
dashboard_import:
  package_import_url: github://cpyarger/esphome-templates/sonoff-ifan04-l.yaml@main


captive_portal:

binary_sensor:
  - platform: gpio
    id: button_light
    pin: GPIO0
    on_press:
      then:
        - light.toggle: fan_light
output:
  - platform: esp8266_pwm
    id: led_pin
    pin: GPIO13
    inverted: true
light:
  - platform: ifan
    id: fan_light
    name: "${friendly_name} Light"
  - platform: monochromatic
    id: led1
    output: led_pin
    default_transition_length: 0s
    restore_mode: always off
button:
  - platform: template
    name: ${friendly_name} Cycle Fan
    on_press:
      then:
        - fan.cycle_speed: the_fan
fan:
  - platform: ifan
    id: the_fan
    name: "${friendly_name} Fan"
    remote_enable: false  
ifan04:
  on_fan:
    - lambda: |-
          auto call = speed ? id (the_fan).turn_on() :  id (the_fan).turn_off();
          call.set_speed(speed);
          call.perform();
  on_light:
    - light.toggle: fan_light

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  #use_address: sonoff-ifan04-l-77cdd8.local
web_server:
2 Likes

@cpyarger
I have installed your code on my ifan04-H wich is working great so far!
I have 3 questions

  1. What cycle fan does , I didnā€™t find something change on my fan ?
  2. Can we disable the buzzer?
  3. On your github repo you have 2 fan components wich one should i use?

Thank you

I have one fan component, the ifan component, the ifan04 component, as stated in the readme of the repo, is for the remote control.

if i remember correctly

fan:
  - platform: ifan
    id: the_fan
    name: "${friendly_name} Fan"
    remote_enable: false  
    buzzer_enable: false

should disable the buzzer

Cycle fan, cycles through the fan speeds, I had the number of speeds set to 100, so cycle was broken, but i fixed that a couple days ago

thanks it works perfectly
made a few adjustments to the backup wifi and remote web