IKEA Vindriktning Air Quality Sensor

OK did a lot more testing and it doesn’t really make sense. Sometimes the sensor works, most of the time does not. If it powers on, it usually continues to work fine. But if it fails on power-up, it will not work until it gets a successful power-on.

After extensive testing what seems to have solved it is adding a 10k pull-up resistor to the sensor’s RX (input) line. Looks like the sensor goes into some other mode or state if RX is not pulled up during power-up. The ESP8266 is supposed to have a pull-up on the TX line (TX pin for the UART, GPIO1) but the pull up is to 3.3 V and so may explain why the sensor sometimes works and sometimes not.

I added a level converter to the RX line since ESP is 3.3 V and the sensor is 4.5 V. It shouldn’t work so conversion at least makes sure of that. Pull up is on the sensor’s side to 5 V. The converter’s Output Enable is controlled by a GPIO that enables the converter just before a measurement is taken. Not sure if that is necessary or not though.

As for consistency, I thought I broke the first sensor so got a new one. Now both of them are connected to HA, one via the original Hypfer style (rx only) and the other one via Esphome with rx/tx. Both are reporting the same value, within a few µg/m^3.

Taking a lot of inspiration from this discussion I’ve finally put together my own Air Quality Sensor based on the Vindriktning. I’ve managed to fit a fair bit into this little device, including a DHT22 Temperature and Humidity sensor which I have cut out so its exposed to the outside of the case, a MH-Z19 for CO2 sensing, and an addressable RGB LED, all coming back to the Wemos D1 mini. The RGB led was added to provide a secondary red, yellow, green indicator controlled by HA for the CO2 level or any other notification I guess.

8 Likes

This is almost exactly what I did too! I have a HTU21D for RH/T, PM1006 for PM2.5 and MH-Z19B for CO2, all in the Vindriktning case. I thought about adding the leds just like you did, still in process of designing it, the D1 Mini does not have many IO left…

1 Like

Look at this:

This Czech company designed a complete replacement board for the device.

1 Like

I built 3 air quality sensors using the Vindriktning along with a SCD40 for Temperature, CO2 and Humidity measurements. On my first 2, I had problems with the pads lifting (mentioned earlier in this thread) as I was trying to route the wires. So on those 2, I completely removed the stock board and replaced it with a small strip of WS2812B LEDs. I also cut a hole in the rear to plug power directly into the D1 Mini. The advantage to this is you have addressable RGB so you can make them any colour based on any parameter. The disadvantage of removing the default board is loss of the USB Type C charge port and the built in fan control (mine run 100% of the time).

I moved the fan power from 5V to 3.3V as suggested in this thread. They are seemingly silent but I notice my temperature measurements seem a degree or 2 too warm which might indicated 3.3V isn’t sufficient to dissipate the heat from the microcontroller. I also wonder if other readings, like PM2.5 are affected.

After 1 week of use, I learned a lot about my home and HVAC system. For reference, I live in a well insulated home (built 6 years ago) and in my region (Canadian prairies), the experts recommend not running your HRV during the summer because it brings in humidity from outside which causes your Air Conditioner to work harder. The CO2 sensors told me my baseline CO2 levels were around 800. I also had my HVAC fan set to ‘Auto’ which only runs when the furnace or AC activates. In my bedroom with those settings, my CO2 levels reached nearly 2500 PPM while sleeping, well over the 1000 PPM threshold Health Canada recommends. Even sitting in my office, I would get up to 1200 PPM. So I changed my HRV to run continuously at low speed and run my HVAC fan continuously as well. Now my baseline CO2 levels are 500-600 PPM and barely move in rooms that are occupied. Eventually, I think I should get a smart thermostat that integrates with my HRV so I can request fresh air based on CO2 levels in the home instead of based on humidity.

The PM2.5 sensor is handy too. I see the spikes when I’m cooking. I also do some painting with an airbrush and it’s useful to see if my ventilation is adequate and if it’s safe to remove my respirator.





9 Likes

