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. Hello, works great with HS100 on Raspbian, works also on my Arch Linux after installing gnu-netcat, but script is hanging for long time without error, may be missing some dependences?

    Like

  2. HI, I’m trying to make this work, so far unsuccessfully. I’m not running a linux script. I’m sending the payload using another program. I tried converting the payload to hex using an online base64 decode tool. A couple of things. The hex output is not 55 bytes, but 46 bytes. Also, the payload on and payload off differ at bytes 43, 44 45 and 46.
    Do you have any advice for what to try? Do you have the actual payload going to the unit in hex, as I suspect its the base64 decode I’m getting wrong?

    Like

    1. A user emailed me last year with a similar issue. I think something gets lost during copy-pasting in the browser with online decoders. You probably should try a base64 decoder program for your operating system. The on and off payloads differing sounds plausible. The binary payload must be sent to the plug; scripts can’t store binary data, so it is written there with the base64 encoding and later translated to binary with a base64 decoder. If you can run docker, you always can try the dockerised version of the script.

      Like

  3. Thanks for your efforts figuring out how to communicate with these smart plugs! I’ve just acquired several HS103s and your script works wonderfully with them. However, one thing I’ve noticed is that it seems like the script takes a long time to finish running, upwards of 20 – 30 seconds. Is this the expected behavior for these devices and is there anything I can do to speed things up?

    Like

    1. Thanks for letting me know about the HS103s compatibility. If you give the script a name rather than an IP it could be a name resolution issue (eg. DNS trying first to resolve IPv6 before falling back to IPv4). If you add “echo linenumber” statements into key locations at the script and show me the output maybe I can figure it out?

      Like

      1. I’ve had a chance to do a little more troubleshooting and figured out the issue. For some reason the smart plug is holding the connection open for quite awhile after a command is sent, so the script has to wait for it to time out, and then parses/displays the output.

        There are two ways to address the issue by varying the invocation of the netcat command in your send_to_plug() function. The first is by adding the “q” parameter and the second by adding the “w” parameter. Both of these parameters control timeouts in netcat. The “q” parameter tells netcat to disconnect X seconds after EOF on STDIN, and the “w” parameter tells netcat to disconnect after the remote end has been idle for X seconds.

        In experimenting with the script, the “q” parameter results in quicker execution, but for very short timeout values may result in netcat disconnecting before it receives the output from the smart plug. This is really only an issue for commands like “status” that rely on receiving data back. The “w” parameter is safer since it waits until data has been received, but seems to add a couple extra seconds to the overall execution time regardless of the timeout used.

        Here’s my modification to the netcat invocation in line 64 in the send_to_plug() function:

        nc -q 1 -v $ip $port

        That gives me a near instant execution, but I would suggest as a generally “safe” modification to the script to use something like -w 5

        Like

  4. Thanks for your work on this. The netcat implementation on arch linux doesn’t have a nice timeout that works (as per Jordan W.’s comment) so I rewrote the bare functionality in python. Working with a hs110 (including the emeter functionality – which is what I bought the plug for).
    https://github.com/Nealefelaen/hs100

    Like

  5. Still works great on

    {“system”:{“get_sysinfo”:{“sw_ver”:”1.1.5 Build 200828 Rel.081157″,”hw_ver”:”4.0″,”model”:”HS100(EU)”

    Which i just bought from amazon Germany. I’ll now make an android termux script (with termux api app) to turn off the plug and stop charging my phone at 85% battery when i leave it plugged overnight. Thank you!

    Like

    1. Would you care to share the script, or mention the Android termux command to monitor the battery charge level?

      Like

    2. Could you please specify how this was done? I’m also trying to get this to work with termux but without a clue ATM.

      Like

      1. I don’t want to speak for Todor, but I suspect what he meant was he monitors the phone’s battery level. There are several task automation apps for Android (eg. IFTTT) which can execute commands in response to events (such as the battery charge changing).

        Like

      2. Yes i do know of IFTTT and monitor the battery, it was Moe the termux (command line) command for the TP-Link i was interested it. Like if there is one curl or anything similar to use when to tur off and on. Would be glad to get a reply on this, but i also found out about “pip install python-kasa” that candy find / TP-Link devices on the network. So i made a script instead to send that command to turn of and off my plug.

        Like

  6. Hi, this works great – thank you for sharing…do you know if it will also work on the newer KP105 plugs….cant see any reason why it shouldnt

    Like

      1. Hi again,

        I can confirm that these work the same as the originals 🙂 🙂

        ________________________________

        Liked by 1 person

  7. I can confirm that the same shell script, and Nealefelaen’s python script above, both can control a Kasa HS210 3 way switch. I haven’t hooked it up in a 3 way light circuit yet but the switch relay sounds as though successive ‘off’ commands toggle the state, as evidenced by a slightly different relay sound. The ‘on’ command makes no relay click noise.
    My end goal is to implement a time delay turn off function for this outdoor 3 way switched light.

    Liked by 1 person

    1. Thanks for bringing this up. When analysing the traffic between app and the internet we found sensitive information like location being transmitted – the only real security hole in this scenario, imho, is the Kaza app.

      Like

  8. Hi George,

    What an excellent script and resource!

    I purchased some KP105s, running out of the box the script works fine on them. It’s pretty cool. I’ve been using them to remotely trigger when Motion detects events from some pi cameras.

    Best wishes,
    Dr P

    Like

  9. With the recent Tplink update, it appears to be broken now. It was causing my HOOBS to not run. After deleting this TPlink Plugin my HOOBS will run again. But now I can’t install this particular plugin. Any thoughts?

    Like

  10. Hello,
    I launched a wifi accesspoint on a raspberry pi, isolated from the internet, then connected to that wifi with the phone having the Kasa app. But when launching Kasa to “add a device”, the app doesn’t accept the smartplug : “no internet, make sure that your router is functioning properly and that you have a strong wi-fi signal”, and from there it loops for re-scanning any plug nearby.
    So, as I saw on your comprehensive article that the plug has to reach tplink servers to somehow “activate”, I tried to bypass that step by downloading alternatives apps : PlugMonitor and MyLocalTPlink. With PlugMonitor, I can connect directly to the plug’s own WLAN and switch it ON/OFF through the app but it’s not secure (no password asked by the plug), so anyone could connect to the plug’s WLAN I guess, but worse, anyone downloading that app could turn it on/down while I got it down/on.
    The app can display current wattage and notify the phone on which it’s spinning on, or turn the plug off, when the wattage crosses a threshold. So it requires :
    *the phone being wired to live as long as it has to monitor the plug before it triggers it off
    *+ having the plug as the phone’s wifi-source, so I don’t know if there are native ways for the phone to notify other devices on the external-world internet.
    I never checked but I never heard about spawning an Android VM on a tower server, to get rid of the need for a phone to recharge .

    TLDR : did you manage to bypass the plug wanting to handshake tplink servers ? Like, did you connect your pi to the internet with an ethernet wire, but blocked the plug to access the internet, while letting your kasa-phone believe it had access to the internet ? (in order to configure the plug as the pi-router’s client, and not being a self-router anymore)

    By thanking you, someone confused

    Like

    1. I’m not sure the plug won’t “activate” (as you write) if it can’t talk to tplink servers. The main point of the setup procedure is to get the plug to connect to your home wifi so that the kasa app (on your phone) can talk to the plug. Once the plug is connected to your wifi, I think the script will work even without the plug ever connecting to tplink servers. While I was still using the hs100 (I don’t any more), I had it (and all other similar devices) connect to a password-protected local wifi with no internet access – that way no one outside your home network can control it.

      To answer your question: after the first setup, I never had the need to re-activate the plug, so I didn’t pursue the goal of running another setup that bypasses tplink servers.

      Like

      1. I can confirm that once the plug is connected to wifi (have you tried resetting it first ??) it certainly does connect locally to, for example a Pi….using Georges’ script it works very well 🙂

        Like

      2. thank you for your fast reply, what do you mean by “the first setup” ? At first your pi+plug+phone were never reaching the internet ? Or do they and then you isolated them for the rest of their life ?

        Any one reading this and still using the device are welcome to answer please, they may be adding constraints to their kasa app from back in 2016 when George did the reverse engineering to when I downloaded and used it, idk…

        Like

      3. > what do you mean by “the first setup” ?
        The first time you power up the device

        > At first your pi+plug+phone were never reaching the internet
        During the first-time-ever setup, the phone is connected to the plug’s wifi; at that point, neither the plug nor the phone are connected to the internet and yet they talk to each other.

        Like

      4. I plug the socket into the mains
        I open the KASA app and search for / add the new device
        once done I can control it with the pi

        Like

    2. I’m using this Python script:
      https://github.com/softScheck/tplink-smartplug

      The HS100/HS110 does not need an Ineternet connection at all. The Kasa app does. It’s phoning home sharing your data with the Kasa servers.
      Here’s what I do:

      When the HS100/HS110 is starting up for the first time and after it has been reset to factory settings, it sets up it’s own wifi with SSID TPLINK_SmartPlug_XXXX and no password. Connect your computer to this wifi network and send this command.

      tplink-smartplug.py -t 192.168.0.1 -j ‘{“netif”:{“set_stainfo”:{“ssid”:”SSID”,”password”:”PASSWORD”,”key_type”:3}}}’

      Obviously, you change SSID and PASSWORD to the ssid and password of your own wifi network. The target ip is 192.168.0.1.

      When the plug receives the command, it connects to your wifi and turns off it’s own wifi network. If you make a typo, it can’t connect (obviously) and you can simply try again. When it connects to your wifi, you lose connection to the plug. Then you connect to your own wifi network and find the plug’s ip address. Now you can control it using the tplink-smartplug.py script. This way you can set up and use the HS100/HS110 plugs in any location that has never ever seen an Internet connection, as long as you have electric power and a router.

      Liked by 1 person

  11. JSON is totally unfamiliar to me so I have a couple of questions about the JSON strings provided along with the script.
    This JSON string is supposed to add a new schedule:

    ‘{“schedule”:{“add_rule”:{“stime_opt”:0,”wday”:[1,0,0,1,1,0,0],”smin”:1014,”enable”:1,”repeat”:1,”etime_opt”:-1,”name”:”lights on”,”eact”:-1,”month”:0,”sact”:1,”year”:0,”longitude”:0,”day”:0,”force”:0,”latitude”:0,”emin”:0},”set_overall_enable”:{“enable”:1}}}’

    My main question is this: What exactly are the given parameters or arguments in the string?
    What does this mean or do:
    stime_opt
    wday (workdays?)
    smin
    etime_opt
    eact
    sact
    emin
    set_overall_enable

    A couple more questions:
    – How do I write a JSON string that switches the plug on in 5 minutes from adding the schedule and is never repeated?
    – Can I write a schedule that will switch the plug on even if the router crashes?

    Like

      1. If you scroll down the page you linked to, you will find the string under the headline Schedule Commands. I just hoped that you or someone who reads this blog might know their JSON much better than I. But, I can of course try to contact the person who found out these strings. If I learn more, I’ll post a comment here because someone else might find it interesting.

        Like

Leave a reply to George Georgovassilis Cancel reply

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