IKEA VINDSTYRKA Zigbee Air quality sensor

IKEA recently introduced a new Zigbee-enabled air quality sensor that measures PM2.5 (Particulate Matter), tVOC (change in Volatile Organic Compounds over time), relative humidity, and room temperature.

Product page: VINDSTYRKA Air quality sensor, smart - IKEA

I picked up three of them to put around the house and connected them to HA using ZHA.

First impressions: the display is easy to read, the backlight gets plenty bright and has an auto shutoff feature, the PM2.5 measurement is very responsive (more on this later), and USB-C makes them convenient to power.

Remarks

  • There is a small fan inside of the device (to draw air over the sensor) that makes a slight but audible buzzing noise.
  • VOC is not reported through Zigbee; the display also does not show any VOC value other than an arrow that indicates levels are increasing/decreasing/flat over time.
  • Temperature is displayed and reported in whole numbers only (no decimal places), this makes them less than ideal for controlling a thermostat.
  • Pairing works different compared to other IKEA Home Smart devices, instead of holding down the button to pair you have to press the link button 4 times to reset the device after which it will immediately be discoverable.
  • While the update rate of the display on the unit itself is very fast, the reporting rate through Zigbee is very slow and random (ranging from several minutes between sensor updates to several hours without updates, even though the values on the display definitely changed). Not sure if this is a quirk with the device, ZHA, or my Conbee II coordinator.

These can also be paired to IKEA air purifiers for automatic control, though I haven’t been able to test that.

The device is held together by four recessed Torx T6 screws

Pictures

Main PCB:

Sensor block:

Display:

Bottom:

Rear air intake:

ZHA: Workaround for slow entity updates

I noticed that when I spam the “read attribute” button in the Zigbee device diagnostics panel I can get HA to acquire new PM2.5 readings in near-real-time, so as a workaround I am now polling the attributes every minute over the WebSocket API to keep the entities up-to-date.

Unfortunately there is no zha.get_zigbee_cluster_attribute service (only set_zigbee_cluster_attribute), that would have made it possible to do the polling internally through an automation instead of externally using a script on a timer.

I can share the script if there is interest for this.

11 Likes

Nice, a the successor of the “dumb” IKEA Vindriktning, also a PM2.5 sensor but lacking Zigbee and temperature/humidity. Could be made smart with an ESP but with this new model will have all in one. More expensive though.

I found something interesting by “brute-forcing” the Zigbee clusters/attributes:

Cluster 0xfc7e / 64638 provides a measurement in the range of 0-500:

[INFO] Get attr: {'id': 1, 'ieee': '38:5c:fb:ff:fe:c7:1c:a1', 'endpoint_id': 1, 'cluster_id': 64638, 'cluster_type': 'in', 'attribute': 0}
[INFO] Response: {'id': 1, 'type': 'result', 'success': True, 'result': '202.0'}
[INFO] Get attr: {'id': 2, 'ieee': '38:5c:fb:ff:fe:c7:1c:a1', 'endpoint_id': 1, 'cluster_id': 64638, 'cluster_type': 'in', 'attribute': 1}
[INFO] Response: {'id': 2, 'type': 'result', 'success': True, 'result': '0.0'}
[INFO] Get attr: {'id': 3, 'ieee': '38:5c:fb:ff:fe:c7:1c:a1', 'endpoint_id': 1, 'cluster_id': 64638, 'cluster_type': 'in', 'attribute': 2}
[INFO] Response: {'id': 3, 'type': 'result', 'success': True, 'result': '500.0'}
[INFO] Get attr: {'id': 4, 'ieee': '94:34:69:ff:fe:c1:df:9c', 'endpoint_id': 1, 'cluster_id': 64638, 'cluster_type': 'in', 'attribute': 0}
[INFO] Response: {'id': 4, 'type': 'result', 'success': True, 'result': '239.0'}
[INFO] Get attr: {'id': 5, 'ieee': '94:34:69:ff:fe:c1:df:9c', 'endpoint_id': 1, 'cluster_id': 64638, 'cluster_type': 'in', 'attribute': 1}
[INFO] Response: {'id': 5, 'type': 'result', 'success': True, 'result': '0.0'}
[INFO] Get attr: {'id': 6, 'ieee': '94:34:69:ff:fe:c1:df:9c', 'endpoint_id': 1, 'cluster_id': 64638, 'cluster_type': 'in', 'attribute': 2}
[INFO] Response: {'id': 6, 'type': 'result', 'success': True, 'result': '500.0'}
[INFO] Get attr: {'id': 7, 'ieee': '94:34:69:ff:fe:86:a7:41', 'endpoint_id': 1, 'cluster_id': 64638, 'cluster_type': 'in', 'attribute': 0}
[INFO] Response: {'id': 7, 'type': 'result', 'success': True, 'result': '69.0'}
[INFO] Get attr: {'id': 8, 'ieee': '94:34:69:ff:fe:86:a7:41', 'endpoint_id': 1, 'cluster_id': 64638, 'cluster_type': 'in', 'attribute': 1}
[INFO] Response: {'id': 8, 'type': 'result', 'success': True, 'result': '0.0'}
[INFO] Get attr: {'id': 9, 'ieee': '94:34:69:ff:fe:86:a7:41', 'endpoint_id': 1, 'cluster_id': 64638, 'cluster_type': 'in', 'attribute': 2}
[INFO] Response: {'id': 9, 'type': 'result', 'success': True, 'result': '500.0'}

