I wanted a presence sensor for my office but am cheap, so I ordered a ZG-204ZM from AliExpress. One of the main attractions was that it is USB powered and I am tired of changing batteries, and it does temperature / humidity / luminance as well. The device I thought I ordered is a Zigbee PIR plus 24GHz mmWave sensor that a lot of people seem happy with. What arrived was a ZG-204W, which is its WiFi sibling that talks Tyua and lacks a mmWave radar. This is of course entirely my fault — I had ordered the wrong variant. It also turns out the WiFi version is PIR only, with no mmWave radar at all, so it is a motion sensor rather than a presence sensor. It also has temperature, humidity and light sensors though, so I decided to make the best of it.
The obvious way to run a Tuya WiFi device without the Tuya cloud in the loop is tuya-local, which talks to the device directly on your LAN. The catch is that tuya-local identifies devices by matching their data points (DPs) against a library of device configs, and as of 2026.9.2 there is no config for this one. For reference, product ID is m1ggi8iay3aa8g9u which is also my home password weridly, and the Tuya cloud calls it the charmingly ungrammatical “WiFi PIR Motion & T&H Sensor”. Its best guess for my sensor was a sprinkler controller with a 36% match, which is… not ideal. The generic PIR sensor config does match, but only gives you motion and battery.
The local DPs reported when you add the device look like this:
{
"1": "none",
"4": 100,
"7": 240,
"8": 34,
"10": "60s",
"11": 277,
"101": "ok",
"102": 5,
"103": 6,
"104": 30,
"105": 30,
"106": "c",
"108": 0,
"109": 0
}
So I wrote a config, and by wrote a config I mean “said encouraging things to Claude Opus while it wrote a config”, but you get the general idea. I would normally send that upstream as a pull request, but tuya-local has paused pull requests from non-collaborators while the maintainer works through a backlog, which is entirely fair. Instead, here it is, so that anyone else who searches for this device can use it. I should also note that I bought two of these devices and both of them had absolutely terrible temperature and humidity sensors — I think because the SOC is heating the sensor even when it is shucked from its case. I have therefore removed the temperature and humidity elements from the profile. That is, this is what I am actually using after testing.
One note about power: the sensor takes batteries, but it also has a USB-C port. tuya-local’s documentation points out that battery powered WiFi devices sleep most of the time and are a poor fit for local control, and that is true. I run mine on USB-C with the batteries removed, which keeps it awake and connected permanently, and in that configuration local control has been reliable. If you want to run yours on batteries, I wouldn’t expect this to work well.
Tuya’s cloud only documents four of these (motion, battery, temperature and the PIR keep time). For the rest, I changed one setting at a time in the Smart Life app and watched which DP changed. The ranges below are the ones the app allows.
| DP | What it is | Values |
|---|---|---|
| 1 | Motion | "pir" or "none" |
| 4 | Battery | % |
| 7 | Temperature | °C × 10 |
| 8 | Humidity | % |
| 10 | PIR keep time | 30s, 60s, 120s, 240s, 300s, 600s, 1800s |
| 11 | Illuminance | Lux |
| 101 | Unknown | Not mapped |
| 102 | Temperature report threshold | 0.1 to 5.0 °C (× 10) |
| 103 | Humidity report threshold | 1 to 50 % |
| 104 | Temperature sampling interval | 10 to 3600 seconds, in 10 second steps |
| 105 | Light sampling interval | 1 to 180 minutes |
| 106 | Temperature unit | "c" or "f" |
| 107 | Temperature in °F × 10 | Only reported while the unit is °F, not mapped |
| 108 | Temperature correction | -5.0 to +5.0 °C (× 10) |
| 109 | Humidity calibration | -20 to +20 % |
The cloud only knows about keep times of 30, 60 and 120 seconds, but the device accepts all seven. With a PIR-only sensor, the longer keep times are the useful ones if you want “someone is in the room” rather than “someone just moved”.
In terms of installing the custom config, tuya-local only loads device configs from its own devices directory, so installing this is a matter of putting one file there:
1. Save the YAML below as custom_components/tuya_local/devices/zg204w_motion_sensor.yaml in your Home Assistant config directory.
2. Restart Home Assistant.
3. Add the device through tuya-local as normal (the cloud assisted setup with the Smart Life app’s QR code is the easiest way to get the local key). Because the config lists the product ID, it should be offered as the best match. It is called “Motion sensor” in the device type list.
If you have already added the sensor with a different type, tuya-local can’t change the type of an existing device. Delete the device from tuya-local and add it again.
Be aware that updating tuya-local (through HACS or otherwise) replaces the whole custom_components/tuya_local directory, which will remove this file. Put it back after each update until tuya-local ships a config for this device itself. I manage my custom components from git with a small local overlay, so this happens automatically for me, but that is a story for another post.
name: Motion sensor
products:
- id: m1ggi8iay3aa8g9u
model: ZG-204W
entities:
- entity: binary_sensor
class: motion
dps:
- id: 1
type: string
name: sensor
mapping:
- dps_val: pir
value: true
- dps_val: none
value: false
- entity: sensor
class: battery
category: diagnostic
dps:
- id: 4
type: integer
name: sensor
unit: "%"
class: measurement
- entity: select
name: Keep time
icon: "mdi:camera-timer"
category: config
dps:
- id: 10
type: string
name: option
mapping:
- dps_val: "30s"
value: "30s"
- dps_val: "60s"
value: "60s"
- dps_val: "120s"
value: "120s"
- dps_val: "240s"
value: "240s"
- dps_val: "300s"
value: "300s"
- dps_val: "600s"
value: "10min"
- dps_val: "1800s"
value: "30min"
- entity: sensor
class: illuminance
dps:
- id: 11
type: integer
name: sensor
unit: lx
class: measurement
- entity: number
name: Light sampling interval
category: config
class: duration
icon: "mdi:brightness-5"
dps:
- id: 105
type: integer
name: value
unit: min
range:
min: 1
max: 180
I hope this saves someone, possibly future me, some time.