IKEA UPPÅTVIND ESP32-C3 Smart Mod

Overview

This project describes how to modify the IKEA UPPÅTVIND air purifier using an ESP32 and ESPHome for local smart home integration.

The original button and LED/PWM signals of the purifier are connected to the ESP32, allowing full control and status monitoring through Home Assistant.

The modification keeps the original fan controller and hardware intact while adding WiFi connectivity and automation capabilities.

Price of the purifier: 25.05.2025 30€ at the local store and have a good availability.

Motivation

  • Manual button control stays
  • Integration in Home Assistant
  • Automate air purification (3d Printer/ spray paint can/ food dehydrator/ Dust )
  • Monitor purifier state remotely
  • Reuse existing hardware instead of replacing it

Features

  • ESPHome integration
  • Home Assistant fan entity
  • Fan speed detection
  • Simulated hardware button presses
  • WiFi control
  • Optional automation with sensors i have a Pico Air Monitoring Expansion running so it can be used to trigger the UPPÅTVIND

Cost Estimate

Approximate modification cost:

  • ESP32-C3 Super Mini: ~2–5$
  • Resistors / wiring: ~1$
  • Optional level shifter: ~1$
  • Optional soldering material/tools

Total: ~3–8$ excluding the purifier itself.

Hardware plan

Required:

  • IKEA UPPÅTVIND Air Purifier
  • ESP32-C3 Super Mini
  • 10kΩ resistor
  • 20kΩ resistor
  • Wiring
  • Soldering equipment

Optional:

  • Logic level shifter
  • Heat shrink tubing
  • Multimeter

Internal Signals

The purifier PCB exposes several useful test points. Here is a nice Project esphome-ikea-uppatvind

Known signals:

Test Point Function
TP2 GND
TP3 5V
TP4 Button Input
PWM FAN PWM Sense
TP6 Optional LED Signal

GPIO Mapping

i added a Transistor

BC547C
┌─────┐
│     │
└─────┘
 | | |

 C B E
UPPÅTVIND ESP32-C3 Function
TP2 GND Ground
TP3 5V Power
TP4 GPIO10 Button Simulation
PWM GPIO4 PWM Detection
TP6 (optional) GPIO5 Additional LED State

Wiring

Basic Wiring

UPPÅTVIND
PWM ---10kΩ ---+--- GPIO0 - yellow
               |
              20kΩ
               +-------- GND black
               
TP6 ---10kΩ ---+--- GPIO1 - green
               |
              20kΩ
               +-------- GND black
               
               
TP2 -------------------- GND - black
TP3 -------------------- 5V - red
TP4 -------------------- BC547 C - blue
ESP GPIO04 -- 4.7k ----- BC547 B
TP2 -------------------- BC547 E

System Behavior

  • ESP is always powered
  • Original purifier MCU stays active
  • ESP simulates button presses
  • Fan state is detected through LED/PWM signal monitoring
  • Home Assistant controls the purifier locally

ESPHome

Functions implemented:

  • Button simulation
  • Fan speed detection
  • Fan entity
  • API integration
  • WiFi fallback AP
# ------------------------------
#  Sensors Configuration
# ------------------------------

# ------------------------------
#  Sensors & Status
# ------------------------------
binary_sensor:
  - platform: status
    name: "${devicename} Status"

# TP6 -> GPIO1 (Filter LED)
  - platform: gpio
    pin:
      number: GPIO1
      mode:
        input: true
        pullup: true
    name: "UPPATVIND Filter Warning"
    id: filter_led
      
# ------------------------------
# Information Sensors
# ------------------------------


  - platform: pulse_counter
    pin:
      number: GPIO0
      mode:
        input: true
        pullup: true
    name: "UPPATVIND Motor Pulses"
    id: uppatvind_rpm
    update_interval: 1s
    internal: true

# ------------------------------
#  Text Sensors (Diagnostics)
# ------------------------------
text_sensor:
  - platform: wifi_info
    ip_address:
      name: "${devicename} IP Address"
    ssid:
      name: "${devicename} Connected SSID"
    bssid:
      name: "${devicename} BSSID"
    mac_address:
      name: "${devicename} MAC Address"

  - platform: template
    name: UPPATVIND Mode
    id: uppatvind_mode
    lambda: |-
      float rpm = id(uppatvind_rpm).state;
      if (rpm < 1000)
        return {"Off"};
      if (rpm < 9500)
        return {"Low"};
      if (rpm < 13500)
        return {"Medium"};
      return {"High"};
    update_interval: 1s

# ------------------------------
#  Restart Switch Configuration
# ------------------------------
switch:
  - platform: restart
    name: ESP-UPPATVIND restart

