Textual description of firstImageUrl

Controlling the TP-LINK HS100 Wi-Fi smart plug

[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 <ip of plug> <port> <command>, 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
This post appeared first on George's Techblog

27 comments :

  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

    ReplyDelete
  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

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

      Delete
    2. This comment has been removed by the author.

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

    ReplyDelete
    Replies
    1. 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?

      Delete
    2. 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.

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

    ReplyDelete
    Replies
    1. ...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=="

      Delete
    2. 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... :)

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

      Delete
    4. This comment has been removed by the author.

      Delete
    5. 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 <stdio.h>

      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");
      }

      Delete
    6. Thanks Thomas Baust. That thing is seriously reporting an GPS location?

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

      Delete
    8. This comment has been removed by the author.

      Delete
    9. Unbelievable but true, I managed to convert your programme into a bash script and integrate it with the rest of the plug script.

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

      Delete
  5. any examples of how you query system information? thank you!

    ReplyDelete
    Replies
    1. Hi Sean, see the discussion with Thomas Baust earlier in this thread.

      Delete
  6. This comment has been removed by a blog administrator.

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

    ReplyDelete
  8. How's the Raspberry Pi as an access point post coming along ? ;-)

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

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

    ReplyDelete
  10. This comment has been removed by the author.

    ReplyDelete

You might also like...