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

hs100.sh 192.168.1.20 9999 check


#!/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

72 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

Leave a Reply to Sean Seah Cancel reply

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

WordPress.com Logo

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

Google photo

You are commenting using your Google 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 )

Connecting to %s

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