Controlling the TP-LINK HS100 Wi-Fi smart plug

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

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 (fr.pool.ntp.org)… whatever. Then it looks up devs.tplinkcloud.com 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: 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
hs100.sh 192.168.1.20 9999 check


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

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

hs100 on Docker hub
https://hub.docker.com/repository/docker/georgovassilis/hs100

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

    Thanks,
    Paul

    Like

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

    Thanks,
    Paul

    Like

  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.

    Like

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

    Like

  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.

    Like

  6. 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 😉

    Like

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

    Like

  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

    #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”);
    }

    Like

  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!

    Like

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

    Like

  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

      Like

      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!

        Like

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

    Like

  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!

    Like

  14. 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?

    Like

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

    Like

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

    Like

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

      Liked by 1 person

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

    Like

    1. Nevermind my above post. It was an issue with the formating of the code somehow but i have now fixed it and it works!

      Like

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

    Like

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

      Like

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

    Like

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

    Like

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

    Like

  22. 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)?

    Like

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

    Like

    1. The payload I’m using for a 10 second delay on a HS105 reboot is: “AAAAItDygfiL/5r31e+UtsShw6zDt5Wv1PaS95v6g6Gbqprnmuc=”

      Like

      1. Thank you very much! I’ll try it out on my HS100 when I have time and come back with the result.

        Like

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

    Like

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

      Like

  25. 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?

    Like

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

    Like

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

    Like

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

    Like

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

        Like

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

    Like

Leave a reply to Tushar Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.