Really nice ! Could you please share your ESPHome Yaml ? Do you have any reference to buy the display and some picture of the IO you used on the board ?

Thanks !

Verry nice.

I made a CO² analyser, I was afraid of the result, specially à 7h00 in the morning: door closed, I often read over 2200 ppm.
I tried to put 5 leds (power, green orange red and a fifth one for… I don’t now…), but I ran out of availlable GPIO… some times, the esp8266 didn’t boot.
i put one adressable led, a PL9823 (compatible WS2812)
if I am able to lit it in red (or any color I want from the web side of the esp) i cannot find how to lit it with the percentage of data read in the captor. this must be running locally in the esp.

about the CO², it is so hot now in Paris than we have the windows open 24/7 and the MH-Z19 always stays at 400 pph, that is its lower base.
I would like to see your yaml file for the ribbon-led !!!

I will see later for the particule sensor. (as soon as my led will display the quality of the air)

@bentou

Here’s my yaml. I was planning on setting the LED colour using an automation but haven’t got around to it yet.

esphome:
  name: airquality1

esp8266:
  board: d1_mini

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

wifi:
  networks:
    - ssid: !secret IoT_wifi_ssid
      password: !secret IoT_wifi_password

captive_portal:

i2c:
  sda: D1
  scl: D2
  scan: true
  
uart:
  rx_pin: D4
  tx_pin: D3
  baud_rate: 9600

sensor:
  - platform: pm1006
    pm_2_5:
      name: "Particulate Matter 2.5µm Concentration"
    update_interval: 60s
    
  - platform: scd4x
    co2:
      name: "CO2"
    temperature:
      name: "Temperature"
    humidity:
      name: "Humidity"
    temperature_offset: 4
    measurement_mode: "low_power_periodic"
      
light:
  - platform: neopixelbus
    variant: WS2812x
    pin: D8
    num_leds: 3
    type: GRB
    name: "FastLED WS2812B Light"
1 Like

I made something like that… it is just the beginning.
I can switch on or off the status light and change the colors from Home Assistant
or directly in the embeded web site of the ESP.

light:
  - platform: neopixelbus
    name: "my_status Light"
    pin: GPIO12        #d6
    variant: WS2812
    id: status_light
    num_leds: 1
    type: rgb

    effects:
      - automation:
          name: "Green"
          sequence:
            - light.addressable_set:
                id: status_light
                red: 0%
                green: 100%
                blue: 0%

      - automation:
          name: "Orange"
          sequence:
            - light.addressable_set:
                id: status_light
                red: 100%
                green: 50%
                blue: 0%
 
     - automation:
          name: "Yellow"
          sequence:
            - light.addressable_set:
                id: status_light
                red: 100%
                green: 100%
                blue: 0%

      - automation:
          name: "Rouge"
          sequence:
            - light.addressable_set:
                id: status_light
                red: 100%
                green: 0%
                blue: 0%

I could not manage directly the colors with the “on_value” of the co2 sensor.
not yet…

I got it working today. Here’s my automation yaml:

alias: Air Quality 1 LED
description: ""
trigger:
  - platform: time_pattern
    minutes: /1
