Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Controlling Tuya devices with cloud API instead of controlling them locally #20

Open
michmike opened this issue Jan 25, 2018 · 126 comments
Open

Comments

@michmike
Copy link

hi there,

First thank you for your valuable contributions to the tuya library. I realize this is a long shot, but i am wondering if anyone had success in calling the tuya cloud API? i used fiddler and i was able to decipher a lot of the information on the calls, but there is one thing missing.

How is tuya calculating the MD5 hash? I was not able to replicate their "sign" parameter to the URL and the details on this as slim. Most of the info is located at https://docs.tuya.com/en/cloudapi/cloud_access.html#access-mode (search for accesskey) but i could not get it to work following the example. (I had to order my parameters, i used localKey as the accessKey, and then i did a utf-8 encoded MD5 hash). For the time, i got it in seconds using Unix epoch time.

Once i complete my work, i will share the PowerShell script that can be replicated into standard http/json requests.

@michmike michmike changed the title Cloud Access Anyone able to call into tuya devices using Cloud Access? Jan 25, 2018
@michmike
Copy link
Author

You have to apply to https://developer.tuya.com/user/cloud to get an accesskey. i will update this thread once i have more details. but if anyone has additional ideas, please let me know

@codetheweb codetheweb changed the title Anyone able to call into tuya devices using Cloud Access? Controlling Tuya devices with cloud API instead of controlling them locally Jan 25, 2018
@codetheweb
Copy link
Owner

I believe the MD5 hash is the same as what's implemented in TuyAPI (see here), although I could be wrong.

@bahorn
Copy link

bahorn commented Jan 26, 2018

In the cloud API sign is generated by:

  • Sorting all the parameters it cares about[1] into alphabetical order by their key names, ignoring null valued ones.
  • Joining them in the form: value1=test||value2=test||...||valuen=test
  • Appending the appSecret to the string (so it becomes value1=test||value2=test||...||appSecret
  • MD5 this string.

There is a special case for the parameter postData, which is done by:

  • MD5'ing the JSON form data
  • rearrange the ASCII encoded MD5 hash in this order: bytes[8:16] + bytes[0:8] + bytes[24:32] + bytes[16:24].

Just before you do the final MD5 of the whole string, it should look like this:
a=<action>||...||postData=<hash>||...||appSecret

[1] ["a", "v", "lat", "lon", "lang", "deviceId", "imei", "imsi", "appVersion", "ttid", "isH5", "h5Token", "os", "clientId", "postData", "time", "n4h5", "sid", "sp"]

@michmike
Copy link
Author

@bahorn thank you for the reply. i will try this as soon as i get the accesskey from tuya.
This is funny though. their instructions at https://docs.tuya.com/en/cloudapi/cloud_access.html#access-mode are incorrect. they do tell you to sort, but they use only one | and the access key is in the front and not the back of the string.

@michmike
Copy link
Author

Also one thing i was not aware of is that the postData is part of the MD5 hash to generate the sign. thank you

@dominicklee
Copy link

@bahorn May I ask where did you get this information for the cloud API sign ?

I just got my accessKey and keySecret from Tuya and I have tried both your method and the instructions on Tuya, that @michmike said. Using PostMan, I tried doing API requests and end up getting the following response after multiple tries:

{
    "t": 1517217462399,
    "success": false,
    "errorCode": "SING_VALIDATE_FALED_4",
    "status": "error",
    "errorMsg": "Parameter or Data Error"
}

If I wish to send postData, for example to turn on my application, could you perhaps share a screenshot or collection in PostMan of what needs to be sent? I've spent hours on this to no avail.

@Ericmas001
Copy link

@dominicklee I just recorded on my old phone (android old enough so i could have a root certificate for fiddler) so i got all the information about resquest&post-data + responses.

I have recorded the following scenarios with the official SmartLife app:

  • Connecting with auto-connect (saved password in app-data) & going all the way to log-out
  • Connecting with mobile-no + password & going all the way to log-out
  • Connecting with mobile-no + sms-code & going all the way to log-out
  • From main menu, removing a device
  • From main menu, adding a device

I will need a little time to format everything (and scramble some personnal info before making it public) but i will make all the info available, maybe tonight (GMT -5) if I have time

The only thing i'm not able to record is the action on the device. If I click to turn it on or off, nothings happens in fiddler, so no HTTP request seams to be made to the server, it must be something else. I checked for UDP on the network and it's not that either. So that's the part I did not figure out !

@bahorn
Copy link

bahorn commented Jan 29, 2018

@dominicklee I stripped down my personal Python implementation that should clear up the issues.
https://gist.github.com/bahorn/9bebbbf37c2167f7057aea0244ff2d92

@Ericmas001 Devices are controlled by MQTT. Use Wireshark to log it, you can set a filter for just MQTT.

Just to explain more of how this works, when you attempt to login you are given:

  • a token
  • A RSA public key
  • An exponent for this key.

You construct the RSA key given and use it to encrypt the MD5(all 128 bits encoded as hex) of your password. This is then padded and then sent on to the server (along with the token). If the login details are correct, you get returned a session ID for use in other mobile requests. Worth noting is the "ecode" and the "p10001" thing that are returned along with the session ID, which you need to login to the MQTT server.

After you login, you should make a call to "tuya.m.device.my.list" to get a list of devices, their uuids and local keys. With this, you can then connect to their MQTT server to issue commands.
MQTT login details are of the form:
Username: pThing+"_"+appKey+"_mb_"+sessionID+md5(md5(appKey)+ecode)[8:24]
Password: MD5(MD5(appSecret)+ecode)[8:24]

@dominicklee
Copy link

I would like to update that I have tried the methods as mentioned by @bahorn and sadly I have received "PERMISSION_DENIED" with the access keys that I have been given. I don't exactly understand why this does not work. Perhaps Tuya is limiting the cloud API access to the respective companies who ordered custom devices.

If anyone got the Tuya cloud REST API working on a no-name (generic) Tuya device, please let me know.

@Ericmas001
Copy link

Ericmas001 commented Jan 30, 2018

https://github.com/Ericmas001/Tuya-Api-Tools/wiki/Requests

I scrambled some info and applied a little formatting to my fiddler outputs (I did not do it manually, of course a little script helped me 😃)
So you can see all the Requests/Responses that were sent.

There is more information than needed, but it's never too much when you try to understand something 😃

@bahorn The appKey and appSecret, can they be seen on those request or taken from existing app, or I really need to apply for one at the Tuya API Team ?

@codetheweb
Copy link
Owner

codetheweb commented Jan 30, 2018

@Ericmas001 wow, that looks like it was a lot of work - thank you. The API keys I thought were sent in HTTP/HTTPS requests to Tuya's servers, but from your requests it looks like that isn't the case.

@dominicklee
Copy link

@Ericmas001 the clientId is actually the accessKey which Tuya app uses. But you will not be able use any Tuya Cloud API without knowing the keySecret, which is technically in the md5 hashed sign.

Unless you can decode the sign, which is virtually impossible, you won't be able to get the keySecret.

And as said, I rightfully requested for a set of API credentials from Tuya and tried them. They apparently do not give me permission to do anything on any generic devices.

@dominicklee
Copy link

Also, one more things you guys should understand is that Tuya devices can be controlled via both MQTT and HTTPS API. However, their app chose to use MQTT for some reason, which explains why actions could not be recorded in an HTTP sniffer.

@bobalob
Copy link

bobalob commented Jan 30, 2018

I've done a bit of wiresharking between my phone, device and tuya cloud using the eFamilyCloud app and I can successfully decode all of the MQTT messages from the device using my device key.

The MD5 example on the Tuya site is not 100% accurate and needs some modification to get the MD5 hash that the site shows out the other end. I think their formatting in the code boxes is a bit messed up.

I can login to the cloud and subscribe to MQTT queues using the mqttfx app and if I send a control action from my phone, I can see it appear in the subscription queue and then decode the message using a PowerShell script I wrote. Wireshark shows your username and password in the MQTT connect message in plain text.

I've tried reversing this and constructing a message to send to the MQTT queue using the same method but unfortunately it just gets dropped by the server. I'm pretty sure that I'm signing the message in the same way that the eFamilyCloud app does so I had got to the point where I thought there must be some set up done over HTTPS before the MQTT conversation starts. My decode script decodes both genuine MQTT data and my own constructed data in the same way.

I've gone as far as exporting a conversation between my phone and the cloud (which successfully controls the device) and then another conversation between mqttfx and the cloud with my encoded data (which doesn't work) and the MQTT conversation looks identical (bar the data and IPs etc.)