Output from all three sensors. Attribute 0 is the current measurement, attribute 1 is the lower bound, and attribute 2 is the upper bound.

Yes! This is the tVOC measurement. The reading goes up and down in conjunction with the tVOC “trend display” on the unit itself when I spray some body spray into the room.

Now I only need to figure out how to get Home Assistant to understand this as an Entity :slight_smile:


The Sensirion SEN54 sensor module inside VINDSTYRKA reports a “VOC index” on a scale of 0-500, which matches the bounds reported by the attribute.

So it looks like we’re getting the processed value directly from the sensor: https://sensirion.com/media/documents/02232963/6294E043/Info_Note_VOC_Index.pdf

4 Likes

Please let this work :slight_smile:

I have a 3d printer in my office and want to monitor my VOC levels, but i want something with a display AND compatible with home assistant.

Please say if you get this to work :slight_smile:

My Dirigera upgraded automatically last night and suddenly the Vindstyrka sensor measurements show up in Home Assistant! Good work IKEA - I wasn’t even at home to trigger any firmware upgrade or HA refresh - it just started as a time series from around 3.30 in the night. The recent at-a-glance graph appears to automatically to a neat averaging - to be picky, actually measurements could have better resolution, but it hardly matters in real life.

1 Like

Does it show measurements for both VOC and PM2.5? Unfortunately I don’t have an IKEA hub to test with.

Here is some good news, a wild VOC entity appeared:

image

Writing the quirk was the easy part, now it needs to be made production ready and somehow merged in all the right places :stuck_out_tongue:

Besides creating the quirk there is also some patching required in HA Core in order for ZHA to recognize the “VOC index” sensor type. Now I just hot patched it in my running container, testing in production like a true developer.

6 Likes

I bought one and it works fine with zigbee2mqtt and Conbee2 stick. Data is same as above.

1 Like

Zigbee2mqtt was indeed quicker with adding support for the VOC value: Add VOC index to IKEA Vindstyrka by 0ip · Pull Request #5546 · Koenkk/zigbee-herdsman-converters · GitHub

That’s good to know for reference.

1 Like

Progress!

Awaiting merging of the “quirk” before asking this one to be reviewed:

1 Like

Strange. I just get this device and no way to see it using conbee2+zha. Everything on hassos.

1 Like

great work, the deice is connected via ZHA but like you mentioned the update intervals are realy unreliable.
You mentioned a script, would you be able to share it?

Try resetting the device by quickly pressing the link button 4 times, do this while HA is searching for Zigbee devices. It should appear immediately.

If this doesn’t work try upgrading the firmware of your ConBee II stick first (they often ship with heavily outdated firmware). Upgrade instructions can be found here: Update deCONZ manually · dresden-elektronik/deconz-rest-plugin Wiki · GitHub

8 Likes

Yes, though it’s more of an external hack that continuously “pokes” ZHA so it fetches new values.

This runs completely outside of HA and requires some setup, so I’m assuming at least some basic knowledge of Python.

Requirements:

pyyaml
websockets

Main script: zha-poll.py

import yaml
import asyncio
import websockets
import json
import logging

logging.basicConfig(level=logging.INFO, format='[{levelname:4.4s}] {message}', style='{')
logger = logging.getLogger()

with open('config.yml') as cf:
    config = yaml.safe_load(cf)


async def ha_send(ws, event_type, data):
    """ Send event to HA websocket """
    await ws.send(json.dumps({'type': event_type, **data}))


async def ha_recv(ws):
    """ Receive events from HA websocket """
    return json.loads(await ws.recv())


async def ha_await(ws, message_id):
    """ Await a message with a specific ID """
    while True:
        message = await ha_recv(ws)
        if message['id'] == message_id:
            return message