condition: []
action:
  - choose:
      - conditions:
          - condition: and
            conditions:
              - type: is_carbon_dioxide
                condition: device
                device_id: 80988a988e5bdf16e2b012845f20ccae
                entity_id: sensor.co2_1
                domain: sensor
                below: 1000
              - type: is_pm25
                condition: device
                device_id: 80988a988e5bdf16e2b012845f20ccae
                entity_id: sensor.particulate_matter_2_5um_concentration_1
                domain: sensor
                below: 50
        sequence:
          - service: light.turn_on
            target:
              device_id: 80988a988e5bdf16e2b012845f20ccae
              entity_id: light.fastled_ws2812b_light
            data:
              brightness: 127
              rgb_color:
                - 0
                - 249
                - 0
      - conditions:
          - condition: or
            conditions:
              - type: is_carbon_dioxide
                condition: device
                device_id: 80988a988e5bdf16e2b012845f20ccae
                entity_id: sensor.co2_1
                domain: sensor
                above: 1000
                below: 1500
              - type: is_pm25
                condition: device
                device_id: 80988a988e5bdf16e2b012845f20ccae
                entity_id: sensor.particulate_matter_2_5um_concentration_1
                domain: sensor
                above: 50
                below: 100
        sequence:
          - service: light.turn_on
            data:
              rgb_color:
                - 255
                - 252
                - 65
              brightness: 126
            target:
              device_id: 80988a988e5bdf16e2b012845f20ccae
              entity_id: light.fastled_ws2812b_light
      - conditions:
          - condition: or
            conditions:
              - type: is_carbon_dioxide
                condition: device
                device_id: 80988a988e5bdf16e2b012845f20ccae
                entity_id: sensor.co2_1
                domain: sensor
                above: 1500
              - type: is_pm25
                condition: device
                device_id: 80988a988e5bdf16e2b012845f20ccae
                entity_id: sensor.particulate_matter_2_5um_concentration_1
                domain: sensor
                above: 100
        sequence:
          - service: light.turn_on
            data:
              rgb_color:
                - 255
                - 38
                - 0
              brightness: 126
            target:
              device_id: 80988a988e5bdf16e2b012845f20ccae
              entity_id: light.fastled_ws2812b_light
    default: []
mode: single

So once a minute, it checks the conditions.
If CO2 ppm < 1000 AND PM 2.5 < 50 ug/m3, the LED is green
If CO2 ppm 1000-1500 OR PM2.5 is 50-100, the LED is yellow
If CO2 ppm > 1500 OR PM2.5 > 100, the LED is red

If I wanted to get even fancier, I think I could make the addressable LEDs indicate different values but this is good enough for me.

After 8 months of usage since my setup, I am noticing the fan noise got quite loud. The noise doesn’t bother me as much since the box is placed over the fridge. I noticed this winding noise a few days ago and I thought my fridge is dying (the fridge is well over the 7 years of usage). It turns out that the noise was from the sensor. I am hoping the fan noise is just a noise and will not pose any danger to the point of melt down. I didn’t modify the fan and kept it as it from factory.

Ok, It is quite good for the LED without any automation from the HA side.

sensor:
  # MH-Z19 Analyseur de CO2
  - platform: mhz19
    co2:
      name: "Valeur CO² $long_devicename"
      id: mhz19_co2
      on_value_range:

#        - above: 400
        - below: 899
          then:
          - light.addressable_set:
              id: status_light # Green
              red: 0%
              green: 100%
              blue: 0%

        - above: 900
          below: 1499
          then:
          - light.addressable_set:
              id: status_light # Yellow
              red: 100%
              green: 100%
              blue: 0%

        - above: 1500
          below: 2199
          then:
          - light.addressable_set:
              id: status_light # Orange
              red: 100%
              green: 50%
              blue: 0%

        - above: 2200
          then:
          - light.addressable_set:
              id: status_light # Red
              red: 100%
              green: 0%
              blue: 0% 

    temperature:
      name: "Temperature $long_devicename"
      id: mhz19_temp

light:
  - platform: neopixelbus
    name: "Status Light $long_devicename"
    pin: GPIO12   # pin d6
    variant: WS2812
    id: status_light
    num_leds: 1
    type: rgb

the only problem I still have is that the LED doesn’t work properly at the boot time…
either, it doesn’t lit, either it lits in white.

if it lits in white, I have to wait for a higher value of CO², (in order to change the color of the led).
if it doesn’t lit, I can switch it on with the web page or with HA.
when it is started, it works … :man_facepalming:

I have the same problem. Did you ever solve it? Or @fabiosoft do you know?

Edit:
After playing around with it for ~8 hours I would say if you go the full control route, ditch the IKEA pcb. I am no expert in electronics yet feel like the design is so weird. It’s a pain with the LEDs.

