[update 7 June 2020] Available now on Docker: https://hub.docker.com/repository/docker/georgovassilis/hs100
[update 27 January 2017] TP-LINK HS100 project on github: https://github.com/ggeorgovassilis/linuxscripts
[update December 2020] TL;DR; This is about a shell script which controls the TP-LINK HS100, HS103s, HS110, HS200 and KP105 Wi-Fi smart power plugs. Other models in that product line may also work (the tplink-smarthome-api project uses a similar concept to this script to communicate with a wider range of plugs).
[update January 2021] I realised that when moving from Blogger to WordPress, user comments were lost. I sincerely apologise to everyone who contributed questions and remarks.
I was scurrying down the home automation isle at the local electronics discounter, firm in my determination to make it without distractions to the computer section, when one of those fancy new Wi-Fi power sockets caught my attention. I haven’t quite caught on with the home automation hype yet, but with everything going on I felt left behind and it seemed like a good idea to use the long weekend ahead and try to catch up a bit. And so it came that I left the shop 40€ poorer and with a TP-LINK HS100 Wi-Fi smart plug [1] in the pocket.
Usage is simple: hs100.sh , e.g.
switch plug on:
hs100.sh 192.168.1.20 9999 on
check whether plug is switched on:
Thomas Baust documented the encryption algorithm in a comment (which was unfortunately lost while migrating from Blogger to WordPress):
Hi George, This little c-prgramm can decode the messages – just pipe the raw data to it like: echo AAAAI9Dw0qHYq9+61/XPtJS20bTAn+yV5o/hh+jK8J7rh+vLtpbr|base64 -d|./hs1xdec #include int main(int argc, char *argv[]) { int c=getchar(); int skip=4; int code=0xAB; while(c!=EOF) { if (skip>0) { skip–; c=getchar(); } else { putchar(c^code); code=c; c=getchar(); } } printf(“\n”); }
Hi George, your payload_off sequence is to long, just use AAAAKtDygfiL/5r31e+UtsWg1Iv5nPCR6LfEsNGlwOLYo4HyhueT9tTu3qPeow== Btw: The “encryption” is just xor every Byte with the previous one. As result you will get a Json message with 5bytes Header and maybe one char padding at the end 😉
Thomas Baust 26 June 2016
Here is an outdated version of the script, head over to github for a recent one.
#!/bin/bash ## # Switch the TP-LINK HS100 wlan smart plug on and off, query for status # Tested with firmware 1.0.8 # # Credits to Thomas Baust for the query/status/emeter commands # # Author George Georgovassilis, https://github.com/ggeorgovassilis/linuxscripts ip=$1 port=$2 cmd=$3 check_binaries() { command -v nc >/dev/null 2>&1 || { echo >&2 "The nc programme for sending data over the network isn't in the path, communication with the plug will fail"; exit 2; } command -v base64 >/dev/null 2>&1 || { echo >&2 "The base64 programme for decoding base64 encoded strings isn't in the path, decoding of payloads will fail"; exit 2; } command -v od >/dev/null 2>&1 || { echo >&2 "The od programme for converting binary data to numbers isn't in the path, the status and emeter commands will fail";} command -v read >/dev/null 2>&1 || { echo >&2 "The read programme for splitting text into tokens isn't in the path, the status and emeter commands will fail";} command -v printf >/dev/null 2>&1 || { echo >&2 "The printf programme for converting numbers into binary isn't in the path, the status and emeter commands will fail";} } # base64 encoded data to send to the plug to switch it on payload_on="AAAAKtDygfiL/5r31e+UtsWg1Iv5nPCR6LfEsNGlwOLYo4HyhueT9tTu36Lfog==" # base64 encoded data to send to the plug to switch it off payload_off="AAAAKtDygfiL/5r31e+UtsWg1Iv5nPCR6LfEsNGlwOLYo4HyhueT9tTu3qPeow==" # base64 encoded data to send to the plug to query it payload_query="AAAAI9Dw0qHYq9+61/XPtJS20bTAn+yV5o/hh+jK8J7rh+vLtpbr" # base64 encoded data to query emeter - hs100 doesn't seem to support this in hardware, but the API seems to be there... payload_emeter="AAAAJNDw0rfav8uu3P7Ev5+92r/LlOaD4o76k/6buYPtmPSYuMXlmA==" usage() { echo Usage: echo $0 ip port on/off/check/status/emeter exit 1 } checkarg() { name="$1" value="$2" if [ -z "$value" ]; then echo "missing argument $name" usage fi } checkargs() { checkarg "ip" $ip checkarg "port" $port checkarg "command" $cmd } sendtoplug() { ip="$1" port="$2" payload="$3" echo -n "$payload" | base64 -d | nc -v $ip $port || echo couldn''t connect to $ip:$port, nc failed with exit code $? } check(){ output=`sendtoplug $ip $port "$payload_query" | base64` if [[ $output == AAACJ* ]] ; then echo OFF fi if [[ $output == AAACK* ]] ; then echo ON fi } status(){ payload="$1" code=171 offset=4 input_num=`sendtoplug $ip $port "$payload" | od --skip-bytes=$offset --address-radix=n -t u1 --width=9999` IFS=' ' read -r -a array <<< "$input_num" for element in "${array[@]}" do output=$(( $element ^ $code )) printf "\x$(printf %x $output)" code=$element done } ## # Main programme ## check_binaries checkargs case "$cmd" in on) sendtoplug $ip $port "$payload_on" > /dev/null ;; off) sendtoplug $ip $port "$payload_off" > /dev/null ;; check) check ;; status) status "$payload_query" ;; emeter) status "$payload_emeter" ;; *) usage ;; esac exit 0
Resources
https://github.com/ggeorgovassilis/linuxscripts/tree/master/tp-link-hs100-smartplug
hs100 on Docker hub
https://hub.docker.com/repository/docker/georgovassilis/hs100
Ha great!!!, thank you George.
I was afraid for a second that this was encrypted data and would only work in your wifi. However I just tried it and it works great.
Thanks,
Paul
LikeLike
George,
Would you have the base64 string for polling as well? I tried to capture/convert it from the 2nd picture, but I was able to successfully make a working base64 string out of that.
Further I wanted to share this PHP snippet of sendtoplug, which might be helpful for someone else.
function sendtoplug ($ip, $port, $payload, $timeout) {
$client = stream_socket_client('tcp://'.$ip.':'.$port, $errno, $errorMessage, $timeout);
if ($client === false) {
echo “Failed to connect: $errno $errorMessage”;
} else {
stream_set_timeout($client, $timeout);
fwrite($client, base64_decode($payload));
echo base64_encode(stream_get_line ( $client , 1024 ));
fclose($client);
}
}
// Result coming back here is always, irrespective of switching on or off.
// AAAALdDygfiL/5r31e+UtsWg1Iv5nPCR6LfEsNGlwOLYo4HkluS72LfTtpSunuOe4w==
Thanks,
Paul
LikeLike
Thanks 🙂
Polling is tricky: the poller opens an UDP socket, then tells the plug the address and port of that UDP socket (somehow, haven't found out how) and then the plug sends periodically to that UDP socket a heartbeat.
LikeLike
I wrote some Java code using your command strings and it works correctly. Using Wireshark, I can see the traffic from my code to the device. However, I can't see traffic (except the UDP heartbeat) via the android app. I guess your “WLAN access point with a Raspberry Pi” is the key to sniffing-out the command strings? I ask because I am interested in reverse engineering other similar devices (ex. Ankuoo NEO).
LikeLike
Right, this works only if you can eavesdrop on the traffic. Maybe it's possible to set up a laptop as an access point for both the mobile phone and the power plug?
LikeLike
I am running Wireshark on a Win10 laptop and can see the phone app sending the UDP messages to port 9999. I don't see any TCP messages from the phone.
LikeLike
Hi George,
your payload_off sequence is to long, just use
AAAAKtDygfiL/5r31e+UtsWg1Iv5nPCR6LfEsNGlwOLYo4HyhueT9tTu3qPeow==
Btw: The “encryption” is just xor every Byte with the previous one. As result you will get a Json message with 5bytes Header and maybe one char padding at the end 😉
LikeLike
…now I got it – the first 4 bytes is the message length (not encryted, big endian), then the encryption sequence starts (at the fifth byte) with 0xAB as initial key (every JSON messages starts with an open bracket 😉
btw: query for system information is “AAAAI9Dw0qHYq9+61/XPtJS20bTAn+yV5o/hh+jK8J7rh+vLtpbr”
and query emeter (HS110) “AAAAJNDw0rfav8uu3P7Ev5+92r/LlOaD4o76k/6buYPtmPSYuMXlmA==”
LikeLike
any examples of how you query system information? thank you!
LikeLike
This comment has been removed by a blog administrator.
LikeLike
Thank you, that worked! I updated the script with a “query” parameter to allow querying the plug's status. I'm curious about how you found out about the query command… 🙂
LikeLike
Hi Sean, see the discussion with Thomas Baust earlier in this thread.
LikeLike
I just noticed that it takes a while for the plug to settle on a status; if you query the state right after changing it you still get the old state.
LikeLike
This comment has been removed by the author.
LikeLike
Hi George,
This little c-prgramm can decode the messages – just pipe the raw data to it like:
echo AAAAI9Dw0qHYq9+61/XPtJS20bTAn+yV5o/hh+jK8J7rh+vLtpbr|base64 -d|./hs1xdec
#include
int main(int argc, char *argv[])
{
int c=getchar();
int skip=4;
int code=0xAB;
while(c!=EOF)
{
if (skip>0)
{
skip–;
c=getchar();
}
else
{
putchar(c^code);
code=c;
c=getchar();
}
}
printf(“\n”);
}
LikeLike
Thanks Thomas Baust. That thing is seriously reporting an GPS location?
LikeLike
Hi George,
yes, but only if you set it at least once (it is set by the Kasa app with help of your mobile internal GPS sensor) 😎
LikeLike
This comment has been removed by the author.
LikeLike
Thank you very much!
LikeLike
Unbelievable but true, I managed to convert your programme into a bash script and integrate it with the rest of the plug script.
LikeLike
Thank you for this! Just like you, I tried looking for API when I bought this a couple of months ago but didn't find any. Searched for it again and you had done the research and published it!
LikeLike
How's the Raspberry Pi as an access point post coming along ? 😉
LikeLike
Queued behind a few other posts 🙂 In the meantime I highly suggest [1] as a reading.
[1] https://frillip.com/using-your-raspberry-pi-3-as-a-wifi-access-point-with-hostapd/
LikeLike
Hi George,
i have managed to implement a wireshark dissector – just save it in your plugin directory (with .lua as exension):
— declare the protocol
hs1x0_proto_TCP = Proto (“HS1x0U”, “TP Link HS1x0 (TCP”)
hs1x0_proto_UDP = Proto (“HS1x0T”, “TP Link HS1x0 (UDP)”)
function common_dissector(buffer,start,tree)
local code = 171
local size = buffer:len()
local decoded = “”
— Apply XOR cipher and save the decoded string
for i=start,size-1 do
data = bit.bxor(buffer(i,1):uint(), code)
decoded = decoded .. string.char(data)
code=buffer(i,1):uint()
end
return decoded
end
function hs1x0_proto_TCP.dissector(buffer,pinfo,tree)
pinfo.cols.protocol = “TP HS1x0”
local decoded = common_dissector(buffer,4,tree)
— Make wireshark display our results
local subtree = tree:add(hs1x0_proto_TCP, buffer() ,”HS1x0 Protocol (TCP)”)
subtree:add(buffer(0,size), “Decoded: ” .. decoded)
end
function hs1x0_proto_UDP.dissector(buffer,pinfo,tree)
pinfo.cols.protocol = “TP HS1x0”
local decoded = common_dissector(buffer,0,tree)
— Make wireshark display our results
local subtree = tree:add(hs1x0_proto_UDP, buffer() ,”HS1x0 Protocol (UDP)”)
subtree:add(buffer(0,size), “Decoded: ” .. decoded)
end
— load the tcp port table
tcp_table = DissectorTable.get (“tcp.port”)
udp_table = DissectorTable.get (“udp.port”)
— register the protocol to port 9999
tcp_table:add (9999, hs1x0_proto_TCP)
udp_table:add (9999, hs1x0_proto_UDP)
LikeLike
This comment has been removed by the author.
LikeLike
The move to a custom blog domain means, unfortunately, that we can't use g+ comments any more. Please either use blogger comments or the github repository: https://github.com/ggeorgovassilis/linuxscripts
LikeLike
This comment has been removed by the author.
LikeLike
Thanks! Works great in the Bash shell in Windows 10 (TP-Link does not provide a Windows app of any sort). So now I just need to figure out how to do the same use C# and .NET or Javascript & HTML (I’m not much of a programmer! -Hello, World! is about my speed).
LikeLiked by 1 person
If you care only about switching the plug on and off and don’t care about querying the status then you can just dump the binary commands into the plug over a socket connection; that should be reasonably easy in C#, but I doubt it’s possible with Javascript from a browser since it doesn’t send UDP packages
LikeLike
Thanks for the suggestion. Apparently C# async socket connections have become a lot easier in .NET 4.5 so I’ll give it a try. Knowing that I can communicate with my device (a TP-Link HS200 switch) via your script gives me a great jumping-off point. Since I’d eventually like to do it outside my LAN, I might need to learn a little something about secure IOT connections over the Internet, too!
LikeLike
Oops! Meant to cite https://msdn.microsoft.com/en-us/library/1w48w47c(v=vs.110).aspx as a good starting point for people like me who went from not knowing much about C to not knowing much about C#.
LikeLike
While further researching how to communicate with TP-Link Smart Home devices, I came across this in-depth softScheck security analysis of the HS-110 that goes nicely with and extends what George and Thomas Baust have found out: https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/ On GitHub, Stroetmann and Esser provide several modules to interact with the HS-100 or HS-110 including a WireShark plug-in to decrypt TCP packets from the devices and a Python client to issue commands to the devices. According to the authors, there are some security vulnerabilities in the devices. Stroetmann and Esser did their work several months after George first experimented with the HS-100.
LikeLike
I wish I had half their patience 🙂 I generally operate all this stuff with a dedicated, isolated WLAN on a software router.
LikeLike
Well, this is very cool. I copied your script onto a Raspberry Pi, along with the ha-bridge program. Ha-bridge is a Philips Hue Bridge software emulator, and allows devices like old X10 smart plugs and switches, for example, to appear as Hue devices and be controlled by voice via Amazon Echo. It also allows Echo on, off, and dim voice commands meant for Hue hardware to trigger scripts, like yours. So, by inserting your TP-Link on and off commands into ha-bridge, I can now control my smart switches using Alexa, but without going through TP-Link’s Kasa Skill or using their cloud server. Doing everything locally through the Pi is both faster and more dependable, as TP-Link’s cloud server has been down or very slow for long periods of time in the past. Nice, thank you!
LikeLike
i’m getting this error when trying to run the command or any of the commands.
hs100.sh 10.0.0.244 check
hs100.sh: 81: Syntax error: redirection unexpected (expecting word)
Can you please help what this means?
LikeLike
I think you’re missing the port. The syntax is:
hs100.sh ip port command
for example
hs100.sh 192.168.1.20 9999 on
LikeLike
thanks for this. the on/off is working great
but for cmds that parse a return string, i’m getting “od: cannot skip past end of combined input” perhaps i have a newer revision and the return string is different
my bash-fu is not strong enough for this. there is very little on the internets about the od command, surprisingly
LikeLike
Hello, i bought an HS100 also and looking for a program for Windows / Linux I found your article.
I tried the commands discovered by you and then I decided to create a program for Windows 10 in my free time.
I planned to release a version of the software also for Linux.
I still do not have finished (schedule, countdown and anti-theft rules edition is not available yet) but a first version is available here if you want to try :
https://github.com/inventospracticos/JAUSEWIN/releases
Thanks!
LikeLike
Thanks. I couldn’t find the source code in your repo.
LikeLike
Mate thank you! I spent 2 days trying to find my way in Wireshark because I couldn’t find the IP. I literally was about to give up and buy another plug! Thanks! It worked like a charm
LikeLiked by 1 person
Hello, I put in the repository only the Windows binary of the program. Is here :
[redacted]
LikeLike
The link you posted contains a binary without the source code, so I deleted the link.
LikeLike
Hi George,
I was just looking for such a script as i have the HS110 plug which is connected to my 3D printer which i control via a Raspbian Image running Repetier Server. I copied your script over to my /home/pi/ directory and wanted to give it a go and test it running it with:
bash hs100.sh 192.168.1.120 9999 check
But i get following error:
hs100.sh: line 2: $’\r’: command not found
hs100.sh: line 10: $’\r’: command not found
hs100.sh: line 14: $’\r’: command not found
hs100.sh: line 16: $’\r’: command not found
hs100.sh: line 19: $’\r’: command not found
hs100.sh: line 22: $’\r’: command not found
hs100.sh: line 25: $’\r’: command not found
hs100.sh: line 28: $’\r’: command not found
hs100.sh: line 30: $’\r’: command not found
hs100.sh: line 31: syntax error near unexpected token `$'{\r”
‘s100.sh: line 31: `check_dependencies() {
I also tryed to run it like this:
sh hs100.sh 192.168.1.120 9999 check
But then i get following error:
: not found: hs100.sh:
: not found0: hs100.sh:
: not found4: hs100.sh:
: not found6: hs100.sh:
: not found9: hs100.sh:
: not found2: hs100.sh:
: not found5: hs100.sh:
: not found8: hs100.sh:
: not found0: hs100.sh:
hs100.sh: 71: hs100.sh: Syntax error: redirection unexpected
I am not really a linux guru so i am not sure what i am missing here.
Any help would be appriciated.
LikeLike
Nevermind my above post. It was an issue with the formating of the code somehow but i have now fixed it and it works!
LikeLike
Hi George,
Can you explain what steps you took to extract the payload and convert it to Base 64 in order to replay it back to the device in your script please. Any tips would be much appreciated.
Cheers Scott
LikeLike
I realise I promised a post where I describe the payload extraction process but I never delivered The short version:
Step 1: set up a WIFI hotspot which captures network traffic. E.g. https://makezine.com/projects/build-raspberry-pi-network-scanner/
Step 2: analyse the captured traffic with a packet analyser, e.g. Wireshark (I explain that in the post) and identify the payload
Step 3: We need base64 only because we want to inline the payload into the script. You could also store the binary payload verbatim in files and pipe those into netcat. For base64 encoding see e.g. https://scottlinux.com/2012/09/01/encode-or-decode-base64-from-the-command-line/
LikeLike
But how do you export the payload out of Wireshark? I can see the data from my capture in the PSH/ACK packet being sent to port 9999. Do I just right click and export packet bytes then run base64 on that file somehow? Or am I way off the mark? Thanks in advance.
LikeLike
It’s been a while since I’ve used Wireshark and memory is fuzzy on that part. I ‘assume’ something like https://osqa-ask.wireshark.org/questions/29693/export-selected-packet-bytes-how-to-cut-off-the-payload-in-a-pcap-file
LikeLike
Thanks for this..I had been using json and the cloud api server but after my token expired I decided to search for a better way, rather than dealing with that every so often.
The only thing I’ve changed is I hard-coded the port.
LikeLike
I desperately need a way to query parental control settings of my TP-Link Deco M9 wifi mesh from the TP cloud but I have been unable to accomplish that. Have any of you by any change tried to interact with the Deco systems? The traffic I have captured looks quite similar to this smart plug traffic.
LikeLike
Thanks for the data in this article.
It enabled me to write a small Java application to control the HS100 switch. Please find it at:
https://github.com/stuartdd/TP-Link_HS100-Controller
Comments and feedback are welcome.
LikeLike
I’m wondering if the same control is possible with a HS210 TP-Link 3 way wall light switch, using the same Wireshark examination and modifying the existing smart plug effective commands (payload codes)?
LikeLike
Hi. Thanks for the script. It sort of works, even with the newest firmware (1.5.4).
But since I don’t have neither the time nor skills to figure it out myself within a reasonable time, I’d like to ask you, what is the payload for
{“system”:{“reboot”:{“delay”:10}}}
This command is supposed to turn the HS100 off and then back on again after n seconds (in my example, 10 seconds).
Kind regards from Norway
LikeLike
The payload I’m using for a 10 second delay on a HS105 reboot is: “AAAAItDygfiL/5r31e+UtsShw6zDt5Wv1PaS95v6g6Gbqprnmuc=”
LikeLike
Thank you very much! I’ll try it out on my HS100 when I have time and come back with the result.
LikeLike
Thank you very much for the explanation and the script. I will try a similar kind of implementation for a 4 Outlet wifi Plug, hopefully will be able to do that..
LikeLike
Will this work for the HS105 – TP-Link – Kasa Smart Wi-Fi Plug Mini?
There is one reference to HS105, so IO think so.
LikeLike
Sorry, I don’t know if that will work 😦
LikeLike
I’ve used this script to test my HS-105 units. For the most part, this will work with the HS105. ‘on/off/status’ work correctly. ‘check/emeter’ do not. Since ‘toggle’ relies on the check command, it only works when the unit is off.
LikeLike
I thought I bought two HS105s. They turned out to be HS103s! They look identical in the photos.
Anyway, ‘on/off’ work fine, but nothing else.
The other problem, it that hs100.sh never seems to exit! OK, it does after about a minute. I guess it times out waiting for status?
LikeLike
I bought two HS103 smart plugs. I thought I ordered HS105, the photos are identical. I have no idea what differs between them.
Anyway, ‘on/off/status’ work OK. check does not.
I tried Philip Branning fork of hs100.sh. ‘on/off/check/status’ all work!
Thanks to you both!
LikeLike
Great write-up and research! Thanks!
Anyone tried scripting with multiple outlet versions of the Kasa devices? KP300 (Outdoor 2 outlets); HS107 (2 outlet); HS300/303 (6/3 Outlets); or the KP200 (2 Socket Wall Outlet)
LikeLike
Hi George,
Fantastic job and write up 🙂
Do you know if there’s a way you can use the emeter function to query the daily_kh figure as opposed to the total_wh on the HS110 please?
It’s shown in the Kasa app as “Total Consumption” on the ‘Today’ page but don’t know if this is a ‘cloud only’ value.
Thank you in advance!
LikeLike
Sorry, I don’t 😦 I only have the HS100 which doesn’t support that function
LikeLike
No problem, thank you for your quick reply!! your script is embedded in my home solar automation project as part of my DIY PowerDiverter, It’s working well, Thank you!
LikeLike
Hello,
Thank you for the very helpful tutorial! I am currently attempting to do the same for the P100 TP Link smart plug, but I am still struggling with the packets. I have identified the IP’s and MAC’s of my mobile and the plug, but in wireshark I can only see broadcast packets to 255.255.255.255, once I open the Tapo application on my mobile. I tried setting filters for the specific MAC’s and IP’s but I am not getting any results.
I thought I should try it in Kali linux over a Monitored wlan interface, but I still have not managed to get that to work.
Is there something you understand with your experience that I am probably missing? Thank you in advance George!
Best wishes,
Dim
LikeLike
I remember dedicating a raspberry pi to the task. There are plenty of instructions around, I hope you find something that works for you: https://www.google.com/search?q=raspberry+pi+capture+wifi+traffic
LikeLike
By the way: broadcast packets always appear to be sent to 255.255.255.255; it’s a special IP address which means “to all recipients”.
LikeLike