It's possible that I'm not encoding the 'data' json part of the message correctly.

When I get a few minutes, I'll tidy up the encode/decode PowerShell scripts and publish them.

Turns out I had a few minutes, here's where I got with the encode/decode

https://github.com/bobalob/PS-Tuya-Tools

@Ericmas001
Copy link

@bobalob I'm a MQTT total noob, so maybe this makes no sence, but does MQTT have some kind of headers, like HttpHeaders, that could be different, something like a userAgent that would be blocked or i don't know ...

@bahorn
Copy link

bahorn commented Jan 31, 2018

@Ericmas001 You can extract them from the many apps, which was discussed early on in #5. In the app I looked at I found the signing process was dumped in the android logs, which includes the AppSecret and AppKey. (Look for "SignRequest" or something on that line.)

I was able to send messages directly to their MQTT server using the paho-mqtt Python library. I just logged in using details I took from packet captures (I only later figured out how usernames/passwords were generated). The app actually supports MQTT over TLS but never uses it for some reason.

Worth noting that you can actually replay messages sent over MQTT as the time is actually ignored (or at least by the devices I used).

@bobalob Are you sending it to the topic "smart/mb/out/"? You code for producing messages seems correct but I don't have a Windows box on me right to verify. The first commented out template is what I've used to control devices.

@dominicklee I actually didn't know the Cloud/App API supported that. I thought it would fall back to it when I forced everything though a HTTP proxy but it never did so I assumed they didn't support it.

I tested the tuya.m.device.dp.publish action yesterday and was able to turn the light on/off.

Just wondering, did you modify my code to use any of the cloud actions listed on https://docs.tuya.com/en/cloudapi/cloudAPI/index.html
I only ever got an "PERMISSION_DENIED" when attempting to use an action that wasn't a mobile one (where my API key was from). I assume cloud Keys can't access mobile ones as well.

@dominicklee
Copy link

@bahorn I'm impressed that you were able to use tuya.m.device.dp.publish to turn lights on/off. I actually requested for cloud keys but for some reason I could not get those to work with the cloudAPI in doing the dp.device publish. See image for details
Tuya screenshot

@dominicklee
Copy link

@bahorn You have successfully got the app access working it seems. However, the cloud access for API is not exactly the same, and that is what I am trying to figure out.

@bahorn
Copy link

bahorn commented Jan 31, 2018

@dominicklee
I just made some progress on getting the cloud API working literally 30 seconds ago.
Turns out they actually were doing something different. (The secret was being added to the start of signing string?!).
Each endpoint is specific to the type of key though.

Just got a call to both tuya.p.weather.city.info.list and tuya.cloud.device.get working.

Code:
https://gist.github.com/bahorn/160b4143badd1b6fae61cec629fce339

@bobalob
Copy link

bobalob commented Jan 31, 2018

@bahorn Yes, I was publishing on the "smart/mb/out/devId" while subscribed to smart/mb/in and another topic pXXXXXX/mb/euXXXXXXXX. I wasn't aware you could replay a message from the phone. I did try that but that also got dropped. Perhaps the application I'm using is doing something weird or I'm missing something.

@bobalob
Copy link

bobalob commented Jan 31, 2018

@Ericmas001 there are headers in the packet for various MQTT controls like message type, flags, QoS, retain etc. I'm setting those all the same as the app does.

@dominicklee
Copy link

@bahorn and @Ericmas001 Thank you guys for your helpful inputs! I have been able to to get Tuya mobile API working using your code examples and hints. Although I still haven't been able to control devices with the Tuya Cloud API, I feel the mobile API would do just as well in terms of controlling devices.