From more testing and online discussion I have to conclude that it is possible that the LEDs are not powered by the power supply but by the ESP. That is not best practice. I am going to ditch the IKEA PCB, except for the bottom and the capacitor. Then use a custom RGB anode LED with BC547 transistors.

My package is:

  • ESP32, close to bare; 3.3V
  • AMS1117 voltage regulator (5V → 3.3V)
  • PM1006 (PM2.5), fan continuously at 3.3V
  • MH-Z19B (CO2)
  • SGP41 (VOC and NOx)
  • RGB LED
  • PCBs and wires to hook it all up

Might make a how to for noobs like myself. Also, it was very difficult to cut and fit everything. Precision such as wire length is important. Sensor values were compared to sensors outside the box. They are very close.

Hi, unfortunately the dim orange LED is still an issue, anyway your solution seems good.
I digged into the IKEA board to give sense and repurpose to local control this very cool and cheap device, for me, i doesn’t make sense get this device and then completly rebuild with external components.

Happy Vindriktning! :laughing:

the site says it’s $50 shipping (for a $20 part) to the US.

Have you seen any other options - either a store in the US or better shipping from the Czech Republic?

ouch, very expensive… maybe a more DIY implementation is cheaper… or if you have schematics you can order the PCB from china

Sorry, no idea. That is a purpose made board from that company. It is their design and work. Those prices what you are quoting seems normal regarding shipment cost, customs tax, etc.
That is not a dodgy Chinese webshop, they are a company in Europe. If it would be a US company and I would like to order it to Europe, then they would quote similar shipping costs.

1 Like

Thanks for this, gave me the push to get control off of HA and onto the device locally. I too have the issue of it not working on boot. Looking at running a on boot command to check levels and set initial color

I rewrote my configuration, here is the new one.

I have some trouble with the

substitutions:
  devicename: esp8266-air-chambre
  long_devicename: Capteur CO2 Chambre
  
esphome:
  name: $devicename
  platform: ESP8266
  board: nodemcuv2
  comment: $long_devicename
  on_boot:
    # Allumage de la led
#    priority: -100.0
    then:
      - light.addressable_set:
          id: voyant_led
          red: 50%
          green: 0%
          blue: 100%
          color_brightness: 25%
      - light.turn_on:
          id: voyant_led
          brightness: 25%
          
# Enable logging
logger:
#  baud_rate: 0
#  level: VERBOSE

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  power_save_mode: light
  ap:
    ssid: $devicename
    password: !secret ap_password

captive_portal:
  
api:
  encryption:
    key: "Gp0ThuSE0gFfxIPJ0yCfny0a3O7zyLWj1N7HElcGSt8="
  # service definition to force manual zero calibration from HA
  services:
    - service: mhz19_calibrate_zero
      then:
        - mhz19.calibrate_zero: mhz19_calibration

ota:
  password: "a0c4bfe83247ed98b0be4f2f9ff78f02"
  
# Enable Web server.
web_server:
  port: 80

# Sync time with Home Assistant.
time:
  - platform: homeassistant
    id: homeassistant_time

font:
  - file: "arial.ttf"
    id: my_font2
    size: 9
  - file: "arial.ttf"
    id: my_font3
    size: 16

display:
  - platform: ssd1306_i2c
    model: "SSD1306 128x64"
    reset_pin: D0
    address: 0x3C
    lambda: |-
      it.printf(0,  4, id(my_font2), "CO2");
      it.printf(32, 0, id(my_font3), "%.1f ppm", id(mhz19_co2).state);
      
      it.printf(0, 20, id(my_font2), "Temp");
      it.printf(32, 16, id(my_font3), "%.1f °C", id(bme280_temp).state);

      it.printf(0, 36, id(my_font2), "Hygr");
      it.printf(32, 32, id(my_font3), "%.1f %%", id(aht10_hyg).state);
      
      it.printf(0, 52, id(my_font2), "Press");
      it.printf(32, 48, id(my_font3), "%.1f hPa", id(bme280_pr).state);
      
