IKEA FÖRNUFTIG in Home-Assistant

SIBA 1A SMD fuse 6.1x2.6mm
Wemos-d1-mini
ROT-1A/B or KY-040 (it’s quite cheap and popular, sold with different names)
DSN-1504-3A buck converter
JST-XH 2 and 4 pin connectors (standard 2.5mm)

1 Like

Thx, any recommendations on where to get them?

I got the PCB’s and built my version.
(Many thanks for building and sharing the layout !!!)

I had trouble with the GPIO picking up a reliable signal (don’t have the pins decoupled due to lack of caps).
Also, the state of the fan was not always clear/obvious to me. So I made a variation using the rotary encoder component instead.

here is my yaml

esphome:
  name: "fornuftig"

esp8266:
  board: d1_mini

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

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

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Fornuftig-AP"
    password: ""

captive_portal:

globals:
  - id: fan_speed
    type: int
    restore_value: yes
    initial_value: "0"

  - id: speed_0
    type: int
    restore_value: yes
    initial_value: "0"

  - id: speed_1
    type: int
    restore_value: yes
    initial_value: "33"

  - id: speed_2
    type: int
    restore_value: yes
    initial_value: "66"

  - id: speed_3
    type: int
    restore_value: yes
    initial_value: "100"

switch:
  - platform: restart
    name: "${friendly_name} Restart"

sensor:
  - platform: template
    name: "$friendly_name - Power consumption"
    id: fonuftig_power_consumption
    device_class: power
    state_class: measurement
    unit_of_measurement: "W"
    accuracy_decimals: 1
    update_interval: 30s
  - platform: integration
    name: "$friendly_name - Consumed Energy"
    sensor: fonuftig_power_consumption
    time_unit: "h"
    device_class: energy
    state_class: total_increasing
    unit_of_measurement: "Wh"
    restore: false
    integration_method: left

  - platform: pulse_counter
    pin:
      # Fan - FG
      number: D2
      mode: INPUT_PULLUP
    unit_of_measurement: "RPM"
    accuracy_decimals: 0
    id: fan_tach
    name: Fornuftig Fanspeed
    filters:
      - multiply: 0.1

  - platform: template
    id: set_fan_freq
    filters:
      - multiply: 3
      - lambda: |-
          if (id(fornuftig_fan).state){
            if(x > 300) { // limit max frequency
              return 300;
              
            } else if(x < 36 && x > 3) { // limit low frequency
              return 36;
              
            } else if(x <= 3) {
              return 0;
              
            } else {
              return x;
              
            }
          } else {
            return 0;
          }
    on_value:
      then:
        - output.esp8266_pwm.set_frequency:
            id: pwm_output
            frequency: !lambda "return int(x);"
        - logger.log: "on_value called"
        - lambda: |-
            // send current status to homeassistant
            if(x) {
              auto call = id(fornuftig_fan).turn_on();
              call.set_speed(int(x)/3);
              call.perform();
            } else {
              auto call = id(fornuftig_fan).turn_off();
              call.perform();
            }
        - sensor.template.publish:
            id: fonuftig_power_consumption
            state: !lambda "return int(15*(id(my_rotary_encoder).state)/100);"

  - platform: rotary_encoder
    id: my_rotary_encoder
    name: "Rotary Encoder"
    min_value: 0
    max_value: 100
    publish_initial_value: true
    restore_mode: ALWAYS_ZERO

    pin_a: # clk
      number: D5
      inverted: true
      mode:
        input: true
        pullup: true

    pin_b: # DT
      number: D6
      inverted: true
      mode:
        input: true
        pullup: true
    on_clockwise:
      then:
        - light.turn_on:
            id: status_led
        - logger.log:
            format: "rotary sensor state was: %.1f"
            args: ["id(my_rotary_encoder).state"]
        - lambda: |-
            if (id(my_rotary_encoder).state < id(speed_1)) {
              // set rotary to speed1, turn on fan
              id(my_rotary_encoder).set_value(id(speed_1));
              id(status_led).turn_on();

            } else if (id(my_rotary_encoder).state < id(speed_2) && id(my_rotary_encoder).state > id(speed_0) && id(my_rotary_encoder).state < id(speed_3)) {
              // set rotary to speed2
              id(my_rotary_encoder).set_value(id(speed_2));

            } else if (id(my_rotary_encoder).state < id(speed_3) && id(my_rotary_encoder).state > id(speed_1) && id(my_rotary_encoder).state > id(speed_0)) {
              // set rotary to speed1
              id(my_rotary_encoder).set_value(id(speed_3));

            } else {
              // do nothing;
            }
        # trigger set fan speed
        - fan.turn_on:
            id: fornuftig_fan
            speed: !lambda "return int(id(my_rotary_encoder).state);"

        - logger.log:
            format: "rotary sensor state set to: %.1f"
            args: ["id(my_rotary_encoder).state"]

    on_anticlockwise:
      then:
        - logger.log:
            format: "rotary sensor state was: %.1f"
            args: ["id(my_rotary_encoder).state"]
        - lambda: |-
            if (id(my_rotary_encoder).state > id(speed_2)) {
              // set rotary to speed2 
              id(my_rotary_encoder).set_value(id(speed_2));

            } else if (id(my_rotary_encoder).state > id(speed_1) && id(my_rotary_encoder).state < id(speed_3) && id(my_rotary_encoder).state > id(speed_0)) {
              // set rotary to speed1
              id(my_rotary_encoder).set_value(id(speed_1));

            } else if (id(my_rotary_encoder).state > id(speed_0)  && id(my_rotary_encoder).state < id(speed_1)) {
              // set rotary to speed0; turn fan & led off
              id(my_rotary_encoder).set_value(id(speed_1));

            } else {
              // do nothing;
            }
        # trigger set fan speed
        - fan.turn_on:
            id: fornuftig_fan
            speed: !lambda "return int(id(my_rotary_encoder).state);"

        - logger.log:
            format: "rotary sensor state set to: %.1f"
            args: ["id(my_rotary_encoder).state"]