To confirm and clear up any confusion, the mobile API is signed as:
a=tuya.m.device.dp.publish||clientId=<accessKey>||lang=en||os=Android||postData=<your request JSON md5>||sid=<needed for dp actions>||time=<unix time in seconds>||v=1.0||<keySecret>

For mobile, you will need to be logged in (with an SID provided) to perform actions.

While the Tuya Cloud API is signed as:
<keySecret>a=tuya.cloud.device.dp.publish|clientId=<accessKey>|lang=en|os=Linux|postData=<your request JSON md5>|time=<unix time in seconds>|v=1.0

Both API credentials are different. However, even if you request the cloud API credentials for Tuya, they will only allow you to access only the devices you manufacture with them. If you are able to get the API keys for another working app, that may work but it goes beyond the scope of this issue.

@michmike
Copy link
Author

@bahorn have you been able to use the mobile or cloud API to control the devices as well?

I have yet to try what @dominicklee mentioned where you can use the API versus MQTT to control the power plug

@dlashua
Copy link

dlashua commented Feb 15, 2018

@dominicklee I'd love to see your progress. Any chance you have a fork hosted somewhere with these changes?

@dlashua
Copy link

dlashua commented Feb 15, 2018

I like the idea of this library using LOCAL control over Cloud control. Of course, having both options would be ideal, giving the user choice, and providing failover if one isn't available.

Another nice aspect of getting at least SOME of the Cloud API worked out... the Cloud API returns deviceID and localKey for all devices. Having this piece in place would keep people from having to wireshark their keys and would act as a "discovery" mode of sorts.

@codetheweb
Copy link
Owner

@dlashua right, it would be ideal if a user could just sign in with the same username & password and be able to control all their devices.

I'm planning to add cloud control of devices as optional functionality if this ever works. (Meaning, TuyAPI will default to controlling stuff locally unless told otherwise by the user.)

@dlashua
Copy link

dlashua commented Feb 15, 2018

That's perfect! I applied for the Cloud API Key 3 days ago so that I could help get this underway, but I've yet to get info from them and my application still shows as "under review".

It's a shame they didn't make this a little bit easier. Tuya products are EVERYWHERE, they work quite well (as long as you use their permissions laiden apps), and are fairly inexpensive. And all that's "missing" is some documentation, and a way to easily access localKey.

There are several big downsides to the cloud approach as Tuya presents it. As best as I can tell, you'll need to perform a "tuya.cloud.user.sync" for each user that wishes to use the cloud through your key. Additionally, at that point, that cloud account will have access (without a password, from that point on) to that user's devices. So, this means 1) you can't put the Cloud API creds directly in the library or else everyone will have access to everyone else's devices, 2) because of this, a intermediate API will need to be developed for the library to hit, 3) this API will have to be hosted somewhere ($$$) and publically available, 4) users will have to trust this cloud service with their credentials.

Another option will be to require that every user seek their own Cloud API credentials, but, as you can see from my experience, this doesn't cater to the "I want it now" mentality, as I've been waiting three days with no response.

@ghost
Copy link

ghost commented Nov 5, 2019

I haven't tested it, but it looks like Tuya released their docs for their API at some point.

Tuya Docs: https://docs.tuya.com/docDetail?code=K8v0h3gsie1b9

@codetheweb
Copy link
Owner

They've had those up for a while. :)

Although some endpoints do seem more thoroughly documented now.

@ghost
Copy link

ghost commented Nov 5, 2019

You wouldn't happen to know if it costs money to use the API @codetheweb? I am having a tough time figuring that out by looking at their website.

I want to implement a library in C++ for the api, but I don't plan on selling it as a product, so I am only going to do it if it is free.

@codetheweb
Copy link
Owner

The API was free to use, then they added a $2000 paywall, and now it appears it might be free to use again.

It's not clear because it says to contact support to get the the API secret.

If you want to try, create an account on iot.tuya.com and make a new app under App Service.

@sven5
Copy link

sven5 commented Nov 15, 2019

This could be of interest: https://github.com/unparagoned/cloudtuya

@mateusscheper
Copy link

You can request an API user + secret for free.
I got mine and tried to make requests using Postman, but I always get permission denied.

I contacted support for help but I really don't understand what to do.

I'm leaving the entire conversarion here. If anyone have a clue about what do to, please answer me, because I'm in the dark.

Support conversation:

Me 2019-11-12 02:02:02
Hi!

I'm trying to use the open API but I'm getting permission denied on some requests. I've been reading in the docs and I have to set "schema", but I'm using Smart Life App in my phone and Postman in my PC. How can I do this?

Thank you!

Tuya 2019-11-12 11:17:14
hi dear.When you call a device’s related interface and are prompted “Permission denied”, check the two dimensions of permissions following and ensure you are conforming to them.
App dimension: Users linked with devices are the developer’s users on the Tuya Cloud app; developers have indirect permissions to operate the devices of their app users.
Product dimension: the devices used by the products belong to the developer on Tuya Cloud product devices; developers have operation permissions on these devices.

Me 2019-11-12 12:50:36
Hi! Thank you for your reply.

I still don't quite understand.
I'm following this tutorial: https://docs.tuya.com/en/iot/open-api/quick-start/quick-start
Yet I can't make requests. What am I missing?

Thank you.

Tuya 2019-11-12 13:06:23
In brief, the product and app you develop must be under your account, and you can check it from the two dimensions I sent above.Smart Life App is tuya's app. You can't develop it~best wishes

Me 2019-11-13 22:55:19
Hi!

I'm not developing an app. I'm just using the open API to make requests.

Tuya 2019-11-14 10:26:07
hi dear The device API you call must be a product you created on the IOT platform, or it will be prompted “Permission denied”

Me 2019-11-15 13:07:48
Ah, you are saying I need to create an OEM app! I see now!

Well, I did, but it asks to reset my smart plug, but I cannot do this because it is already connected to Smart Life App.
Can I use Smart Life App's schema inside API? I don't want to use my own app for this.

Tuya 2019-11-15 13:18:23
Hello,smart life belongs to tuya, which cannot make API calls from the application dimension. You need to create OEM app or app SDK. best wishes~

Me 31 minutes ago
Hi!

Ok, I created an app. The app's package name is "com.mateus.smartplug", so I set schema in Postman as "mateussmartplug", but I'm still getting permission denied. What am I doing wrong?