binary_sensor:
  # statut
  - platform: status
    name: "Statut $long_devicename"
  
sensor:
  # Mesure Temps de fonctionnement
  - platform: uptime
    id: uptime_sec

 # MH-Z19 Capteur de CO2
  - platform: mhz19
    co2:
      name: "Valeur CO² $long_devicename"
      id: mhz19_co2
      on_value_range:

        - below: 401
          then:
          - light.addressable_set:
              id: voyant_led
              red: 0%
              green: 100%
              blue: 0%

        - above: 401
          below: 899
          then:
          - light.addressable_set:
              id: voyant_led
              red: 0%
              green: 100%
              blue: 0%

        - above: 900
          below: 1499
          then:
          - light.addressable_set:
              id: voyant_led
              red: 100%
              green: 100%
              blue: 0%

        - above: 1500
          below: 2199
          then:
          - light.addressable_set:
              id: voyant_led
              red: 100%
              green: 50%
              blue: 0%

        - above: 2200
          then:
          - light.addressable_set:
              id: voyant_led
              red: 100%
              green: 0%
              blue: 0% 

    temperature:
#      name: "Temperature MH-Z19 $long_devicename"
      id: mhz19_temp
      
    automatic_baseline_calibration: false
    uart_id: uart_mhz19
    id: mhz19_calibration
    update_interval: 60s
    
  # ATH10 Capteur de temperature et d'humidité I²C
  - platform: aht10
    temperature:
      name: "Temperature AHT10 $long_devicename"
      id: aht10_temp
      
    humidity:
      name: "Humidite AHT10 $long_devicename"
      id: aht10_hyg
    address: 0x38
    update_interval: 60s
    
  # BME280 Capteur de temperature et de pression I²C
  - platform: bme280
    address: 0x76
    update_interval: 60s
    
    temperature:
#      name: "Temperature BME280 $long_devicename"
      id: bme280_temp
              
    pressure:
      name: "Pression BME280 $long_devicename"
      id: bme280_pr
      on_raw_value:
      - sensor.template.publish:
          id: bme280_pr_hg
          state: !lambda 'return x * 0.75006375541921;'

  - platform: template
    name: "Barometre $long_devicename"
    id: "bme280_pr_hg"
    unit_of_measurement: 'mmHg'
    accuracy_decimals: 0

text_sensor:
  # Affichage Temps de Fonctionnement
  - platform: template
    name: "Uptime $long_devicename"
    lambda: |-
      int seconds = (id(uptime_sec).state);
      int days = seconds / (24 * 3600);
      seconds = seconds % (24 * 3600); 
      int hours = seconds / 3600;
      seconds = seconds % 3600;
      int minutes = seconds /  60;
      seconds = seconds % 60;
      return { (String(days) +"j " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
    icon: mdi:clock-start
    update_interval: 60s

switch:
  # Bouton de redémarrage
  - platform: restart
    name: "Redémarrage $long_devicename"
    
  # switch configuration to turn calibration on/off
  - platform: template
    name: "Calibration $long_devicename"
    optimistic: true
    on_turn_on:
      mhz19.abc_enable: mhz19_calibration
    on_turn_off:
      mhz19.abc_disable: mhz19_calibration

uart:
  - rx_pin: GPIO13   # pin d7
    tx_pin: GPIO15   # pin d8
    baud_rate: 9600
    id: uart_mhz19
    
i2c:
  - sda: GPIO05     # pin d1
    scl: GPIO04     # pin d2
    scan: true
    id: bus_i2c

light:
  - platform: neopixelbus
    name: "Status Light $long_devicename"
    pin: GPIO12   # pin d6
    variant: WS2812
    id: voyant_led
    num_leds: 1
    type: rgb

The initialisation of the light is a bit better than the previous script, mainly because the on_value_range start too early and is refreshed only when the value is changing of range. So I add a second range near the floor value of the CO2 sensor.

the data-in of the led is connected on D6, data-out is not connected.