# ------------------------------
# TP4 -> GPIO04 (Button)
# ------------------------------
output:
  - platform: gpio
    pin:
      number: GPIO4
      inverted: false
    id: fan_button

select:
  - platform: template
    name: "UPPATVIND Speed"
    id: uppatvind_speed
    optimistic: false
    options: ["Off", "Low", "Medium", "High"]
    lambda: |-
      return id(uppatvind_mode).state;
    set_action:
      - if:
          condition:
            lambda: |-
              return x == "Off";
          then:
            - button.press: uppatvind_off
            - delay: 6s
      - if:
          condition:
            lambda: |-
              return x != "Off";
          then:
            - script.execute:
                id: goto_speed
                target: !lambda 'return x;'
    
script:
  - id: goto_speed
    mode: queued
    parameters:
      target: string
    then:
      - repeat:
          count: !lambda |-
            int current = 0;
            int desired = 0;
            auto state = id(uppatvind_mode).state;
            if (state == "Low")
              current = 1;
            else if (state == "Medium")
              current = 2;
            else if (state == "High")
              current = 3;

            if (target == "Low")
              desired = 1;
            else if (target == "Medium")
              desired = 2;
            else if (target == "High")
              desired = 3;
            if (current == desired)
            {
              ESP_LOGI("uppatvind", "Already at target");
              return 0;
            }
            int steps = (desired - current + 4) % 4;
            ESP_LOGI(
              "uppatvind",
              "Current=%d Desired=%d Steps=%d",
              current,
              desired,
              steps
            );
            return steps;
          then:
            - button.press: uppatvind_button
            - delay: 1500ms

button:
  - platform: template
    name: "UPPATVIND Next"
    id: uppatvind_button
    on_press:
      then:
        - output.turn_on: fan_button
        - delay: 250ms
        - output.turn_off: fan_button

  - platform: template
    name: "UPPATVIND OFF"
    id: uppatvind_off
    on_press:
      then:
        - output.turn_on: fan_button
        - delay: 4000ms
        - output.turn_off: fan_button

Home Assistant Integration

Possible automations:

  • Start purifier remote when 3d Printer is running
  • Enable purifier when PM2.5 max target is reached
  • Enable higher speed when PM2.5 increases
  • Turn off when PM2.5 min target is reached
  • Turn off when 3d printer is not running
  • Combine with air quality sensors
  • Dashboard integration
type: custom:stack-in-card
cards:
  - type: custom:mushroom-template-card
    primary: UPPATVIND I
    secondary: >
      {% if is_state('binary_sensor.uppatvind_filter_warning','on')
      %}
        ⚠ Filter Warning
      {% else %}
        Filter OK
      {% endif %}
    icon: mdi:air-purifier
  - type: custom:mushroom-chips-card
    chips:
      - type: template
        icon: mdi:fan-off
        entity: select.uppatvind_speed
        icon_color: >
          {% if is_state('select.uppatvind_speed','Off') %} red {%
          endif %}
        tap_action:
          action: perform-action
          perform_action: select.select_option
          target:
            entity_id: select.uppatvind_speed
          data:
            option: "Off"
      - type: template
        icon: mdi:fan-speed-1
        entity: select.uppatvind_speed
        icon_color: >
          {% if is_state('select.uppatvind_speed','Low') %} green {%
          endif %}
        tap_action:
          action: perform-action
          perform_action: select.select_option
          target:
            entity_id: select.uppatvind_speed
          data:
            option: Low
      - type: template
        icon: mdi:fan-speed-2
        entity: select.ppatvind_speed
        icon_color: >
          {% if is_state('select.uppatvind_speed','Medium') %} orange
          {% endif %}
        tap_action:
          action: perform-action
          perform_action: select.select_option
          target:
            entity_id: select.uppatvind_speed
          data:
            option: Medium
      - type: template
        icon: mdi:fan-speed-3
        entity: select.uppatvind_speed
        icon_color: >
          {% if is_state('select.uppatvind_speed','High') %} red {%
          endif %}
        tap_action:
          action: perform-action
          perform_action: select.select_option
          target:
            entity_id: select.uppatvind_speed
          data:
            option: High

Calibration

The LED/PWM signal values may vary between purifier revisions.

After flashing ESPHome:

  • Monitor pulse width values
  • Record values for:
    • OFF
    • Speed 1
    • Speed 2
    • Speed 3
  • Adjust thresholds in ESPHome YAML

Safety Notes

  • Disconnect power before soldering
  • Verify voltages with a multimeter
  • ESP32-C3 GPIOs are NOT 5V tolerant
  • Avoid short circuits inside the purifier

Future Improvements

  • Fully native fan control
  • Automatic air quality mode
  • ESPHome dashboard improvements
  • Internal USB-C power mod
Drucken/exportieren