Tuya 23 minutes ago
hi dear pls refer to this link:https://docs.tuya.com/en/iot/open-api/tuya-open-platform-access-guide/simple-grant best wishes~

This is what I'm getting:
postman-tuya-open-api

@limkopi78
Copy link

limkopi78 commented Nov 16, 2019 via email

@c3k
Copy link

c3k commented Dec 31, 2019

So if I want to use the Cloud API I can't use Google Assistant with the same device, since the device can't be in two apps at the same time, and a rebranded app will not appear in Google Assistant list. Correct?

@codetheweb
Copy link
Owner

Yes, AFAIK.

@IanAdd
Copy link

IanAdd commented Mar 13, 2020

Is the development of the HA integration still active?
I have a thermostat for underfloor heating (BHT002) which has two temp sensors, air and floor. HA is only seeing one - the air (room) temp. The smart-life app sees two, so does the app which was shipped with the device called my smart thermostat. In addition the thermostat obviously has three states, off/heating and idle. The HA integration only shows off or blank. The temperature value being doubled has been easily solved with a hack to climate.py, but beyond that I am lost as, I assume, the dialog with the cloud is currently not acquiring this extra sensor data.
In hope !!

@nk-gears
Copy link

nk-gears commented May 7, 2020

When i tried to Login and get a key from Tuya Developer Console, it says the following. Looks like they going to replace with a different platform.

"
Cloud API Authorization has been upgraded to SaaS Development Platform,this entrance will be officially closed on 2020-05-30

  1. This upgrade will not affect your existing services. Both the AccessId and AccessKey already obtained can be used normally.
  2. Tuya will create a default SaaS for authorized Tuya SaaS developers to integrate the development rights, including application key, API permissions, message subscriptions, etc.
  3. Any further question, please contact us.Click here
    "

@codetheweb
Copy link
Owner

#303

@banavalikar
Copy link

For people trying to get the Tuya Cloud API working, the advice here 100% works - https://developer.tuya.com/en/docs/iot/open-api/quick-start/quick-start1?id=K95ztz9u9t89n

You have to make 2 calls -

  1. To get a session
  2. Control the LED

Plus one optional call to find out the control API your device supports.

Hope this helps.

@ramonetnet
Copy link

I read a very interesting sentence here : "Tuya devices can be controlled via both MQTT and HTTPS API"
As I see no MQTT traffic, must I deduce my device (Teckin SP21) is managed using HTTPS ?
By the way : where can I find the description of this API ?
Thanks !

@ramonetnet
Copy link

If the MQTT API is used, where can I find the names of the topics ?

@Nigel1992
Copy link

Nigel1992 commented Dec 28, 2020

I can confirm you can control your Tuya devices using HTTP GET/POST with Cloud API [HTTPS].

  1. Create a Developer account at https://developer.tuya.com/en/
  2. Contact Tech Support and ask for a free personal license at https://service.console.tuya.com/
  3. Once you have access the free personal license, go to https://developer.tuya.com/en/docs/iot/open-api/quick-start/quick-start1?id=K95ztz9u9t89n and follow the steps provided there.

A few things to keep in mind...
1.
The API URL has to be changed to your region.
https://openapi.tuyacn.com in China.
https://openapi.tuyaeu.com in Europe.
and so on...

HMAC can be generated using ClientID, current Unix time [in ms] and your Secret as the HMAC Key/Secret.
In order to use actually control a device, you need to generate a new HMAC using ClientID, Access Token, Current Unix Time [in ms], and your Secret as the HMAC Key/Secret.

The script below is to control an RGB Bulb but should be nearly identical for other Tuya products.
Simple use the Get device info on the API to see what your device supports.

GET /v1.0/devices/DEVICEID HTTP/1.1
Host: openapi.tuyaeu.com
client_id: ClientID
access_token: Access Token
sign: Sign
t: Current Unix Time [in ms]
sign_method: HMAC-SHA256

Here's an example of my project:

[SETTINGS]
{
  "Name": "Tuya Example",
  "SuggestedBots": 1,
  "MaxCPM": 0,
  "LastModified": "2020-12-28T17:43:33.8142682+01:00",
  "AdditionalInfo": "",
  "RequiredPlugins": [],
  "Author": "Nigel",
  "Version": "1.2.2",
  "SaveEmptyCaptures": false,
  "ContinueOnCustom": false,
  "SaveHitsToTextFile": false,
  "IgnoreResponseErrors": false,
  "MaxRedirects": 8,
  "NeedsProxies": false,
  "OnlySocks": false,
  "OnlySsl": false,
  "MaxProxyUses": 0,
  "BanProxyAfterGoodStatus": false,
  "BanLoopEvasionOverride": -1,
  "EncodeData": false,
  "AllowedWordlist1": "",
  "AllowedWordlist2": "",
  "DataRules": [],
  "CustomInputs": [],
  "ForceHeadless": false,
  "AlwaysOpen": false,
  "AlwaysQuit": false,
  "QuitOnBanRetry": false,
  "DisableNotifications": false,
  "CustomUserAgent": "",
  "RandomUA": false,
  "CustomCMDArgs": ""
}

[SCRIPT]
#REQUEST_UNIXTIME REQUEST GET "https://openapi.tuyaeu.com/v1.0/token?grant_type=1" 
  

#PARSE_Unixtime PARSE "<SOURCE>" LR ",\"t\":" "}" -> VAR "unixtime" 

#FUNCTION_Sign FUNCTION HMAC SHA256 "YOURSECRETCODEFROMAPP" "YOURCLIENTID<unixtime>" -> VAR "sign" 

#Get_token REQUEST GET "https://openapi.tuyaeu.com/v1.0/token?grant_type=1" 
  
  HEADER "Host: openapi.tuyaeu.com" 
  HEADER "client_id: YOURCLIENTID" 
  HEADER "sign: <sign>" 
  HEADER "t: <unixtime>" 
  HEADER "sign_method: HMAC-SHA256" 

PARSE "<SOURCE>" LR "access_token\":\"" "\"" -> VAR "access token" 

