TP-Link HS105 with Java API

HS-105 Initial Setup

Connecting to WiFi using the Kasa for Mobile App

First, get the free Kasa app (iOS | Android) (in the Apple App Store on my iPhone, searched for "tp-link kasa" and found it).

I disconnected from my 5 GHz network and connected to the 'regular' (2.4 GHz) WiFi network, as apparently the TP-Link devices only speak 2.4 GHz.

Clicked the '+' (upper right corner) to add a device, and scrolled down to find the Smart Plug Mini.

My HS105 was already plugged in and powered on (and running a RetroPie ... ;) ) ... Once it started blinking orange/blue, went into Settings on the iPhone to connect to its WiFi network (TP-Link_Smart Plug_DEB5).

Back to the Kasa app.

Gave it a name (RPi Plug).

Gave it the credentials for my WiFi network.

Updated the firmware when prompted (took about 60 seconds).

Determine the Network Address

From a UNIX (Mac OS X) machine, fired up Terminal.app and ran 'ping' on my broadcast address:


$ ping -c 4 192.168.1.255

And used the MAC address (provided on a sticker on the HS105) to determine its IP address:


$ arp -a  | grep -i b0
... 192.168.1.118 ...


Ran nmap to ensure port 9999 is being used:

$ nmap -Pn 192.168.1.118

Starting Nmap 7.60 ( https://nmap.org ) at 2018-02-17 08:23 PST
Nmap scan report for HS105.local (192.168.1.118)
Host is up (0.0045s latency).
Not shown: 999 filtered ports
PORT     STATE SERVICE
9999/tcp open  abyss

Nmap done: 1 IP address (1 host up) scanned in 49.71 seconds

Java API

Downloaded hs100-master.zip from http://www.insxnity.net/hs-100.html

Created a new Java project in Eclipse, added the bundled gson-2.6.2.jar to its build path ("Build Path" / "Add External Archives..."). Dragged and dropped the 'net' directory into 'src' which auto-created the net.insxnity.hs100 package. My first test code seemed to fail:

HS100 plug = new HS100("192.168.1.118");
System.out.println("Plug present? " + plug.isPresent());

Always returned false. A bit of digging showed .isPresent() just returned the result of InetAddress.isReachable(), so I rolled my own:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;

import net.insxnity.hs100.HS100;

public class Main {

public static void main(String[] args) throws IOException {
HS100 plug = new HS100("192.168.1.118");
System.out.println("Plug present? " + isReachable( plug.getIp(), plug.getPort(), 1500));
}

static boolean isReachable(String addr, int port, int timeout) {
    try {
        Socket sock = new Socket();
        sock.connect(new InetSocketAddress(addr, port), timeout);
        return true;
    } catch (IOException ex) {
        return false;
    }
}

That worked. Folding that into hs100.java:

    /**
     * return if the Plug is valid and present
     *
     * @return present
     */
    public boolean isPresent() {
    try {
    Socket sock = new Socket();
    sock.connect(new InetSocketAddress(getIp(), getPort()), 1500);
    return true;
    } catch (IOException ex) {
        return false;
    }
//        try {
// 
//            InetAddress ip = InetAddress.getByName(getIp());
//            return ip.isReachable(1500);
//        } catch (IOException ex) {}
//        return false;
    }

That worked, too. Next step ... Watching with debugging as it seemed to be hanging on .isOn() - it wasn't hanging, but it was taking forever (45+ seconds):

public static void main(String[] args) throws IOException {
   HS100 plug = new HS100("192.168.1.118");
   System.out.println("Plug present? " + plug.isPresent() );
   System.out.println("Plug on? " + plug.isOn() );
}

Incidentally, here's the payload being returned from the HS105:

{"system":{"get_sysinfo":{"sw_ver":"1.5.1 Build 171109 Rel.165500","hw_ver":"1.0","type":"IOT.SMARTPLUGSWITCH","model":"HS105(US)","mac":"B0:4E:26:1F:DE:B5","dev_name":"Smart Wi-Fi Plug Mini","alias":"RPi Plug","relay_state":1,"on_time":2379,"active_mode":"none","feature":"TIM","updating":0,"icon_hash":"","rssi":-57,"led_off":0,"longitude_i":0,"latitude_i":0,"hwId":"E5D7E6089B060EF662783C23AE110522","fwId":"00000000000000000000000000000000","deviceId":"8006A2A8B0D68C671EEDE1DB05B6DEEB1967F775","oemId":"003E098AF0D44D4BAB796B3F6A7A830E","err_code":0}}}

(Noticed in my poking that sendCommand is hard-coded to open a socket to port 9999, changed that to use .getPort() )

My modifications to hs100.java (the full, modified, file I put in pastebin):

    protected String sendCommand(String command) throws IOException {

//      Socket socket = new Socket(getIp(), 9999); // flying-geek
        Socket socket = new Socket(getIp(), getPort() ); // flying-geek
        socket.setSoTimeout(1000); // flying-geek
...
   private String decrypt(InputStream inputStream) throws IOException {
        int in;
        int key = 0x2B;
        int nextKey;
        StringBuilder sb = new StringBuilder();
        try { // flying-geek
        while((in = inputStream.read()) != -1) {
            nextKey = in;
            in = in ^ key;
            key = nextKey;
            sb.append((char) in);
        }
        } catch(SocketTimeoutException e) {} // flying-geek
        return "{" + sb.toString().substring(5);
    }

Now, instead of hanging (blocking) at the end of the read() loop, if no further data has been read by the end of the timeout period (set at a relatively low 1 second here), it wraps things up on its own. For some reason .isOff() wasn't in the .java file I had, so I added a simple one (watched a couple of debug runs to determine the only change between the plug being switched on, and off, was relay_state set to 1 and 0, respectively, so reproducing any query, etc., from .isOn() made no sense):

    /**
     * check if the plug is off (added by flying-geek)
     *
     * @return STATE_ON oder STATE_OFF
     */
    public boolean isOff() throws IOException {
       return !isOn();
    }

With the API source code sufficiently tweaked, the following proof of concept worked like a charm:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;

import net.insxnity.hs100.HS100;

public class Main {

    public static void main(String[] args) throws IOException {
        HS100 plug = new HS100("192.168.1.118");
        System.out.println("Plug address " + plug.getIp() + ":" + plug.getPort());
        System.out.println("Plug present? " + plug.isPresent() );
        for(int i = 0; i < 3; i++) {
            System.out.println("Iteration " + i);
            System.out.println("Plug on? " + plug.isOn() );
            System.out.println("Plug off? " + plug.isOff() );
            if( plug.isOn() ) {
                System.out.println("... switching off:");
                plug.switchOff();
            } else {
                System.out.println("... switching on:");
                plug.switchOn();
            }
        }
    }
}

Plug address 192.168.1.118:9999
Plug present? true
Iteration 0
Plug on? false
Plug off? true
... switching on:
Iteration 1
Plug on? true
Plug off? false
... switching off:
Iteration 2
Plug on? false
Plug off? true
... switching on:


Comments