Tuya WiFi Smart Plug
Tuya has some cheap WiFi capable smart plugs which you can control remotely using your phone. These smart plugs will need to talk with Tuya's servers in order to work. There are alternative firmware that can be loaded over the air (OTA) on the device.
Tuya Setup
The initial setup with Tuya requires the Tuya app or the TuyAPI command line in conjunction with the Tuya Cloud.
When the smart plugs are initialized for the first time, a randomized AES key is generated which is required in order to interface with the device. You will need the following 3 things in order to control a plug:
- The device IP address
- UUID / Device ID
- localKey

Follow the instructions at https://github.com/codetheweb/tuyapi/blob/master/docs/SETUP.md. You will need to obtain a Cloud Development Project API key which you can then use with the tuya-cli tool to register the device with Tuya's servers and then obtain the localKey.
$ npm install @tuyapi/cli
$ ~/node_modules/.bin/tuya-cli link --api-key ******************** --api-secret ******************************** --schema steamrdemo --ssid "WiFi Network" --password "Password" --region us
✔ Device(s) registered!
[
{
id: '********************',
ip: 'X.X.X.X',
localKey: 'cf39acf048ca0762',
name: 'SP25'
}
]
Using Tuya-Convert
The Tuya-Convert project simplifies the flashing of the Tuya smart plugs. It turns a linux computer with a wireless adapter into an access point and bundles all the necessary services (dns, http, mqtt) such that a smart plug in setup mode will automatically be found and flashed. During the flashing procedure, you can choose to use either espurna or tasmota.
After choosing Tasmota, the device should reboot and it should show up as an open access point. Connect to it and configure the WiFi settings. It will reboot once more and connect to your network. This step takes a few minutes. After it comes back up, you need to update its firmware to the latest version via the web interface. Use the default update server URL to pull the OTA firmware. The device will reboot after it is done, but this step takes a few minutes.
See the project website at https://github.com/ct-Open-Source/tuya-convert.
Node-RED Integration
You can use Node-RED and MQTT to control the tuya smart plugs. While there are existing libraries and nodes that can help facilitate communication with the plugs, these libraries appear to rely on the HTTP mechanism which appears to be very unreliable. Issues I've encountered while using these libraries include unreliable triggering (where calls to turn on or off a plug are missed), or where triggering causes some undefined behavior (such as the plug resetting, disconnecting from WiFi, or toggling the switch rather than turn on or off). I was able to get reliable triggering after changing the flows to use MQTT exclusively.
MQTT
The Tuya smart plugs will attempt to connect to a MQTT server at mq.gw.tuyaus.com on port 1883. You can either use this server or override the DNS entry within your network to point to a self-hosted MQTT server.
Commands should be sent to the topic smart/device/in/$DEVICE_ID. The message should conform to the typical Tuya format described below.
The following function node will convert a JSON payload into the Tuya format that the plug will understand.
var CryptoJS = global.get("CryptoJS");
// mqtt topic
msg.topic = "smart/device/in/" + msg.deviceid;
// the payload data (time and device id)
msg.payload.t = Math.round((new Date()).getTime() / 1000);
msg.payload.data.devId = msg.deviceid;
// payload as json
text = JSON.stringify(msg.payload);
// encrypt json payload
t = CryptoJS.AES.encrypt(text, CryptoJS.enc.Utf8.parse(msg.localkey),
{ mode: CryptoJS.mode.ECB, keySize: 128 });
cyphertext = t.toString();
// generate md5 hash
hash = "data=" + cyphertext + "
The plug will automatically send an update to the topic smart/device/out/$DEVICE_ID. Plugs with additional capabilities such as power monitoring will also report these values to this topic. The message generated by the plug also conforms to the typical Tuya format.
The following function node will decrypt the message from a plug into a JSON message.
var CryptoJS = global.get("CryptoJS");
key = CryptoJS.enc.Utf8.parse(msg.localkey);
cypher = msg.payload.substring(19);
e = CryptoJS.AES.decrypt(cypher, key,
{ mode: CryptoJS.mode.ECB, keySize: 128 });
msg.payload = CryptoJS.enc.Utf8.stringify(e).toString();
return msg;
TuyAPI
TuyAPI calls are wrapped by the tuya-smart contrib package. This Node-RED package will provide a Tuya node that can be added to your flows to interact with a smart plug. The TuyAPI library appears to only use the HTTP call which I have found to be more unreliable than using MQTT.
To get started, install (node-red-contrib-tuya-smart) on the Node-RED server. If using docker, install it in the Dockerfile.
By default, the Tuya node will run the default command that is configured in the node ({"schema": true} which polls all status information). You can override the command by pushing a message into the node's input. To turn the plug on, send {"set": true, "dpsIndex": 1} as a payload to the tuya node input. Refer to my reverse engineering notes in the section below for the other DPS values relevant to the AWP04L model. You may wish to convert the DPS index values into named values using a function node as well. What I ended up doing was:
msg.payload.status = {
"state": msg.payload.data.dps[1],
"timer": msg.payload.data.dps[2],
"amps": msg.payload.data.dps[4] / 1000.0,
"watts": msg.payload.data.dps[5] / 10.0,
"volts": msg.payload.data.dps[6] / 10.0,
}
delete msg.payload.data;
return msg;
The flow I ended up with looks something like this:
Reverse Engineering
These Tuya smart plugs are hard coded to these servers:
- mq.gw.tuyaus.com via MQTT
- a.tuyaus.com via HTTP
To self host these services, you will need to hijack the DNS records to your self hosted service.
MQTT communication is encrypted with 128-bit AES ECB using the localKey as the encryption key. The topic is smart/device/out/device_id. Because it is using ECB, similar messages will have similar cipher texts as seen below.
smart/device/out/02200194dc4f22135606 2.1f9d6aaa8c028751dtahaVgb6sZEC2uRNfToAVHb+qE94qh+hk1R7VkGt0djgWetQX1e4IxSvtQYLfwBhOA9eLsEfLfBdgpfZV3z0kkeUENQM7x/OcUTnhv31fBPFcxdihiAfGBTUgtG1OUjL
smart/device/out/02200194dc4f22135606 2.1468d26b81e3787aatahaVgb6sZEC2uRNfToAVHb+qE94qh+hk1R7VkGt0djgWetQX1e4IxSvtQYLfwBhOA9eLsEfLfBdgpfZV3z0kkeUENQM7x/OcUTnhv31fBPK039bcLeTGse/67AOz6Lm
smart/device/out/02200194dc4f22135606 2.1c320cd2dfa2d20e1tahaVgb6sZEC2uRNfToAVHb+qE94qh+hk1R7VkGt0djgWetQX1e4IxSvtQYLfwBhOA9eLsEfLfBdgpfZV3z0kkqeR4eguTTZfmoDnLO6Gsv8UBqGAblVCndgzJmUDrlO
smart/device/out/02200194dc4f22135606 2.168c2d8f05211a108tahaVgb6sZEC2uRNfToAVMhq/cediwGM0WFFnEJ4kvvgWetQX1e4IxSvtQYLfwBhOA9eLsEfLfBdgpfZV3z0khHz8nKhR2hdXlCbUYtj/GjX0qFqGa/6s3uSB36TRSSE
smart/device/out/02200194dc4f22135606 2.15447b84b25063944tahaVgb6sZEC2uRNfToAVOiDIlXh48IBRomThtqXzoDnn+1z5MCcHEIrwfVuPfvgpu5AYqA77Ys1ADM0zpHXV6YJLnpWJXghRTcedNpbhfpkK5IMk+UvEQNenmF0sduA
smart/device/out/02200194dc4f22135606 2.1f5542a52dabfcf73tahaVgb6sZEC2uRNfToAVOiDIlXh48IBRomThtqXzoDnn+1z5MCcHEIrwfVuPfvgpu5AYqA77Ys1ADM0zpHXV0RQlDrQca4knVjDakm02aatECxAMdcGsMmLjUtCk0mr
smart/device/out/02200194dc4f22135606 2.15ef756451e1db8aatahaVgb6sZEC2uRNfToAVGtwt9PXVjhurww9gAAsVUXnn+1z5MCcHEIrwfVuPfvgpu5AYqA77Ys1ADM0zpHXV6YJLnpWJXghRTcedNpbhfqVhbDB6i1efHyNUnWaDsr/
smart/device/out/02200194dc4f22135606 2.15c4fe47ade0801e7tahaVgb6sZEC2uRNfToAVGtwt9PXVjhurww9gAAsVUXnn+1z5MCcHEIrwfVuPfvgpu5AYqA77Ys1ADM0zpHXV0RQlDrQca4knVjDakm02abAea+OhxGULiAXXFiilhMA
The payload in plain text looks like this:
{"protocol":4,"t":27,"data":{"devId":"02200194dc4f22135606","dps":{"1":true,"2":0}},"s":9}
The device also phones home via HTTP. It appears that blocking HTTP completely will render the device somewhat crippled. Certain features such as voltage / wattage are unavailable.
A few captured requests are shown below.
POST /gw.json?a=atop.online.debug.log&gwId=0220xxxxxxxxxxxxxx606
GET
Array
(
[a] => atop.online.debug.log
[gwId] => 0220xxxxxxxxxxxxxx606
[t] => 1317
[sign] => 0214c93ac7dc7d6f3834d6dfda475e3a
)
POST
Array
(
[data] => D37735B04A...
)
Headers:
Content-Type: application/json;charset=UTF-8'
Content-Length: 56'
Content-Language: zh-CN'
Server: Tuya-Sec'
User-Agent: Go-http-client/1.1
Response from Server:
{"result":true,"t":1566787618,"e":false,"success":true}
POST /gw.json?a=s.gw.dev.timer.count&gwId=0220xxxxxxxxxxxxxx606
GET
Array
(
[a] => s.gw.dev.timer.count
[gwId] => 0220xxxxxxxxxxxxxx606
[t] => 7614
[sign] => 2d9833c929f652d72a905d1459ea6b7a
)
POST
Array
(
[data] => D377...
)
Headers:
Content-Type: application/json;charset=UTF-8'
Content-Length: 56'
Content-Language: zh-CN'
Server: Tuya-Sec'
User-Agent: Go-http-client/1.1
Response from Server:
{"result":{"devId":"0220xxxxxxxxxxxxxx606","count":0,"lastFetchTime":0},"t":1566787629,"e":false,"success":true}
tuya python
The status output from the AWP04L smart plug looks like this. The plug has monitoring features and should return the voltage, amps, and watts.
{u'devId': u'02200194dc4f22137d7f', u'dps': {
u'1': True, outlet status
u'2': 0, timer, decrements by 5, in seconds
u'5': 56, watts (5.6)
u'4': 73, amps (0.073 amps)
u'6': 1198 volts
}}
{u'devId': u'02200194dc4f22135606', u'dps': {u'1': True, u'2': 0, u'5': 187, u'4': 176, u'6': 1190}}
I don't understand why amps * volts != watts...
You will probably still need to allow it to connect to the cloud once. If you don't let it completely connect, some features like voltage/wattage are disabled from the output (?). MQTT must be working, or else these features are disabled:
{u'devId': u'02200194dc4f22135606', u'dps': {u'1': True, u'2': 0}}
>>> x.status()
{u'devId': u'02200194dc4f22135606', u'dps': {u'1': True, u'2': 0}}
>>> x.status()
{u'devId': u'02200194dc4f22135606', u'dps': {u'1': True, u'2': 0}}
>>> x.status()
{u'devId': u'02200194dc4f22135606', u'dps': {u'1': True, u'2': 0, u'5': 0, u'4': 0, u'6': 1184}}
>>> x.status()
{u'devId': u'02200194dc4f22135606', u'dps': {u'1': True, u'2': 0, u'5': 0, u'4': 0, u'6': 1184}}
>>> x.status()
{u'devId': u'02200194dc4f22135606', u'dps': {u'1': True, u'2': 0, u'5': 0, u'4': 0, u'6': 1184}}