binary_sensor:
  - platform: gpio
    # SW
    id: "knob_1"
    pin:
      number: D7
      mode: INPUT_PULLUP
      inverted: true
    filters:
      - delayed_on_off: 100ms
    on_press:
      then:
        if:
          condition:
            and:
              - fan.is_off: fornuftig_fan
              - light.is_off: status_led

          then:
            - fan.turn_on:
                id: fornuftig_fan
                speed: !lambda "return id(speed_1);"
            - light.turn_on:
                id: status_led
            - sensor.rotary_encoder.set_value:
                id: my_rotary_encoder
                value: !lambda 'return id(speed_1);'

          else:
            - fan.turn_off:
                id: fornuftig_fan
            - light.turn_off:
                id: status_led
            - logger.log: "SW - pin_c - Turned Off"
            - sensor.rotary_encoder.set_value:
                id: my_rotary_encoder
                value: !lambda "return id(speed_0);"
            - sensor.template.publish:
                id: fonuftig_power_consumption
                state: !lambda "return int(0);"

light:
  - platform: binary
    id: "status_led"
    name: "status LED"
    output: light_output

output:
  - id: light_output
    platform: gpio
    pin: D3

  - platform: esp8266_pwm
    # Fan CLK
    pin: D1
    frequency: 150 Hz
    id: pwm_output
    min_power: 0.5
    max_power: 0.5
    zero_means_zero: true