async def main():
    logging.info(f'Connecting Websocket: {config["uri"]}')
    # Open websocket
    async with websockets.connect(config['uri']) as ws:

        # Authorize with HA
        if (await ha_recv(ws))['type'] == 'auth_required':
            await ha_send(ws, 'auth', {'access_token': config['auth']})
            auth_response = await ha_recv(ws)
            if not auth_response['type'] == 'auth_ok':
                raise Exception(f'Auth failed: {auth_response}')

        logger.info('Connected, reading attributes')

        message_id = 0

        # Instruct ZHA to read the device attributes
        for device in config['devices']:
            for endpoint in device['endpoints']:
                for cluster in endpoint['clusters']:
                    for attribute in cluster['attributes']:
                        # Construnct and send ZHA message
                        message_id += 1
                        event_data = {
                            'id': message_id,
                            'ieee': device['ieee'],
                            'endpoint_id': endpoint['id'],
                            'cluster_id': cluster['id'],
                            'cluster_type': cluster['type'],
                            'attribute': attribute['id']
                        }
                        logger.info(f'Get attr: {event_data}')
                        await ha_send(ws, 'zha/devices/clusters/attributes/value', event_data)

                        # Await and log the response
                        try:
                            response = await asyncio.wait_for(ha_await(ws, message_id), 5)
                            logger.log(logging.INFO if response['success'] else logging.ERROR, f'Response: {response}')

                        except asyncio.TimeoutError:
                            logger.error('Response timed out')

if __name__ == "__main__":
    asyncio.run(main())

Configuration file: config.yml (change the IEEE’s to match those of your devices)

# Home Assistant Websocket URI (use wss:// for HTTPS)
uri: ws://172.16.20.100:8040/api/websocket

# Add your Long-lived access token (https://developers.home-assistant.io/docs/auth_api/#long-lived-access-token)
auth: SECRET

_templates:
  # IKEA VINDSTYRKA
  ikea_vindstyrka: &ikea_vindstyrka
    endpoints:
    - id: 1
      # Clusters to poll attributes from
      clusters:
        - id: 0x042a  # PM2.5
          type: in
          attributes:
            - id: 0
        - id: 0xfc7e  # VOC index
          type: in
          attributes:
            - id: 0

# Devices to poll
devices:
  # VINDSTYRKA Living room
  - ieee: 38:5c:fb:ff:fe:c7:1c:a1
    <<: *ikea_vindstyrka

  # VINDSTYRKA Office
  - ieee: 94:34:69:ff:fe:c1:df:9c
    <<: *ikea_vindstyrka

  # VINDSTYRKA Bedroom
  - ieee: 94:34:69:ff:fe:86:a7:41
    <<: *ikea_vindstyrka

Systemd service & timer for scheduling:

/lib/systemd/system/zha-poll.service

[Unit]
Description=ZHA Poller

[Service]
WorkingDirectory=/path/to/script/dir/
ExecStart=/path/to/python/binary zha-poll.py
RuntimeMaxSec=30
User=some-unpriv-user-on-your-system

/lib/systemd/system/zha-poll.timer

[Unit]
Description=Poll ZHA Attributes every Minute

[Timer]
OnCalendar=*-*-* *:*:00
Persistent=true

[Install]
WantedBy=timers.target

After running the script you should see updated values for your VINDSTYRKA’s in HA, the values are also shown in the log output:

[INFO] Connecting Websocket: ws://172.16.20.100:8040/api/websocket
[INFO] Connected, reading attributes
[INFO] Get attr: {'id': 1, 'ieee': '38:5c:fb:ff:fe:c7:1c:a1', 'endpoint_id': 1, 'cluster_id': 1066, 'cluster_type': 'in', 'attribute': 0}
[INFO] Response: {'id': 1, 'type': 'result', 'success': True, 'result': '1.0'}
[INFO] Get attr: {'id': 2, 'ieee': '38:5c:fb:ff:fe:c7:1c:a1', 'endpoint_id': 1, 'cluster_id': 64638, 'cluster_type': 'in', 'attribute': 0}
[INFO] Response: {'id': 2, 'type': 'result', 'success': True, 'result': '199.0'}

YMMV

2 Likes

That works like a charm, thanks!

Got 2 of these this weekend. I could add them to Deconz, but they were not recognized correctly. Just noticed that Deconz got updated last night :

Changelog
 6.19.0
* Bump deCONZ to 2.21.2

And now it works like a charm. Sensor updates are instant.

Hey, im about to buy some of the new IKEA VINDSTYRKA air quality sensors.

Im wondering if the quality sensors work as zigbee repeater. Can u answer that?

Thanks!

as a mains powered device it also works as a repeater/router, yes.

1 Like

I have installed your quirck :slight_smile:

 custom_zha_quirks git:(master) ✗ ll
total 12K
drwxr-xr-x    2 root     root        4.0K Apr 10 14:34 __pycache__
-rw-r--r--    1 root     root        4.3K Apr 10 14:27 vindstyrka.py

but still no VOC
image

Do i need to wait home assistant being patched ?

Thanks !!

Hi @donny007x

I need your assistant if it’s possible :).

I have install my Ikea Vindstyrka but i can’t show the values pm2_5 and tVOC on HomeAssistant. While i can read the value on deCONZ.

I use a raspberry with raspbee II.

Values i can see on HA

homeassistant_1

Do you have any idea what I need to do?

Best Regards,