Controlling the TP-LINK HS100 Wi-Fi smart plug

[update 27 January 2017] TP-LINK HS100 project on github:

[update 24 October 2016] TL;DR; This is about a shell script which controls the TP-LINK HS100, HS110, HS200 Wi-Fi smart power plugs. 

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.

The installation is rather easy and unspectacular: a smart phone app helps the HS100 connect to the home WLAN and henceforth controls the power plug. The app being the only way to control the plug bothered me soon, since it meant that the plug could not be controlled by any programme, which, I think, defies the purpose of automation.
Admittedly this doesn’t happen often, but searching turned up nothing regarding my problem; people were happily controlling the HS100 through the app or Amazon Echo –  TP-Link publishes no technical documentation (though they do offer the GPL part of their code for download) and no thrifty hackers had reverse-engineered the plug. I was left alone, bearing grudges.
Not willing to give up my 40€ without a fight, I set up a WLAN access point with a Raspberry Pi (that’s another post coming soon) and armed with tcpdump, Wireshark and patience I listened.
The first thing to notice is that the plug contacts a public ntp server in France (… whatever. Then it looks up on AWS in Ireland from which it receives about 3K, containing what appears to be among others a security certificate.
Then the app polls the plug at regular 2 second intervals when the phone is not sleeping, which, while a bit too often, shouldn’t impact phone battery too much in practice.

Now the juicy stuff: the app controls the plug by connecting via TCP to port 9999 of the plug and exchanging a 55 byte payload. I was absolutely delighted to find that the payload varies only at offset 43 between the on and off commands; it doesn’t vary over time, contains no check sums and is not encrypted. I was able to extract the payload and put it into a simple shell script [2] (update 26 June 2016: thanks to Thomas Baust for figuring out the querying commands).

Usage is simple: , e.g.

switch plug on: 9999 on
check whether plug is switched on: 9999 check


# 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,


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

# base64 encoded data to send to the plug to switch it off

# base64 encoded data to send to the plug to query it

# base64 encoded data to query emeter - hs100 doesn't seem to support this in hardware, but the API seems to be there...

usage() {
echo Usage:
echo $0 ip port on/off/check/status/emeter
exit 1

checkarg() {

if [ -z "$value" ]; then
echo "missing argument $name"

checkargs() {
checkarg "ip" $ip
checkarg "port" $port
checkarg "command" $cmd

sendtoplug() {
echo -n "$payload" | base64 -d | nc -v $ip $port || echo couldn''t connect to $ip:$port, nc failed with exit code $?

output=`sendtoplug $ip $port "$payload_query" | base64`
if [[ $output == AAACJ* ]] ;
echo OFF
if [[ $output == AAACK* ]] ;
echo ON

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[@]}"
output=$(( $element ^ $code ))
printf "\x$(printf %x $output)"

# Main programme
case "$cmd" in
sendtoplug $ip $port "$payload_on" > /dev/null
sendtoplug $ip $port "$payload_off" > /dev/null
status "$payload_query"
status "$payload_emeter"
exit 0


[1] TP-LINK HS100 Wi-Fi smart plug

34 thoughts on “Controlling the TP-LINK HS100 Wi-Fi smart plug

  1. 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.



  2. 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 ));
    // Result coming back here is always, irrespective of switching on or off.
    // AAAALdDygfiL/5r31e+UtsWg1Iv5nPCR6LfEsNGlwOLYo4HkluS72LfTtpSunuOe4w==



  3. 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.


  4. 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).


  5. 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.


  6. Hi George,
    your payload_off sequence is to long, just use

    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 😉


  7. …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==”


  8. 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


    int main(int argc, char *argv[])
    int c=getchar();
    int skip=4;
    int code=0xAB;
    if (skip>0)


  9. 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!


  10. 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)

    return decoded

    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)

    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)

    — 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)


  11. 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).

    Liked by 1 person

    1. 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


      1. 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!


  12. 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: 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.


  13. 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!


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s