fan:
  - platform: speed
    output: pwm_output
    name: "Fornuftig Air Purifier"
    id: "fornuftig_fan"
    restore_mode: RESTORE_DEFAULT_OFF
    on_speed_set:
      - lambda: |-
          if(id(fornuftig_fan).speed != id(fan_speed) && id(fornuftig_fan).state){
           id(fan_speed) = id(fornuftig_fan).speed;
           id(set_fan_freq).publish_state(id(fornuftig_fan).speed);
          }

    on_turn_on:
      - lambda: |-
          id(set_fan_freq).publish_state(id(fornuftig_fan).speed);
          id(my_rotary_encoder).publish_state(id(fornuftig_fan).speed);
      - light.turn_on:
          id: status_led

    on_turn_off:
      - output.esp8266_pwm.set_frequency:
          id: pwm_output
          frequency: !lambda "return int(0);"
      - lambda: |-
          id(my_rotary_encoder).publish_state(id(speed_0));
      - light.turn_off:
          id: status_led

      - logger.log: "Fan Turned Off!"

I modified the version from [0xDE4DBEEF], combined the very early version for the power calculations.
I also salvaged the LED from the original board and used it as a status light to show that the fan is actually turned on.

The functionality is as follows:
push the knob:

  • switch fan on, speed lvl (1)
  • switch fan off

set speed:

  • Turn knob clockwise: increase speed lvl (1-2-3)
  • Turn knob counter clockwise: decrease speed lvl (3-2-1)

It’s likely that the script can use some improvements :wink: - still an early adopter of HA.
Let me know if you have any suggestions or improvements.



3 Likes

buck converter:

rotary:

fuse (not sure if these are the right ones):

1 Like

FYI, atm, its missing the sync of the LED and rotary state, if you control the device through HA

that should be taken care of, for now… updated the fan block in the code above.

1 Like

IKEA has a new air purifier called Uppatvind. I created a project for it. You can check the details if you are interested in.

IKEA Uppatvind Air Purifier - Share your Projects! - Home Assistant Community (home-assistant.io)

Thank you for sharing PCB and template, it works really great !

@horvathgergo has released a new v2 board that now comes with a rotary switch. Great work :clap: :clap: :clap: !

The project currently uses micropython to integrate w/ HA. I’m still learning esphome and don’t know much about micropython. Would the esphome yaml file by @mcruiser work with this latest board? It’s unclear in my mind if the v2 board is using the approach that mcruiser used to add the rotary switch.

@horvathgergo Great stuff! Used your Gerber file to order and assemble the board. Can’t wait!

2 Likes

The position of that voltage regulator is a bit weird but i’m pretty sure that the manufacturer will fix it in the review phase.

Very sharp of you! Didn’t even notice before, even after triple checking. And yes, JLCPB corrected it perfectly.

Hi! Did you already check whether you could get this to work with esphome?

Got it to work more easily then I thought. I just changed the GPIO PIN’s from one of the previously shared esphome configuration… @horvathgergo I can turn on and turn off the purifier and select the fan speed from 0 to 100%. Als the knob is working correctly. Could you please check if the below will not negatively affect the used parts of your v2 board?

output:
  - platform: esp8266_pwm
    pin: 5
    frequency: 150 Hz
    id: pwm_output
    min_power: 0.5
    max_power: 0.5
    zero_means_zero: true

fan:
  - platform: speed
    output: pwm_output
    name: "Fornuftig Air Purifier"
    id: "fornuftig_fan"
    restore_mode: RESTORE_DEFAULT_OFF
    on_speed_set:
      lambda: |-
        if(id(fornuftig_fan).speed != id(fan_speed) && id(fornuftig_fan).state){
          id(fan_speed) = id(fornuftig_fan).speed;
          id(set_fan_freq).publish_state(id(fornuftig_fan).speed);
        }
    on_turn_on:
      lambda: |-
       id(set_fan_freq).publish_state(id(fornuftig_fan).speed);
    on_turn_off:
      - output.esp8266_pwm.set_frequency:
          id: pwm_output
          frequency: !lambda 'return int(0);'
      - logger.log: "Fan Turned Off!"

sensor:
  - platform: pulse_counter
    pin: 
      number: 4
      mode: INPUT_PULLUP
    unit_of_measurement: 'RPM'
    accuracy_decimals: 0
    id: fan_tach
    name: Fornuftig Fanspeed
    filters:
      - multiply: 0.1

  - platform: template
    id: set_fan_freq
    filters:
      - multiply: 3
      - lambda: |-
          if (id(fornuftig_fan).state){
            if(x > 300) { // limit max frequency
              return 300;
              
            } else if(x < 36 && x > 3) { // limit low frequency
              return 36;
              
            } else if(x <= 3) {
              return 0;
              
            } else {
              return x;
              
            }
          } else {
            return 0;
          }
    on_value:
      then:
        - output.esp8266_pwm.set_frequency:
            id: pwm_output
            frequency: !lambda 'return int(x);'
        - logger.log: "on_value called"
        - lambda: |- 
            // send current status to homeassistant
            if(x) {
              auto call = id(fornuftig_fan).turn_on();
              call.set_speed(int(x)/3);
              call.perform();
            } else {
              auto call = id(fornuftig_fan).turn_off();
              call.perform();
            }

binary_sensor:
  - platform: template
    id: 'knob_0'
    filters:
      - delayed_on_off: 300ms
    lambda: |-
      if (!id(knob_1).state && !id(knob_2).state && !id(knob_3).state) {
        return true;
      } else {
        return false;
      }
    on_press: 
      lambda: |-
        if(!id(speed_0)){
          auto call = id(fornuftig_fan).turn_off();
          call.perform();
        }else{
          auto call = id(fornuftig_fan).turn_on();
          call.set_speed(id(speed_0));
          call.perform();
        }

  - platform: gpio
    id: 'knob_1'
    pin:
      number: 13
      mode: INPUT_PULLUP
      inverted: true
    filters:
      - delayed_on_off: 100ms
    on_press: 
      then:
        - fan.turn_on:
            id: fornuftig_fan
            speed: !lambda 'return id(speed_1);'
    
    
  - platform: gpio
    id: 'knob_2'
    pin:
      number: 12
      mode: INPUT_PULLUP
      inverted: true
    filters:
      - delayed_on_off: 100ms
    on_press: 
      then:
        - fan.turn_on:
            id: fornuftig_fan
            speed: !lambda 'return id(speed_2);'
  
  
  - platform: gpio
    id: 'knob_3'
    pin:
      number: 14
      mode: INPUT_PULLUP
      inverted: true
    filters:
      - delayed_on_off: 100ms
    on_press:
      then:
        - fan.turn_on:
            id: fornuftig_fan
            speed: !lambda 'return id(speed_3);'

globals:
  - id: fan_speed
    type: int
    restore_value: yes
    initial_value: '0'
    
  - id: speed_0
    type: int
    restore_value: yes
    initial_value: '0'
    
  - id: speed_1
    type: int
    restore_value: yes
    initial_value: '33'
    
  - id: speed_2
    type: int
    restore_value: yes
    initial_value: '66'
    
  - id: speed_3
    type: int
    restore_value: yes
    initial_value: '100'
1 Like

How could I get the v2-board ready to use? Is there any manufacuturer worldwide who can I sent the files to and get an assembled and soldered board from?

Yes. There are many of them. I used this one as it was the easiest (and relatively cheap): https://jlcpcb.com/

คุณช่วยแชร์วิธีสั่งได้ไหมครับ ขอบคุคณครับ

Can you please translate that to English, thank you.

Hi,

Looking to order this from JLC too. Is there a way I can only get one board printer and assembled?

Ordered two from JLC and they work great, thanks @horvathgergo

Anyone know if it’s possible to source a replacement board? I managed to brick mine while trying to power a wifi relay from the 5v on the board, probably shorted something (noob)

Edit: to specify – I meant the original Förnuftig board :slight_smile: