Tuya WiFi Smart Plug

From Leo's Notes
Revision as of 05:06, 4 May 2020 by Leo (talk | contribs)
This page was last edited on 4 May 2020, at 05:06.

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. This article goes over the steps on how to use these plugs without them needing to connect to the internet. Unfortunately, the initial setup process still requires the plugs to go online.

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:

  1. The device IP address
  2. UUID / Device ID
  3. localKey
Tuya Cloud Development API Key

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'
  }
]

Self Hosted

You can use Node-RED and the tuya-smart nodes to control the Tuya Smart Plugs. Refer to the Node-RED page on installing Node-RED and then install the node-red-contrib-tuya-smart npm package on the Node-RED server. The Tuya node should appear in Node-RED.

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:

NodeRED Tuya Plug setup
NodeRED Tuya Plug setup


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}}

See Also