Extract the Device ID and LocalKey from Intelligent Air APP Tuya 3.5 WiFi device (Airmart, Haier, Gree-Tuya, TCL, Galanz, Midea OEM, etc.) using a reproducible Frida + Android emulator method.
No root required.
No MITM.
No firmware hacking.
No cloud access needed.
This method works for devices locked to the Haier/Thingclips cloud which do not expose their local keys through Tuya IoT. Probably will work with other apps that use ThingClips with little or no modification.
USE AT YOUR OWN RISK
-
Works with Tuya 3.5 (encrypted ECDH session)
-
Dumps devId and localKey directly from the official app
-
Compatible with:
- Haier-based Airmart AC units
- Gree OEM units with Tuya WiFi
- “Thingclips Platform” devices
- Tuya 3.3 and 3.4 (legacy)
-
DeviceId & LocalKey ready to use with Tuya Local (please use a static IP in your router for the devices)
Prerequisites:
- Python 3.8+ installed and working.
This step ensures:
- Android Studio installed
- Android SDK + Platform-Tools installed
adbworking globally on macOS / Windows / Linux
macOS / Windows / Linux
- Download the installer from the official Android Studio website.
- Install normally.
- No custom configuration required at this stage.
-
Launch Android Studio.
-
On the welcome screen click:
More Actions → SDK Manager
(If a project is open: File → Settings → Android SDK) -
In SDK Platforms:
- Select any Android version, ideally Android 11 or Android 12.
- Click Apply → wait for the download to finish.
-
Go to SDK Tools tab:
- Enable Android SDK Platform-Tools
- (Optional but recommended) enable:
- Android Emulator
- Android SDK Build-Tools
- Click Apply → OK
This installs:
adb- Emulator system images
- Essential build tools
Try:
adb versionIf it works → you're done.
If not, add platform-tools manually:
export PATH=$PATH:$HOME/Library/Android/sdk/platform-tools # macOS
export PATH=$PATH:$HOME/Android/Sdk/platform-tools # LinuxTo make it persistent:
echo 'export PATH=$PATH:$HOME/Library/Android/sdk/platform-tools' >> ~/.zshrc
source ~/.zshrcTest in PowerShell or CMD:
adb versionIf it prints the version → done.
If it says "adb is not recognized" then add the following folder to your PATH:
C:\Users\<YOUR_USER>\AppData\Local\Android\Sdk\platform-toolsTo add it permanently (PowerShell):
setx PATH "$Env:PATH;C:\Users\<YOUR_USER>\AppData\Local\Android\Sdk\platform-tools"Now close/open the terminal and test again:
adb version
If it prints something like:
Android Debug Bridge version x.y.z
In this step we install Frida CLI and Frida Python bindings on your computer.
We’ll need:
frida(the core)frida-tools(CLI commands likefrida-ps)
Later, we’ll match this version with frida-server on the Android side.
Same commands on macOS / Windows / Linux
(Use python instead of python3 if that’s your default.)
python3 -m pip install --upgrade frida frida-toolsIf you are using a virtualenv, activate it first, then run the same command.
Run this in your terminal:
frida --version
You should see a version number, for example:
16.5.6
Important:
Remember this version. You must download the same version of frida-server later.
Run:
python3 -c "import frida; print(frida.version)"
You should see the same (or very close) version number.
If both checks return a version number →
🎉 STEP 2 is fully complete.
We now create a clean Android environment where we will later run
frida-server and install the com.aircondition.smart app.
This step works the same on macOS / Windows / Linux.
-
Launch Android Studio.
-
On the welcome screen select:
More Actions → Device Manager
(If a project is open: Tools → Device Manager) -
Click Create Device.
Recommended:
- Pixel 6 or Pixel 4
- Android 11 (R) or Android 12 (S)
(Both work perfectly with Frida.)
Avoid:
- Android 13/14 images (some restrictions).
- ARM images on Intel CPUs (too slow).
If possible:
- Prefer x86_64 image (fastest).
- If using Apple Silicon, prefer arm64-v8a.
Click Next → download image if needed → Finish.
In Device Manager:
- Click the ▶ Play button next to your virtual device.
Wait until Android fully boots.
Open a terminal and run:
adb devices
Expected output:
List of devices attached
emulator-5554 device
If your output contains at least one line like:
emulator-5554 deviceemulator-xxxx device
→ The emulator is detected correctly.
If you see unauthorized or nothing shows up:
adb kill-server
adb start-server
adb devices
If still not appearing, close the emulator and start it again.
🎉 STEP 3 is complete when:
- The emulator boots.
adb devicesshows the emulator asdevice.
Now that the emulator is running and ADB detects it, we install
frida-server inside the device.
This is required for hooking the app later.
In your terminal:
adb shell getprop ro.product.cpu.abi
Typical outputs:
x86_64→ emulator using Intel/AMD imagearm64-v8a→ emulator using ARM image (Apple Silicon or ARM-based image)
Remember this value — you need the matching frida-server binary.
- Run on your PC:
frida --version
Example output:
16.5.6
-
Go to the official Frida releases page. (https://github.com/frida/frida/releases)
-
Download the file:
frida-server-<VERSION>-android-<ABI>.xz
For example:
frida-server-16.5.6-android-x86_64.xzfrida-server-16.5.6-android-arm64.xz
- Extract the
.xzfile:
macOS / Linux:
xz -d frida-server-<VERSION>-android-<ABI>.xz
Windows: Use 7-Zip to extract the file.
After extraction you should have a file like:
frida-server-16.5.6-android-x86_64
Run:
adb root
adb push frida-server-<VERSION>-android-<ABI> /data/local/tmp/frida-server
adb shell chmod 755 /data/local/tmp/frida-server
adb root should work automatically on Android emulators
(it does not work on physical unrooted phones).
Run:
adb shell /data/local/tmp/frida-server &
This launches frida-server in the background.
On your PC:
frida-ps -U
Expected output: a list of running Android processes, for example:
PID Name
123 system_server
456 com.android.settings
789 com.android.systemui
If you see processes listed → frida-server is working 🎉
If you get:
Failed to enumerate processes- or
Timed out while waiting for the connection
Then try restarting:
adb kill-server
adb start-server
adb shell /data/local/tmp/frida-server &
frida-ps -U
🎉 STEP 4 is now complete when:
- The emulator is running
- frida-server is installed and executable
frida-ps -Ulists processes
Now we need the exact same app you use on your phone:
com.aircondition.smart (the Intelligent Air / ThingClips-based AC app).
The goal: have it running inside the emulator, so Frida can hook it.
You need an APK file named something like:
com.aircondition.smart.apk
There are several ways to obtain it (you only need one):
-
From your physical Android phone (recommended)
- Use an APK extractor app (e.g. “APK Extractor” from Play Store).
- Extract the Intelligent Air /
com.aircondition.smartapp. - Transfer the resulting
.apkfile to your PC (via USB, email, cloud, etc.).
-
From a trusted APK repository
- Search for
com.aircondition.smarton a reputable APK site. - Download the APK and verify it’s the correct package name.
- Search for
🔴 Important: Make sure the APK is exactly
com.aircondition.smart,
otherwise Frida hooks we write later won’t match.
Place the APK somewhere convenient, e.g.:
- macOS / Linux:
~/Downloads/com.aircondition.smart.apk - Windows:
C:\Users\<YOU>\Downloads\com.aircondition.smart.apk
With the emulator running and ADB working, install the APK:
From the folder where the APK lives, or with full path:
adb install com.aircondition.smart.apk
Or:
adb install ~/Downloads/com.aircondition.smart.apk
In PowerShell or CMD:
adb install C:\Users<YOU>\Downloads\com.aircondition.smart.apk
If the app was previously installed, you may need:
adb install -r com.aircondition.smart.apk
Expected output:
Performing Streamed Install
Success
Run:
adb shell pm list packages | grep aircondition
On Windows (PowerShell):
adb shell pm list packages | findstr aircondition
You should see:
package:com.aircondition.smart
You can start it from the emulator’s app drawer,
or via ADB:
adb shell monkey -p com.aircondition.smart -c android.intent.category.LAUNCHER 1
You should see the app UI appear in the emulator.
🎉 STEP 5 is complete when:
adb installfinishes withSuccessadb shell pm list packagesshowscom.aircondition.smart- You can open the app in the emulator
Goal:
Verify that we can inject code into the app process and see our own log messages.
We’ll:
- Write a tiny Frida script (
test-hook.js) - Run it against
com.aircondition.smart - Confirm we see output from inside the app
On your PC, create a file named:
test-hook.js
With this content:
Java.perform(function () {
console.log("[Frida] test-hook.js loaded inside com.aircondition.smart");
// As a smoke test, print the app's main Application class if any
try {
var appClass = Java.use("android.app.Application");
console.log("[Frida] Application class found:", appClass.$className);
} catch (e) {
console.log("[Frida] Could not access Application:", e);
}
});
This just confirms:
-
Java VM is accessible
-
Our code runs inside the app
Make sure:
- The emulator is running
frida-serveris running (frida-ps -Ushows processes)
Then run:
frida -U -f com.aircondition.smart -l test-hook.js
Explanation:
-
-U → use USB / emulator device (our emulator)
-
-f com.aircondition.smart → spawn this app
-
-l test-hook.js → load our script into it
If the hook is successful, your terminal should display something like:
[Frida] test-hook.js loaded inside com.aircondition.smart
[Frida] Application class found: android.app.Application
The second line may vary or may show an error — that’s fine.
The important part is that the first line appears, meaning:
- The script was injected
- Java.perform() executed inside the target app
- Frida is working end-to-end
You should also see the app launching inside the emulator.
Failed to spawn: unable to connect to remote frida-server
Fix:
- Ensure frida-server is running:
adb shell ps | grep frida
- If not, start it again:
adb shell /data/local/tmp/frida-server &
Frida: Process terminated
Fix:
- Try running the command again.
Some OEM-based apps crash during the first spawn but succeed on the second try.
`frida-ps -U` does not list processes
Fix:
adb kill-server
adb start-server
adb shell /data/local/tmp/frida-server &
frida-ps -U
Once the hook prints:
[Frida] test-hook.js loaded inside com.aircondition.smart
🎉 STEP 6 is fully complete.
Goal:
See the logical request data the app sends to Tuya/ThingClips (before crypto/signing).
This includes things like:
apiName(e.g.smartlife.m.user.email.password.login)- Keys like
email,passwd,gid, etc.
We’ll hook com.thingclips.smart.android.network.ThingApiParams.putPostData(...).
On your PC, create a file:
hook_thingapiparams.js
With this content:
Java.perform(function () {
try {
var ThingApiParams = Java.use("com.thingclips.smart.android.network.ThingApiParams");
// Helper to safely get apiName
function getApiName(obj) {
try {
if (obj.apiName) {
return obj.apiName.value;
}
} catch (e) {}
return "(unknown)";
}
// Overload 1: (String, Object) - very common
try {
ThingApiParams.putPostData.overload('java.lang.String', 'java.lang.Object')
.implementation = function (key, value) {
var apiName = getApiName(this);
console.log("[ThingApiParams.putPostData]");
console.log("apiName =", apiName);
console.log("key =", key, "value =", String(value));
console.log("--------------------------------------------");
return this.putPostData(key, value);
};
console.log("[Frida] Hooked ThingApiParams.putPostData(String, Object)");
} catch (e) {
console.log("[Frida] Could not hook putPostData(String, Object):", e);
}
// Overload 2: (String, JSONObject) - used by some APIs
try {
var JSONObject = Java.use("com.alibaba.fastjson.JSONObject");
ThingApiParams.putPostData.overload('java.lang.String', 'com.alibaba.fastjson.JSONObject')
.implementation = function (key, jsonObj) {
var apiName = getApiName(this);
console.log("[ThingApiParams.putPostData(JSON)]");
console.log("apiName =", apiName);
console.log("key =", key, "value(JSON) =", jsonObj.toJSONString());
console.log("--------------------------------------------");
return this.putPostData(key, jsonObj);
};
console.log("[Frida] Hooked ThingApiParams.putPostData(String, JSONObject)");
} catch (e) {
console.log("[Frida] Could not hook putPostData(String, JSONObject):", e);
}
} catch (e) {
console.log("[Frida] Error setting up ThingApiParams hook:", e);
}
});
This will log every key/value added to the postData for each API call.
With the emulator and frida-server running, execute:
frida -U -f com.aircondition.smart -l hook_thingapiparams.js
You should see something like:
[Frida] Hooked ThingApiParams.putPostData(String, Object)
[Frida] Hooked ThingApiParams.putPostData(String, JSONObject)
The app will launch inside the emulator.
Now, in the emulator:
- Open
com.aircondition.smart(it should already be launched by Frida). - Perform the login with your usual account.
- Wait until the app loads your “Home” / device list.
While you do this, watch the terminal where Frida is running.
You should start seeing logs like:
[ThingApiParams.putPostData]
apiName = smartlife.m.user.email.password.login
key = email value = [email protected]
[ThingApiParams.putPostData]
apiName = smartlife.m.user.email.password.login
key = passwd value = <long encrypted string>
[ThingApiParams.putPostData]
apiName = m.life.my.group.device.list
key = gid value = 263074449
This confirms that:
- The hook is active in
ThingApiParams. - You see the API name (
apiName) for each logical call. - You see all the keys/values for the POST body before encryption/signing.
🎉 STEP 7 is complete when:
frida -U -f com.aircondition.smart -l hook_thingapiparams.jsstarts without errors.- The app opens in the emulator.
- While you log in and navigate to the main/home screen, you see lines like:
apiName = smartlife.m.user.email.password.loginapiName = m.life.my.group.device.list
- For each
apiName, you see severalkey = ... value = ...lines.
Goal:
Intercept the decoded JSON that the SDK parses from Tuya, especially:
m.life.my.group.device.list→ devices +localKey,devId,ip, etc.
We’ll hook com.alibaba.fastjson.JSON.parseObject(String, Class).
Create a file on your PC:
hook_fastjson.js
With this content:
Java.perform(function () {
try {
var JSONcls = Java.use("com.alibaba.fastjson.JSON");
JSONcls.parseObject.overload('java.lang.String', 'java.lang.Class')
.implementation = function (text, clazz) {
var clsName = "";
try {
clsName = clazz.getName();
} catch (e) {}
// Only care about ApiResponeBean
if (clsName === "com.thingclips.smart.android.network.bean.ApiResponeBean") {
try {
var obj = JSON.parse(text);
var api = obj.a;
// This is the one with devId + localKey
if (api === "m.life.my.group.device.list") {
console.log("==== Device list (m.life.my.group.device.list) ====");
(obj.result || []).forEach(function (dev) {
console.log(
"Name:", dev.name,
"| devId:", dev.devId,
"| localKey:", dev.localKey
);
});
console.log("==================================================");
}
} catch (e) {
console.log("[Frida] JSON.parse error:", e);
}
}
return this.parseObject(text, clazz);
};
console.log("[Frida] fastjson hook for device list installed");
} catch (e) {
console.log("[Frida] Error hooking fastjson:", e);
}
});
With the emulator and frida-server running, execute:
frida -U -f com.aircondition.smart -l hook_fastjson.js
You should see:
[Frida] Hooked fastjson.JSON.parseObject(String, Class)
The app will launch inside the emulator. Log in and go to the home / device list screen as usual.
In the Frida CLI output you’ll see messages like:
==== Device list (m.life.my.group.device.list) ====
Name: AC1 | devId: bf2bbc01412371b8942aao | localKey: ZwT(abcd][f07_Vc
Name: AC2 | devId: bf76f639f1230b420aalmo | localKey: HkDbyabcdbiqI$Yr
Name: AC3 | devId: bfa0a38a812380aa0d7a1n | localKey: !6LnGCabcdsfeQ9?
==================================================
STEP 8 is considered complete when:
The command:
frida -U -f com.aircondition.smart -l hook_fastjson.js
runs successfully and prints:
[Frida] Hooked fastjson.JSON.parseObject(String, Class)
And you have a complete list of your devices with the localKey ready to use :)