#FUNCTION_Sign FUNCTION HMAC SHA256 "YOURSECRETCODEFROMAPP" "YOURCLIENTID<access token><unixtime>" -> VAR "sign" 

#Random FUNCTION RandomNum "0" "255" -> VAR "random" 

REQUEST POST "https://openapi.tuyaeu.com/v1.0/devices/YOURDEVICEID/commands" 
  CONTENT "{\"commands\":[{\"code\":\"colour_data\",\"value\":\"{\\\"h\\\":<random>,\\\"s\\\":255.0,\\\"v\\\":255.0}\"}]}" 
  CONTENTTYPE "application/json" 
  HEADER "Host: openapi.tuyaeu.com" 
  HEADER "client_id: YOURCLIENTID" 
  HEADER "access_token: <access token>" 
  HEADER "sign: <sign>" 
  HEADER "t: <unixtime>" 
  HEADER "sign_method: HMAC-SHA256" 

The above script is for a program called OpenBullet: https://github.com/openbullet/openbullet
Simply copy the code, save as .loli and copy to the Config folder of OpenBullet.
Edit and have fun!

Incase you use uBot, I also made this in uBot.
Let me know if you want the project.

This was written in a hurry, but feel free to ask any questions! :)

@newdevsa
Copy link

newdevsa commented Feb 5, 2021

Thanks to you all; I am able to login and get the device list and successfully connect to mqtt server as well, using information from this thread.
My subscription to user topic pxxxxxx/mb/inxxxxx and on device topic is successful. I am now receiving messages on the mqtt queue.
I want to capture the camera feed. All my Api calls are working (success), but have no clue how to connect with my camera.
How can i use result from "tuya.m.ipc.config.get" or mqtt information to connect with my camera? I am new on webRTC.
I am not able to wireshark or mitm (for tcp) my mobile app, but was able to decipher http calls with mitm.

@codetheweb
Copy link
Owner

@newdevsa I don't think anyone else has successfully gotten cameras to work.

Repository owner deleted a comment from eliviakamille Feb 19, 2021
@donavanbecker
Copy link

@ground-creative
Copy link

Here is a php client with an example to open a stream for your tuya smart camera from these docs
https://developer.tuya.com/en/docs/iot/rtsp?id=Kacsdjcqllyql

https://github.com/ifsale/tuyapiphp

use ffplay -i rtsps://xxxxxxxxx to stream the link

@panjanek
Copy link

panjanek commented Dec 2, 2021

Hello everyone!
I spent hours figuring out how my thermostate app (MySmartThermostat) communicates with tuya API.
Resources from this thread were very helpful.

But it seems that different apps use different signing and encryption method so I share my findings:

  1. MySmartThermostat uses HMAC-SHA256 for signing, where, to generate signature, the postData is treated differently: it's replaced with rearanged md5 hash as explaned here: https://gist.github.com/bahorn/9bebbbf37c2167f7057aea0244ff2d92
    After that it's HMAC-SHA256 signature with key created by concatenation two secrets:
sign_request = "||".join(out)  #no appsecret here
hmac_key = "A_"+tuya_bmpkey+"_"+tuya_appsecret     # here you have to use secret2 (encoded in the image file) and standard secret
signature = hmac.new(key=hmac_key.encode('utf-8'),msg=feed.encode('utf-8'),digestmod=hashlib.sha256).hexdigest() 
  1. the postData field has to be encrypted with AES in MODE_GCM with 12 bytes of random nonce as prefix and 16 bytes of validation MAC as suffix. The key is derived from request_id using HMAC-SHA256 with key obtained by contatenation of various secret values:
def encryptPostData(postData, requestId):
    #create key from requestid and ecode. ecode is created together with session id upon login, as far as i can see it is valid undefinietly,
    #so it's easier to sniff it than to request it 
    keyparts = "A_"+tuya_bmpkey+"_"+tuya_appsecret+"_"+tuya_ecode     # secret1, secret2 and ecode used here
    #generate key from request_id and secrets
    keyHex = hmac.new(key=requestId.encode('utf-8'),msg=keyparts.encode('utf-8'),digestmod=hashlib.sha256).hexdigest()     
    shortKey = keyHex[0:16].encode('utf-8')   #yes! you use only the first 16 characters of hexadecimal form as AES key
    postDataStr = json.dumps(postData)   
    nonce = os.urandom(12)
    plainBytes = postDataStr.encode('utf-8')   
    encryptedPostData, mac = AES.new(shortKey, AES.MODE_GCM, nonce).encrypt_and_digest(plainBytes)
    encryptedPostDataWithNonce = nonce+encryptedPostData+mac 
    encryptedPostDataBase64 = base64.b64encode(encryptedPostDataWithNonce).decode("utf-8")
    return encryptedPostDataBase64
  1. The same method is used to decrypt the response. In json response, the "result" field is encrypted:
def decryptResult(result, requestId):
    #create key from requestid and ecode
    keyparts = "A_"+tuya_bmpkey+"_"+tuya_appsecret+"_"+tuya_ecode
    #generate key from request_id and ecode
    keyHex = hmac.new(key=requestId.encode('utf-8'),msg=keyparts.encode('utf-8'),digestmod=hashlib.sha256).hexdigest()     
    shortKey = keyHex[0:16].encode('utf-8')
    encryptedBytes = base64.b64decode(result)
    nonce = encryptedBytes[0:12]
    encryptedPayload = encryptedBytes[12:]
    decrypted = AES.new(shortKey, AES.MODE_GCM, nonce).decrypt(encryptedPayload[:-16]) #drop last 16 bytes, it's MAC signature
    return decrypted.decode("utf-8")  

I fund large log file from some device using this mechanism here: http://jira.skyoss.com/secure/attachment/400894/2021-09-16_16-27-45_logcat.log

hope it'll help someone!

@SakshiRathi77
Copy link

I can confirm you can control your Tuya devices using HTTP GET/POST with Cloud API [HTTPS].

  1. Create a Developer account at https://developer.tuya.com/en/
  2. Contact Tech Support and ask for a free personal license at https://service.console.tuya.com/
  3. Once you have access the free personal license, go to https://developer.tuya.com/en/docs/iot/open-api/quick-start/quick-start1?id=K95ztz9u9t89n and follow the steps provided there.

A few things to keep in mind... 1. The API URL has to be changed to your region. https://openapi.tuyacn.com in China. https://openapi.tuyaeu.com in Europe. and so on...

HMAC can be generated using ClientID, current Unix time [in ms] and your Secret as the HMAC Key/Secret. In order to use actually control a device, you need to generate a new HMAC using ClientID, Access Token, Current Unix Time [in ms], and your Secret as the HMAC Key/Secret.

The script below is to control an RGB Bulb but should be nearly identical for other Tuya products. Simple use the Get device info on the API to see what your device supports.

GET /v1.0/devices/DEVICEID HTTP/1.1 Host: openapi.tuyaeu.com client_id: ClientID access_token: Access Token sign: Sign t: Current Unix Time [in ms] sign_method: HMAC-SHA256

Here's an example of my project:

[SETTINGS]
{
  "Name": "Tuya Example",
  "SuggestedBots": 1,
  "MaxCPM": 0,
  "LastModified": "2020-12-28T17:43:33.8142682+01:00",
  "AdditionalInfo": "",
  "RequiredPlugins": [],
  "Author": "Nigel",
  "Version": "1.2.2",
  "SaveEmptyCaptures": false,
  "ContinueOnCustom": false,
  "SaveHitsToTextFile": false,
  "IgnoreResponseErrors": false,
  "MaxRedirects": 8,
  "NeedsProxies": false,
  "OnlySocks": false,
  "OnlySsl": false,
  "MaxProxyUses": 0,
  "BanProxyAfterGoodStatus": false,
  "BanLoopEvasionOverride": -1,
  "EncodeData": false,
  "AllowedWordlist1": "",
  "AllowedWordlist2": "",
  "DataRules": [],
  "CustomInputs": [],
  "ForceHeadless": false,
  "AlwaysOpen": false,
  "AlwaysQuit": false,
  "QuitOnBanRetry": false,
  "DisableNotifications": false,
  "CustomUserAgent": "",
  "RandomUA": false,
  "CustomCMDArgs": ""
}

[SCRIPT]
#REQUEST_UNIXTIME REQUEST GET "https://openapi.tuyaeu.com/v1.0/token?grant_type=1" 
  

#PARSE_Unixtime PARSE "<SOURCE>" LR ",\"t\":" "}" -> VAR "unixtime" 

#FUNCTION_Sign FUNCTION HMAC SHA256 "YOURSECRETCODEFROMAPP" "YOURCLIENTID<unixtime>" -> VAR "sign" 

#Get_token REQUEST GET "https://openapi.tuyaeu.com/v1.0/token?grant_type=1" 
  
  HEADER "Host: openapi.tuyaeu.com" 
  HEADER "client_id: YOURCLIENTID" 
  HEADER "sign: <sign>" 
  HEADER "t: <unixtime>" 
  HEADER "sign_method: HMAC-SHA256" 

PARSE "<SOURCE>" LR "access_token\":\"" "\"" -> VAR "access token" 

#FUNCTION_Sign FUNCTION HMAC SHA256 "YOURSECRETCODEFROMAPP" "YOURCLIENTID<access token><unixtime>" -> VAR "sign" 

#Random FUNCTION RandomNum "0" "255" -> VAR "random" 

REQUEST POST "https://openapi.tuyaeu.com/v1.0/devices/YOURDEVICEID/commands" 
  CONTENT "{\"commands\":[{\"code\":\"colour_data\",\"value\":\"{\\\"h\\\":<random>,\\\"s\\\":255.0,\\\"v\\\":255.0}\"}]}" 
  CONTENTTYPE "application/json" 
  HEADER "Host: openapi.tuyaeu.com" 
  HEADER "client_id: YOURCLIENTID" 
  HEADER "access_token: <access token>" 
  HEADER "sign: <sign>" 
  HEADER "t: <unixtime>" 
  HEADER "sign_method: HMAC-SHA256" 

The above script is for a program called OpenBullet: https://github.com/openbullet/openbullet Simply copy the code, save as .loli and copy to the Config folder of OpenBullet. Edit and have fun!

Incase you use uBot, I also made this in uBot. Let me know if you want the project.

This was written in a hurry, but feel free to ask any questions! :)

Fantastic work!!!!!,would you please tell me how you have generated sign and access token?

@Nigel1992
Copy link

I can confirm you can control your Tuya devices using HTTP GET/POST with Cloud API [HTTPS].

  1. Create a Developer account at https://developer.tuya.com/en/
  2. Contact Tech Support and ask for a free personal license at https://service.console.tuya.com/
  3. Once you have access the free personal license, go to https://developer.tuya.com/en/docs/iot/open-api/quick-start/quick-start1?id=K95ztz9u9t89n and follow the steps provided there.

A few things to keep in mind... 1. The API URL has to be changed to your region. https://openapi.tuyacn.com in China. https://openapi.tuyaeu.com in Europe. and so on...
HMAC can be generated using ClientID, current Unix time [in ms] and your Secret as the HMAC Key/Secret. In order to use actually control a device, you need to generate a new HMAC using ClientID, Access Token, Current Unix Time [in ms], and your Secret as the HMAC Key/Secret.
The script below is to control an RGB Bulb but should be nearly identical for other Tuya products. Simple use the Get device info on the API to see what your device supports.
GET /v1.0/devices/DEVICEID HTTP/1.1 Host: openapi.tuyaeu.com client_id: ClientID access_token: Access Token sign: Sign t: Current Unix Time [in ms] sign_method: HMAC-SHA256
Here's an example of my project:

[SETTINGS]
{
  "Name": "Tuya Example",
  "SuggestedBots": 1,
  "MaxCPM": 0,
  "LastModified": "2020-12-28T17:43:33.8142682+01:00",
  "AdditionalInfo": "",
  "RequiredPlugins": [],
  "Author": "Nigel",
  "Version": "1.2.2",
  "SaveEmptyCaptures": false,
  "ContinueOnCustom": false,
  "SaveHitsToTextFile": false,
  "IgnoreResponseErrors": false,
  "MaxRedirects": 8,
  "NeedsProxies": false,
  "OnlySocks": false,
  "OnlySsl": false,
  "MaxProxyUses": 0,
  "BanProxyAfterGoodStatus": false,
  "BanLoopEvasionOverride": -1,
  "EncodeData": false,
  "AllowedWordlist1": "",
  "AllowedWordlist2": "",
  "DataRules": [],
  "CustomInputs": [],
  "ForceHeadless": false,
  "AlwaysOpen": false,
  "AlwaysQuit": false,
  "QuitOnBanRetry": false,
  "DisableNotifications": false,
  "CustomUserAgent": "",
  "RandomUA": false,
  "CustomCMDArgs": ""
}

[SCRIPT]
#REQUEST_UNIXTIME REQUEST GET "https://openapi.tuyaeu.com/v1.0/token?grant_type=1" 
  

#PARSE_Unixtime PARSE "<SOURCE>" LR ",\"t\":" "}" -> VAR "unixtime" 

#FUNCTION_Sign FUNCTION HMAC SHA256 "YOURSECRETCODEFROMAPP" "YOURCLIENTID<unixtime>" -> VAR "sign" 

#Get_token REQUEST GET "https://openapi.tuyaeu.com/v1.0/token?grant_type=1" 
  
  HEADER "Host: openapi.tuyaeu.com" 
  HEADER "client_id: YOURCLIENTID" 
  HEADER "sign: <sign>" 
  HEADER "t: <unixtime>" 
  HEADER "sign_method: HMAC-SHA256" 

PARSE "<SOURCE>" LR "access_token\":\"" "\"" -> VAR "access token" 

#FUNCTION_Sign FUNCTION HMAC SHA256 "YOURSECRETCODEFROMAPP" "YOURCLIENTID<access token><unixtime>" -> VAR "sign" 

#Random FUNCTION RandomNum "0" "255" -> VAR "random" 

REQUEST POST "https://openapi.tuyaeu.com/v1.0/devices/YOURDEVICEID/commands" 
  CONTENT "{\"commands\":[{\"code\":\"colour_data\",\"value\":\"{\\\"h\\\":<random>,\\\"s\\\":255.0,\\\"v\\\":255.0}\"}]}" 
  CONTENTTYPE "application/json" 
  HEADER "Host: openapi.tuyaeu.com" 
  HEADER "client_id: YOURCLIENTID" 
  HEADER "access_token: <access token>" 
  HEADER "sign: <sign>" 
  HEADER "t: <unixtime>" 
  HEADER "sign_method: HMAC-SHA256" 

The above script is for a program called OpenBullet: https://github.com/openbullet/openbullet Simply copy the code, save as .loli and copy to the Config folder of OpenBullet. Edit and have fun!
Incase you use uBot, I also made this in uBot. Let me know if you want the project.
This was written in a hurry, but feel free to ask any questions! :)

Fantastic work!!!!!,would you please tell me how you have generated sign and access token?

#FUNCTION_Sign FUNCTION HMAC SHA256 "YOURSECRETCODEFROMAPP" "YOURCLIENTID" -> VAR "sign"

#Get_token REQUEST GET "https://openapi.tuyaeu.com/v1.0/token?grant_type=1"

HEADER "Host: openapi.tuyaeu.com"
HEADER "client_id: YOURCLIENTID"
HEADER "sign: "
HEADER "t: "
HEADER "sign_method: HMAC-SHA256"

PARSE "" LR "access_token":"" """ -> VAR "access token"

@nk-gears
Copy link

nk-gears commented Jul 25, 2022

Here a nodejs snippet which helps to connect to tuya cloud and control the device

https://gist.github.com/nk-gears/a185c83d3e521c47d64d252197e87c88

@SakshiRathi77 , it contains the code to generate HMAC signature

@panjanek
Copy link

Here is the full script in python I'm using:

To use it you have to put values into global variables at the beginning.
I extracted most of these values from andoid emulator memory using memory monitor.:
Blue Stack to run android APK on windows: https://www.bluestacks.com/pl/index.html
Cheat Engine, to monitor bluestack memory, look for json field names like "ecode" or "sid": https://www.cheatengine.org/

I use this script as a part of Home Assistant automation.
You have to call API with sid periodically (for example once a day) because it expires otherwise.

import requests
import hashlib
import time
import uuid
import os
import copy
import json
import urllib3
import logging
import hmac
import base64
import string
import sys
from Crypto.Cipher import AES

tuya_appid =     "<digits and numbers>"
tuya_appsecret = "<secret1, digits and numbers>"
tuya_bmpkey =    "<secret2, digits and numbers, extracted from bitmap in android app resources, can be found in memory>"
tuya_endpoint =  "https://a1.tuyaeu.com/api.json"
tuya_sid =       "<session id extracted, after logging in, from andoid app memory using andoid emulator, digits and numbers. this is because i had problems with implementing authorization with login and password. so this is one session, but it stays active as long as in used by api request. after about a month of inactivity it expires>"
tuya_ecode =     "<ecode extracted from andoid app memory using andoid emulator, digits and numbers>"
tuya_gid =       "<digits, seems unimportant>"
tuya_certsign =  "<cert signature as hexadecimal bytes in form XX:XX:XX:XX...>"

urllib3.disable_warnings()

def addTuyaSignature(params):
    if not "sign" in params:
        values_to_hash = ["a", "v", "lat", "lon", "et", "lang", "deviceId", "imei",
                          "imsi", "appVersion", "ttid", "isH5", "h5Token", "os",
                          "clientId", "postData", "time", "n4h5", "sid", "sp", "requestId"]
        sorted_params = sorted(params)
        out = []
        for key in sorted_params:
            if key not in values_to_hash:
                continue
            if params[key] == "":
                continue    
            value = str(params[key])
            if key == "postData":
                h = hashlib.md5()
                h.update(value.encode('utf-8'))               
                value=h.hexdigest()
                value = value[8:16] + value[0:8] + value[24:32] + value[16:24]
            out += [key + "=" + value]

        feed = "||".join(out)
        hmac_key = tuya_certsign + "_"+tuya_bmpkey+"_"+tuya_appsecret
        signature = hmac.new(key=hmac_key.encode('utf-8'),msg=feed.encode('utf-8'),digestmod=hashlib.sha256).hexdigest()       
        params["sign"] = signature
    return params
    
def decryptResult(result, requestId):
    #create key from requestid and ecode
    keyparts = tuya_certsign + "_"+tuya_bmpkey+"_"+tuya_appsecret+"_"+tuya_ecode
    #generate key from request_id and ecode
    keyHex = hmac.new(key=requestId.encode('utf-8'),msg=keyparts.encode('utf-8'),digestmod=hashlib.sha256).hexdigest()     
    shortKey = keyHex[0:16].encode('utf-8')
    encryptedBytes = base64.b64decode(result)
    nonce = encryptedBytes[0:12]
    encryptedPayload = encryptedBytes[12:]
    decrypted = AES.new(shortKey, AES.MODE_GCM, nonce).decrypt(encryptedPayload[:-16])
    return decrypted.decode("utf-8")     

def encryptPostData(postData, requestId):
    #create key from requestid and ecode
    keyparts = tuya_certsign + "_"+tuya_bmpkey+"_"+tuya_appsecret+"_"+tuya_ecode
    #generate key from request_id and ecode
    keyHex = hmac.new(key=requestId.encode('utf-8'),msg=keyparts.encode('utf-8'),digestmod=hashlib.sha256).hexdigest()     
    shortKey = keyHex[0:16].encode('utf-8')
    postDataStr = json.dumps(postData)   
    nonce = os.urandom(12)
    plainBytes = postDataStr.encode('utf-8')   
    encryptedPostData, mac = AES.new(shortKey, AES.MODE_GCM, nonce).encrypt_and_digest(plainBytes)
    encryptedPostDataWithNonce = nonce+encryptedPostData+mac 
    encryptedPostDataBase64 = base64.b64encode(encryptedPostDataWithNonce).decode("utf-8")
    return encryptedPostDataBase64

def callTuyaApi(action, postData):
    time_param = int(time.time())
    request_id = str(uuid.uuid4())    
    params = {
               "a":action,
               "appRnVersion":"5.44",
               "appVersion":"3.33.5",
               "channel":"oem",
               "clientId":tuya_appid,
               "deviceCoreVersion":"3.29.5",
               "deviceId":"f1cf817055401e82b60fa5f74d8779e64133a59215b7",
               "et":"3",
               "lang":"pl_PL",
               "os":"Android",
               "osSystem":"7.1.1",
               "platform":"ONEPLUS A5000",
               "requestId":request_id,
               "sdkVersion":"3.29.5",
               "sid":tuya_sid,
               "time": str(time_param),
               "timeZoneId":"Europe/Warsaw",
               "ttid":"sdk_tuya_international",
               "v":"1.0" ,
               "gid": tuya_gid
            }

    #print(postData)
    params["postData"] = encryptPostData(postData, request_id)
    params = addTuyaSignature(params)    
    headers = { 'User-Agent' : 'Android/com.google.android.gms/203615023 (OnePlus5 NMF26X)' }   
    #print(params)
    response = requests.post(tuya_endpoint, params=params, headers=headers, verify=False)
    encryptedResult = response.json()["result"]       
    encryptedBytes = base64.b64decode(encryptedResult)
    jsonStr = decryptResult(encryptedResult, request_id)
    return json.loads(jsonStr)

def getDeviceDetails(devId):
    response = callTuyaApi("tuya.m.device.get", { "devId" :devId})
    #print(json.dumps(response, indent=4, sort_keys=True,ensure_ascii=False))
    dps = response["result"]["dps"]
    result = {}
    result["id"] = response["result"]["devId"]
    result["online"] = response["result"]["isOnline"]
    result["active"] = response["result"]["isActive"]
    result["localKey"] = response["result"]["localKey"]
    result["name"] = response["result"]["name"]
    for key in dps:
        result["dps_"+key] = dps[key]
    if "1" in dps and isinstance(dps["1"], bool):
        result["1"] = dps["1"]
    if "2" in dps and isinstance(dps["2"], bool):
        result["2"] = dps["2"]
    if "3" in dps and isinstance(dps["3"], bool):
        result["3"] = dps["3"]
    if "7" in dps and dps["7"]!=0:
        result["usb"] = dps["7"]
    if "20" in dps:
        result["voltage"] = float(dps["20"]) / 10.0
    if "18" in dps:
        result["current"] = float(dps["18"]) / 1000.0
    if "19" in dps:
        result["power"] = float(dps["19"]) / 10.0
    return result

if __name__ == "__main__":
    if len(sys.argv) <= 1:
        #print("usage: python tuyasmart.py <device-id> [1|2|3|usb] [on|off]")
        #print("devices:")
        devices = callTuyaApi("tuya.m.device.ext.prop.list", {})
        all = []
        for dev in devices["result"]:
            devId = dev["devId"]
            device = getDeviceDetails(devId)
            all.append(device)
        print(json.dumps(all, indent=4, sort_keys=True,ensure_ascii=False))
    elif len(sys.argv) == 2: 
        devId = sys.argv[1]
        device = getDeviceDetails(devId)
        print(json.dumps(device, indent=4, sort_keys=True,ensure_ascii=False))
    else: 
        devId = sys.argv[1]
        socket = sys.argv[2]
        if len(sys.argv) == 3:
            device = getDeviceDetails(devId)
            s = "1" if device[socket] else "0"
            print(s)
        else:   
            state = sys.argv[3]
            if state == "on" or state == "off":
                if socket == "usb":
                    socket = "7"
                postData = {}
                postData["devId"] = devId
                postData["dps"] = {}
                postData["dps"][str(socket)] = state == "on"
                response = callTuyaApi("tuya.m.device.dp.publish", postData)
                print(json.dumps(response, indent=4, sort_keys=True,ensure_ascii=False))
            elif state == "set":
                value = sys.argv[4]
                if value == "true":
                    value = True
                elif value == "false":
                    value = False
                elif value.isnumeric():
                    value = int(value)
                postData = {}
                postData["devId"] = devId
                postData["dps"] = {}
                postData["dps"][str(socket)] = value
                response = callTuyaApi("tuya.m.device.dp.publish", postData)
                print(json.dumps(response, indent=4, sort_keys=True,ensure_ascii=False))
                

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests