Tuya WiFi Smart Plug: Difference between revisions
Reorganized content in two sections: stock and modified firmware |
|||
| Line 32: | Line 32: | ||
===MQTT Interaction=== | ===MQTT Interaction=== | ||
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. | 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. The device subscribes to the topic {{code|smart/device/in/$DEVICE_ID}}; encrypted commands published will be processed by the device. Encrypted messages generated by the plug will be published to the topic {{code|smart/device/out/$DEVICE_ID}}. | ||
All messages sent to and received from the plug must be JSON and encrypted using the AES key that was generated when the device was first set up also known as the {{code|localKey}}. The steps to encrypt a message are: | |||
# JSON payload must be minimized with extra spaces removed | |||
# The JSON message is then encrypted using AES ECB using the 128bit AES localKey. | |||
# A MD5 hash of {{code|1=data=$CypherText||pv=2.1||$LocalKey}} is computed. | |||
# The final message concats {{code|"2.1" + MD5[8, 24] + $CypherText}} | |||
A message payload sent to the Tuya plug looks something like the following | |||
{{highlight|lang= | {{highlight|lang=json|code= | ||
{ | |||
"protocol": 5, | |||
"t": 0, // set to current unix timestamp | |||
"data": { | |||
"devId": "", | |||
"dps": { // dps values trigger specific GPIO pins | |||
"1": true, | |||
"7": true, | |||
"11": 0, | |||
"101": 0 | |||
} | |||
} | |||
}}} | |||
Messages generated by the plug will be sent to the topic {{code|smart/device/out/$DEVICE_ID}} and will be encrypted with the same algorithm outlined above. Plugs with additional capabilities such as power monitoring will also report these values to this topic multiple times per minute. | |||
=== Node-RED integration with MQTT === | === Node-RED integration with MQTT === | ||
Revision as of 16:25, 20 May 2020
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.
Usage with stock Tuya firmware
The stock firmware requires setup with the Tuya app or with TuyAPI in combination with the Tuya Cloud. There is no way to set up the device without having it connect to Tuya's servers. If this is a concern, skip to the Tuya-Convert section below to change the stock firmware.
As part of the initial set up, a randomized AES key is generated and is shared with Tuya's servers. Your phone's SmartLife app also obtains this key when you log in.
The following three pieces of information is required to control a device using the Tuya firmware.:
- The device IP address
- UUID / Device ID
- localKey - AES 128 ECB Key

Retrieving this AES key after the initial set up has been difficult (as of 2020) because traffic between the plugs and Tuya are encrypted; traffic between Tuya and the SmartLife app is encrypted and SSL interception isn't possible; and the data stored by the SmartLife app is also encoded or encrypted. If you wish to obtain the AES key, the only method I had success with was to use the TuyAPI project.
The TuyAPI project has instructions on obtaining the AES key at https://github.com/codetheweb/tuyapi/blob/master/docs/SETUP.md. After obtaining the tuya-cli tool, you will need to obtain a Cloud Development Project API key from Tuya which is required to register the device and obtain the AES key when the device is set up.
$ 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'
}
]
MQTT Interaction
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. The device subscribes to the topic smart/device/in/$DEVICE_ID; encrypted commands published will be processed by the device. Encrypted messages generated by the plug will be published to the topic smart/device/out/$DEVICE_ID.
All messages sent to and received from the plug must be JSON and encrypted using the AES key that was generated when the device was first set up also known as the localKey. The steps to encrypt a message are:
- JSON payload must be minimized with extra spaces removed
- The JSON message is then encrypted using AES ECB using the 128bit AES localKey.
- A MD5 hash of
is computed. - The final message concats
"2.1" + MD5[8, 24] + $CypherText
A message payload sent to the Tuya plug looks something like the following
{
"protocol": 5,
"t": 0, // set to current unix timestamp
"data": {
"devId": "",
"dps": { // dps values trigger specific GPIO pins
"1": true,
"7": true,
"11": 0,
"101": 0
}
}
}
Messages generated by the plug will be sent to the topic smart/device/out/$DEVICE_ID and will be encrypted with the same algorithm outlined above. Plugs with additional capabilities such as power monitoring will also report these values to this topic multiple times per minute.
Node-RED integration with MQTT
You can use Node-RED and MQTT to control the Tuya smart plugs. I was able to get reliable triggering after changing the flows to use MQTT exclusively.
Node-RED integration with TuyAPI
If you don't want to use MQTT, you can still control a Tuya plug using the TuyAPI package which uses a HTTP API. I have found this to be more unreliable than MQTT and I do not recommend this method. 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). For Node-RED integration, use the tuya-smart contrib package which provides nodes that interfaces with the TuyAPI library.
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:
Flashing a new firmware with 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.
Teckin SP25:
{"NAME":"Teckin SP25","GPIO":[56,255,255,255,255,255,0,0,22,17,255,21,255],"FLAG":1,"BASE":18}
AWP04L:
{"NAME":"AWP04L","GPIO":[57,255,255,131,255,134,0,0,21,17,132,56,255],"FLAG":0,"BASE":18}
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}}
