IKEA sensors as Zigbee2MQTT
Simulate an IKEA VINDSTYRKA air quality sensor and a PARASOLL door sensor the way Zigbee2MQTT publishes them.
IKEA's VINDSTYRKA air quality sensor and PARASOLL door and window sensor speak Zigbee, not MQTT. What reaches your broker is what Zigbee2MQTT, the bridge between a Zigbee network and MQTT, publishes on their behalf. So if your dashboard or backend reads these sensors through Zigbee2MQTT, you simulate Zigbee2MQTT's messages, and the format comes from Zigbee2MQTT's documentation, not from IKEA.
The format
Zigbee2MQTT publishes two kinds of messages per device:
- State: one JSON object with all of the device's values on
zigbee2mqtt/<friendly_name>. The friendly name is the device's IEEE address unless you give it a name, andzigbee2mqttis the default base topic. See MQTT topics and messages. - Availability:
{"state":"online"}or{"state":"offline"}onzigbee2mqtt/<friendly_name>/availability, as a retained message. Zigbee2MQTT publishes it only when device availability is turned on in its configuration. See Device availability.
The keys of the JSON object depend on the device. The device pages list them:
- E2112 VINDSTYRKA:
temperature(°C),humidity(%),pm25(µg/m³) andvoc_index. - E2013 PARASOLL:
contact(trueorfalse),battery(%) andvoltage(mV).
The project
# IKEA Zigbee sensors as Zigbee2MQTT publishes them (see zigbee2mqtt.io device pages
# E2112 VINDSTYRKA and E2013 PARASOLL): state on zigbee2mqtt/<name>, availability as JSON.
[broker]
host = "localhost"
port = 1883
[[templates]]
id = "ikea-vindstyrka"
interval = "10s"
availability = { topic = "zigbee2mqtt/{{ device_id }}/availability", online = '{"state":"online"}', offline = '{"state":"offline"}' }
[[templates.messages]]
topic = "zigbee2mqtt/{{ device_id }}"
payload = '{"temperature": {{ temperature | round(1) }}, "humidity": {{ humidity | round | int }}, "pm25": {{ pm25 | round | int }}, "voc_index": {{ voc_index | round | int }}, "linkquality": {{ linkquality | round | int }}}'
[templates.fields.temperature]
generator = "sine"
center = 22
amplitude = 1
period = "6h"
noise = 0.1
[templates.fields.humidity]
generator = "random_walk"
start = 45
min = 30
max = 70
step = 1
[templates.fields.pm25]
generator = "random_walk"
start = 5
min = 0
max = 60
step = 2
[templates.fields.voc_index]
generator = "random_walk"
start = 100
min = 50
max = 300
step = 5
[templates.fields.linkquality]
generator = "uniform"
min = 90
max = 160
[[templates]]
id = "ikea-parasoll"
interval = "30s"
availability = { topic = "zigbee2mqtt/{{ device_id }}/availability", online = '{"state":"online"}', offline = '{"state":"offline"}' }
[[templates.messages]]
topic = "zigbee2mqtt/{{ device_id }}"
payload = '{"contact": {{ contact | tojson }}, "battery": {{ battery | round | int }}, "voltage": {{ voltage | round | int }}, "linkquality": {{ linkquality | round | int }}}'
[templates.fields.contact]
generator = "sequence"
values = [true, true, false, true]
[templates.fields.battery]
generator = "constant"
value = 87
[templates.fields.voltage]
generator = "uniform"
min = 2900
max = 3000
[templates.fields.linkquality]
generator = "uniform"
min = 60
max = 140
[[devices]]
id = "living_room_air"
template = "ikea-vindstyrka"
instance_id = "{{ id }}"
autostart = true
[[devices]]
id = "front_door"
template = "ikea-parasoll"
instance_id = "{{ id }}"
autostart = true
- There is one template per device model,
ikea-vindstyrkaandikea-parasoll, and one device of each:living_room_airandfront_door. {{ device_id }}in a topic is the instance ID. Each device has one instance, andinstance_id = "{{ id }}"makes the instance ID equal to the device ID, so the device ID plays the part of the friendly name:zigbee2mqtt/living_room_air.availabilitygives the topic and the exact JSON payloads Zigbee2MQTT uses. Senzor publishes theonlinepayload when an instance connects and theofflinepayload when it stops, both retained, like Zigbee2MQTT. It also registers theofflinepayload as the instance's Last Will (see Availability and Last Will).- The payload is a JSON object written out in the template.
round | intturns a generated number into a whole number, andround(1)keeps one decimal.contact | tojsonwrites the boolean astrueorfalse. - The values come from generators (see Values and generators):
temperaturefollows a slow wave around 22 °C with a period of 6 hours, plus a little noise.humidity,pm25andvoc_indextake random steps within their limits.contactrepeats the sequencetrue,true,false,true.batterystays at 87, andvoltageis a random value between 2900 and 3000.linkqualityis a random value in its range, drawn anew for every message.
- VINDSTYRKA publishes every 10 seconds, PARASOLL every 30 seconds.
Run it
Start a broker as in the Quick start, then run the project in the GUI or the CLI:
senzor examples/ikea-zigbee2mqtt.tomlsenzor-cli run examples/ikea-zigbee2mqtt.tomlIn another terminal, subscribe to everything under zigbee2mqtt/. With the broker container from the Quick start:
docker exec mosquitto mosquitto_sub -v -t 'zigbee2mqtt/#'The output of a 35-second run looks like this; your values differ:
zigbee2mqtt/living_room_air/availability {"state":"online"}
zigbee2mqtt/front_door/availability {"state":"online"}
zigbee2mqtt/front_door {"contact": true, "battery": 87, "voltage": 2957, "linkquality": 140}
zigbee2mqtt/living_room_air {"temperature": 22.1, "humidity": 45, "pm25": 5, "voc_index": 100, "linkquality": 120}
zigbee2mqtt/living_room_air {"temperature": 22.0, "humidity": 46, "pm25": 3, "voc_index": 101, "linkquality": 105}
zigbee2mqtt/living_room_air {"temperature": 22.1, "humidity": 45, "pm25": 4, "voc_index": 98, "linkquality": 140}
zigbee2mqtt/living_room_air/availability {"state":"offline"}
zigbee2mqtt/front_door/availability {"state":"offline"}The last two lines appear when you stop the run. Because availability messages are retained, a subscriber that connects later gets the last online or offline of each device first; if you ran the project before, you see the old offline messages before anything else.
Differences from a real bridge
Interval instead of change. A real PARASOLL reports when its contact changes. Senzor 1.0.0 publishes every message of a template on every tick, so front_door publishes its full state every 30 seconds, whether contact changed or not. To change the contact at a moment you choose, pin contact to true or false in the instance view; the next tick publishes the pinned value.
Offline at once. Zigbee2MQTT marks a device offline only after it has missed its check-ins: by default 10 minutes for mains-powered devices and 25 hours for battery-powered ones. A Senzor instance publishes offline the moment you stop it, and its Last Will arrives as soon as you kill it. See Simulating a device dropping off.
Device topics only. The project simulates the devices' own topics. It does not publish Zigbee2MQTT's bridge topics under zigbee2mqtt/bridge/.
Adapt it
- Your friendly names: change the device
id, for example tohallway_air. The topics follow, because they use{{ device_id }}. - More sensors of the same model: add a
[[devices]]entry with anotheridand the sametemplate. For many identical sensors, usecountwith a pattern such asinstance_id = "air_{{ index }}"(see A fleet of sensors). - Other keys or devices: copy a real message from your bridge with
mosquitto_sub -v -t 'zigbee2mqtt/#'(ordocker exec <container> mosquitto_sub -v -t 'zigbee2mqtt/#'if your broker is Mosquitto in Docker), paste it as the payload and replace each value with a field, as in{{ temperature | round(1) }}. Generators lists the generators you can use for the fields, and Template variables the variables and filters.