AIR & ENVIRONMENT / BUILD GUIDE

IKEA FÖRNUFTIG.
A smarter breath of air.

Six fan speeds in Home Assistant, motor feedback and a configurable filter reminder. Built around a WEMOS D1 Mini.

Experimental YAML validation is automated. Hardware operation still needs verification on your PCB revision; no browser firmware is published for this project.

Make the connections

Disconnect the original power adapter before modifying the board. Use a regulator rated for 24 V input and regulated 5 V output. Never connect the 24 V supply to the ESP’s 5V, 3V3 or GPIO pins. Measure its output before connecting the ESP.

FÖRNUFTIG connections: FG through 10 kΩ to D6, CLK to D7, R13 to D5, regulated 5 V and common ground.
Signal mapping follows Ed Voncken’s modification. Use the original photos to identify R13 and the trace to U1 on your PCB.
Download wiring diagram (SVG)
ConnectionWiringD1 Mini
FG10 kΩ series resistorD6 / GPIO12
CLKDirect connectionD7 / GPIO13
R13 motor controlCut R13 → U1 trace firstD5 / GPIO14
Regulator +5 VRegulated supply only5V
Common groundPCB + regulator + ESPG / GND

Cut the trace between the top of R13 and U1 before connecting D5. This modification replaces the original speed knob, filter reset button and filter LED control. Check the original guide’s board photos before soldering.

Configure your device

  1. Download or clone this repository. Install Python and pip install -r requirements-dev.txt.
  2. Generate a personal configuration:
    python scripts/secure_setup.py ikea_fornuftig --name fornuftig-livingroom
  3. Edit Wi-Fi details in esphome/private/fornuftig-livingroom/secrets.yaml. The generator supplies unique API encryption and OTA credentials.
  4. Flash over USB:
    esphome run esphome/private/fornuftig-livingroom/device.yaml
  5. Check that startup leaves the motor off. Test each speed, stop, feedback and restart before closing the housing. Add the discovered ESPHome device to Home Assistant with its generated encryption key.

The fan starts off after every restart. Feedback is reported as pulses per minute, since the motor’s pulses per revolution have not been verified. The filter counter accumulates minutes with motor feedback; it is saved every five minutes. A sudden power loss can lose recent runtime.

The default reminder is 4,320 running hours. Adjust fornuftig_filter_hours in your personal substitutions. This is a maintenance estimate, not measured filter contamination. Use “Reset filter runtime” after replacing the filter.

Complete project configuration on GitHub

The device YAML

This is the device package. The complete configuration above includes it together with the shared settings. Wi-Fi and credentials belong in your personal configuration.

View fan control and filter runtime YAML
# Pin mapping: https://edvoncken.net/2024/04/ikea-fornuftig-with-esphome/
# Hardware modification originally demonstrated by https://github.com/3ative/ikea-air-filter
# Experimental: verify the PCB revision and wiring before connecting power.
substitutions:
  fornuftig_filter_hours: "4320"  # Configurable reminder, not a dust measurement.

preferences:
  flash_write_interval: 5min

globals:
  - id: fornuftig_runtime_minutes
    type: uint32_t
    restore_value: true
    initial_value: '0'

output:
  - platform: gpio
    id: fornuftig_motor_enable
    pin: GPIO14  # D5 -> top of R13; cut the trace to U1 first.
  - platform: esp8266_pwm
    id: fornuftig_clock
    pin: GPIO13  # D7 -> CLK
    frequency: 50 Hz
  - platform: template
    id: fornuftig_speed_output
    type: float
    write_action:
      - if:
          condition:
            lambda: return state > 0.0f;
          then:
            - output.esp8266_pwm.set_frequency:
                id: fornuftig_clock
                frequency: !lambda return roundf(state * 6.0f) * 50.0f;
            - output.set_level:
                id: fornuftig_clock
                level: 50%
            - output.turn_on: fornuftig_motor_enable
          else:
            - output.turn_off: fornuftig_motor_enable
            - output.turn_off: fornuftig_clock

fan:
  - platform: speed
    id: fornuftig_fan
    name: "${device_name} Fan"
    output: fornuftig_speed_output
    speed_count: 6
    restore_mode: ALWAYS_OFF

sensor:
  - platform: pulse_counter
    id: fornuftig_feedback
    name: "${device_name} Fan feedback"
    pin: GPIO12  # D6 -> FG through a series 10 kOhm resistor.
    count_mode:
      rising_edge: INCREMENT
      falling_edge: DISABLE
    update_interval: 10s
    unit_of_measurement: "pulses/min"
    accuracy_decimals: 0
    entity_category: diagnostic
  - platform: template
    name: "${device_name} Filter runtime"
    id: fornuftig_runtime
    unit_of_measurement: h
    accuracy_decimals: 1
    state_class: total_increasing
    update_interval: 60s
    lambda: return id(fornuftig_runtime_minutes) / 60.0f;

interval:
  - interval: 60s
    then:
      - lambda: |-
          // Count only minutes with a running fan and valid motor feedback.
          if (id(fornuftig_fan).state &&
              !isnan(id(fornuftig_feedback).state) && id(fornuftig_feedback).state > 0) {
            id(fornuftig_runtime_minutes) += 1;
          }

binary_sensor:
  - platform: template
    name: "${device_name} Filter reminder"
    device_class: problem
    lambda: return id(fornuftig_runtime_minutes) >= ${fornuftig_filter_hours}UL * 60UL;

button:
  - platform: template
    name: "${device_name} Reset filter runtime"
    entity_category: config
    on_press:
      - globals.set:
          id: fornuftig_runtime_minutes
          value: '0'
      - sensor.template.publish:
          id: fornuftig_runtime
          state: 0

Add a BME680, optionally

The fan works without an environmental sensor. To add a BME680, connect its 3.3 V supply and I²C pins as shown above, then add this package beside your existing package in the generated personal configuration:

packages:
  device: !include ../../ikea_fornuftig.yaml
  air_sensor: !include ../../package/ikea_fornuftig_bme680.yaml

This standard driver reports temperature, humidity, pressure and gas resistance. It does not calculate IAQ or CO₂ equivalent as the article’s Bosch BSEC library does. Check whether your breakout uses address 0x77 or 0x76.

View optional BME680 YAML
# Optional BME680. Include this package only when the sensor is connected.
# D1/GPIO5 -> SCL, D2/GPIO4 -> SDA, 3V3 -> VCC, G -> GND.
# Standard driver: gas resistance is not a CO2 measurement or an IAQ index.
i2c:
  scl: GPIO5
  sda: GPIO4
  scan: true
sensor:
  - platform: bme680
    address: 0x77  # Change to 0x76 if required by your breakout.
    temperature:
      name: "${device_name} Temperature"
    pressure:
      name: "${device_name} Pressure"
    humidity:
      name: "${device_name} Humidity"
    gas_resistance:
      name: "${device_name} Gas resistance"
    update_interval: 60s

Credit & original build

Hardware mapping: Ed Voncken’s FÖRNUFTIG article and PCB photos, following David Martin / 3ATIVE’s original project. This repository provides a new ESPHome implementation with a native fan entity and runtime-based maintenance reminder.