I have a TPLink Kasa HS-100, a "smart plug", controlling a lamp in my living room. Actually, I had never called that room my "Living Room" until Google Assistant insisted I assign the device to a named logical unit- A reasonable request, if I'm being honest. After a brief set up using a phone app to discover the device and provide WiFi credentials, the lamp could be controlled through the app. After some more digging and configuring, it could be controlled by Google Assistant.

Which, y'know, no complaints. But innovation is about fixing things that aren't broken. A priority for me was being able to control the lamp (and any remote controlled device) from my linux desktop cli. So I set out for how to control the device via ifttt.com; not the ultimate goal, but an excellent starting point. This led me to the blog of the ITNerd, http://itnerd.space/, who had helpfully documented their experience deciphering TPLink's obfuscated and undocumented controls.

The structure appears to be a centralized authentication endpoint, to which you provide username and password in exchange for an API token. The token can be used at the same endpoint to retrieve a list of associated devices, each with an endpoint and device ID which can be used to set the status.

Clear as mud, eh? So to begin, I used Postman to send and manage arbitrary requests and their responses. In addition to being easy to configure and send requests, it makes it easy to interpret responses, export to curl (or whatever you'll end up using), and- A feature that seems trivial but saved my bacon on multiple occasions- Retains a history of the requests and responses. There's nothing quite so easy to forget as API schemes.

Some of the beautiful art from Postman

ITNerd documents the scheme fairly well, so I won't retread. In short, you set up your username and password on TPLink's Kasa app. Then you use that information to make a request for a token, which we'll call get_token(). You can use that token to then make a request for a device list, get_devices(). Finally, you can combine a device ID and token to set the light's state, set_light().

Postman exported the curl commands for making those requests, and they're easy enough to use one at a time. But I won't be H4x0R enough until I can run ~/light.sh on. So I made a shell script that initiated each request as a function. Bash runs one line at a time, so it's important to declare those functions at the top.

The end of the script would be using those functions conditionally, so I set up a control flow to listen to arguments passed to the script. light on should turn the light on, obviously, just as light off should turn it off. light device is a bit of an intermediate, displaying the contents of the device list.

And of course, between the two there needs to be some variables set, like my username and password. I don't know why (or if) you need a UUID, but ITNerd recommends getting one from https://www.uuidgenerator.net/version4. With those, we can get a new token every time we run the script.

To help parse out the json responses I was receiving, I installed and used jq, quite similar to the json parsing libraries used in most languages but this time available in Bash. Not only does it 'prettyprint' the device list, it singles out the token so it can be used in the set_light function.

Current Script


        curl -X POST \
                "https://wap.tplinkcloud.com" \
                -H 'Content-Type: application/json'\
                -H 'cache-control: no-cache' \
                -d '{"method": "login","params": {"appType": "Kasa_Android","cloudUserName": "'$USER'","cloudPassword": "'$PASS'","terminalUUID": "'$UUID'"}}'

        curl -X POST \
                "https://wap.tplinkcloud.com?token=$TOKEN" \
                -H 'Content-Type: application/json'\
                -H 'cache-control: no-cache' \
                -d '{"method": "getDeviceList"}'

        curl -X POST \
                "$URL?token=$TOKEN" \
                -H 'Content-Type: application/json' \
                -H 'cache-control: no-cache' \
                -d "{   'method':'passthrough', 
                        'params': {'deviceId': '$DEVICE', 
                        'requestData': '{\"system\":{\"set_relay_state\":{\"state\":$1}}}' }}"

URL="taken from get_devices"
DEVICE="taken from get_devices"
TOKEN=`get_token | jq --raw-output '.result.token'`

if [ "$1" == "on" ];then 
        set_light 1 
if [ "$1" == "off" ];then 
        set_light 0
if [ "$1" == "list" ];then 
        get_devices | jq

So that's the state of the script now. I've aliased goodnight to run ~/light.sh off; shutdown now. The lamp can still be controlled via the pushbutton on the device, the Kasa app, Google Assistant, and probably an NSA backdoor, but none of that is convenient when leaving my desk. Not as convenient as goodnight, anyway.

There's plenty to improve on! A cleverer set up could manage multiple switches with a Duskers style UI (`switch r1 on; dim l12 50;`) or be actually installed as a command, not just a script. I think the next logical step, however, is to toggle the switch. This requires a deeper investigation into the undocumented API, but I'm confident it's possible, as the app has the functionality. Perhaps I'll have to delve deep and packet capture the app's traffic- But hopefully it's just written down somewhere.

That turned out to be way easier than expected. ITNerd has a node library, https://github.com/adumont/tplink-cloud-api, which I was able to read through. You can turn the switch on by sending system:{set_relay_state:{state:1}} and you can check its status by sending system:{get_sys_info:{}}.

Thanks again to ITNerd for making/explaining the webhooks. TPLink would not have made it possible on their own.