Skip to content

Instantly share code, notes, and snippets.

@agent4788
Last active November 17, 2023 13:34
Show Gist options
  • Star 25 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save agent4788/81beb25cdcdbf7e9371361ca87d3b04a to your computer and use it in GitHub Desktop.
Save agent4788/81beb25cdcdbf7e9371361ca87d3b04a to your computer and use it in GitHub Desktop.
Mit dieser kleinen API kann man die TP-Link HS100 und HS110 Wlan Steckdosen direkt aus Java ansteuern.
package de.test;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
/**
* TP-Link HS100
*
* @author Oliver Kleditzsch
* @copyright Copyright (c) 2016, Oliver Kleditzsch
*/
public class HS100 {
public static final String COMMAND_SWITCH_ON = "{\"system\":{\"set_relay_state\":{\"state\":1}}}}";
public static final String COMMAND_SWITCH_OFF = "{\"system\":{\"set_relay_state\":{\"state\":0}}}}";
public static final String COMMAND_INFO = "{\"system\":{\"get_sysinfo\":null}}";
/**
* Status an
*/
public static final int STATE_ON = 1;
/**
* Status aus
*/
public static final int STATE_OFF = 2;
/**
* IP Adresse der Steckdose
*/
private String ip;
/**
* Port
*/
private int port = 9999;
/**
* @param ip IP Adresse
*/
public HS100(String ip) {
this.ip = ip;
}
/**
* @param ip IP Adresse
* @param port TCP Port Nummer
*/
public HS100(String ip, int port) {
this.ip = ip;
this.port = port;
}
/**
* gibt die IP Adresse der Steckdose zurück
*
* @return IP Adresse
*/
public String getIp() {
return ip;
}
/**
* setzt die IP Adresse der Steckdose
*
* @param ip IP Adresse
*/
public void setIp(String ip) {
this.ip = ip;
}
/**
* gibt den Port der Steckdose zurück
*
* @return Port
*/
public int getPort() {
return port;
}
/**
* setzt den Port der Steckdose
*
* @param port Port
*/
public void setPort(int port) {
this.port = port;
}
/**
* prüft ob die Steckdose im Netzwerk erreichbar ist
*
* @return Erreichbarkeit
*/
public boolean isPresent() {
try {
InetAddress ip = InetAddress.getByName(getIp());
return ip.isReachable(500);
} catch (IOException ex) {}
return false;
}
/**
* prüft ob die Steckdose im Netzwerk erreichbar ist
*
* @param timeout Timeout (Wartezeit in ms)
* @return Erreichbarkeit
*/
public boolean isPresent(int timeout) {
try {
InetAddress ip = InetAddress.getByName(getIp());
return ip.isReachable(timeout);
} catch (IOException ex) {}
return false;
}
/**
* sendet einen Einschaltbefehl
*
* @return true bei Erfolg
*/
public boolean switchOn() throws IOException {
String jsonData = sendCommand(COMMAND_SWITCH_ON);
if(jsonData.length() > 0) {
JsonObject jo = new JsonParser().parse(jsonData).getAsJsonObject();
int errorCode = jo.get("system").getAsJsonObject().get("set_relay_state").getAsJsonObject().get("err_code").getAsInt();
return errorCode == 0;
}
return false;
}
/**
* sendet einen Ausschaltbefehl
*
* @return true bei Erfolg
*/
public boolean switchOff() throws IOException {
String jsonData = sendCommand(COMMAND_SWITCH_OFF);
if(jsonData.length() > 0) {
JsonObject jo = new JsonParser().parse(jsonData).getAsJsonObject();
int errorCode = jo.get("system").getAsJsonObject().get("set_relay_state").getAsJsonObject().get("err_code").getAsInt();
return errorCode == 0;
}
return false;
}
/**
* fragt den aktuellen Status der Steckodse ab
*
* @return STATE_ON oder STATE_OFF
*/
public int readState() throws IOException {
String jsonData = sendCommand(COMMAND_INFO);
if(jsonData.length() > 0) {
JsonObject jo = new JsonParser().parse(jsonData).getAsJsonObject();
int state = jo.get("system").getAsJsonObject().get("get_sysinfo").getAsJsonObject().get("relay_state").getAsInt();
return state == 1 ? STATE_ON : STATE_OFF;
}
return STATE_OFF;
}
/**
* gibt eine Map mit den Systeminformationen der Steckdose zurück
*
* @return Map mit Systeminformationen
*/
public Map<String, String> getInfo() throws IOException {
Map<String, String> result = new HashMap<>();
String jsonData = sendCommand(COMMAND_INFO);
if(jsonData.length() > 0) {
JsonObject jo = new JsonParser().parse(jsonData).getAsJsonObject();
JsonObject systemInfo = jo.get("system").getAsJsonObject().get("get_sysinfo").getAsJsonObject();
for(Map.Entry<String, JsonElement> entry : systemInfo.entrySet()) {
result.put(entry.getKey(), entry.getValue().getAsString());
}
}
return result;
}
/**
* sendet eine Befel an die Steckdose
*
* @param command Befehl
* @return Json STring mit dem Antwortobjekt
* @throws IOException
*/
protected String sendCommand(String command) throws IOException {
Socket socket = new Socket(ip, port);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(encryptWithHeader(command));
InputStream inputStream = socket.getInputStream();
String data = decrypt(inputStream);
outputStream.close();
inputStream.close();
socket.close();
return data;
}
private String decrypt(InputStream inputStream) throws IOException {
int in;
int key = 0x2B;
int nextKey;
StringBuilder sb = new StringBuilder();
while((in = inputStream.read()) != -1) {
nextKey = in;
in = in ^ key;
key = nextKey;
sb.append((char) in);
}
return "{" + sb.toString().substring(5);
}
private int[] encrypt(String command) {
int[] buffer = new int[command.length()];
int key = 0xAB;
for(int i = 0; i < command.length(); i++) {
buffer[i] = command.charAt(i) ^ key;
key = buffer[i];
}
return buffer;
}
private byte[] encryptWithHeader(String command) {
int[] data = encrypt(command);
byte[] bufferHeader = ByteBuffer.allocate(4).putInt(command.length()).array();
ByteBuffer byteBuffer = ByteBuffer.allocate(bufferHeader.length + data.length).put(bufferHeader);
for(int in : data) {
byteBuffer.put((byte) in);
}
return byteBuffer.array();
}
}
package de.test;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
/**
* TP-Link HS100
*
* @author Oliver Kleditzsch
* @copyright Copyright (c) 2016, Oliver Kleditzsch
*/
public class HS110 extends HS100 {
private static final String COMMAND_ENERGY = "{\"emeter\":{\"get_realtime\":null}}";
/**
* Energiedaten
*/
public class EnergyData {
/**
* aktuelle Spannung in Volt
*/
private double nowVoltage;
/**
* aktueller Strom in Ampere
*/
private double nowCurrent;
/**
* aktuelle Leistung in Watt
*/
private double nowPower;
/**
* Energieverbrauch in Kilo Watt Stunden
*/
private double energy;
public EnergyData() {
}
/**
* @param nowVoltage aktuelle Spannung in Volt
* @param nowCurrent aktueller Strom in Ampere
* @param nowPower aktuelle Leistung in Watt
* @param energy Energieverbrauch in Kilo Watt Stunden
*/
public EnergyData(double nowVoltage, double nowCurrent, double nowPower, double energy) {
this.nowVoltage = nowVoltage;
this.nowCurrent = nowCurrent;
this.nowPower = nowPower;
this.energy = energy;
}
/**
* gibt die aktuelle Spannung zurück
*
* @return aktuelle Spannung in Volt
*/
public double getNowVoltage() {
return nowVoltage;
}
/**
* setzt die aktuelle Spannung
*
* @param nowVoltage aktuelle Spannung in Volt
*/
public void setNowVoltage(double nowVoltage) {
this.nowVoltage = nowVoltage;
}
/**
* gibt den aktuellen Strom zurück
*
* @return aktueller Strom in Ampere
*/
public double getNowCurrent() {
return nowCurrent;
}
/**
* setzt den aktuellen Strom
*
* @param nowCurrent aktueller Strom in Ampere
*/
public void setNowCurrent(double nowCurrent) {
this.nowCurrent = nowCurrent;
}
/**
* gibt die aktuelle Leistung zurück
*
* @return aktuelle Leistung in Watt
*/
public double getNowPower() {
return nowPower;
}
/**
* setzt die aktuelle Leistung
*
* @param nowPower aktuelle Leistung in Watt
*/
public void setNowPower(double nowPower) {
this.nowPower = nowPower;
}
/**
* gibt den Energieverbrauch zurück
*
* @return Energieverbrauch in Kilo Watt Stunden
*/
public double getEnergy() {
return energy;
}
/**
* setzt den Energieverbrauch
*
* @param energy Energieverbrauch in Kilo Watt Stunden
*/
public void setEnergy(double energy) {
this.energy = energy;
}
}
/**
* @param ip IP Adresse
*/
public HS110(String ip) {
super(ip);
}
/**
* @param ip IP Adresse
* @param port TCP Port Nummer
*/
public HS110(String ip, int port) {
super(ip, port);
}
/**
* gint die Energiedaten zurück
*
* @return Energiedaten
* @throws IOException
*/
public EnergyData getEnergyData() throws IOException {
EnergyData energyData = new EnergyData();
String jsonData = sendCommand(COMMAND_ENERGY);
if(jsonData.length() > 0) {
JsonObject jo = new JsonParser().parse(jsonData).getAsJsonObject();
JsonObject energyDataJson = jo.get("emeter").getAsJsonObject().get("get_realtime").getAsJsonObject();
energyData.setNowCurrent(energyDataJson.get("current").getAsDouble());
energyData.setNowVoltage(energyDataJson.get("voltage").getAsDouble());
energyData.setNowPower(energyDataJson.get("power").getAsDouble());
energyData.setEnergy(energyDataJson.get("total").getAsDouble());
}
return energyData;
}
}
package de.test;
/**
* TP-Link HS100/HS110 Beispielprogramm
*
* @author Oliver Kleditzsch
* @copyright Copyright (c) 2016, Oliver Kleditzsch
*/
import java.io.*;
import java.util.Map;
public class Test {
public static void main(String[] args) throws IOException, InterruptedException {
HS110 hs100 = new HS110("192.168.10.20");
System.out.println("Switch On");
hs100.switchOn();
System.out.println("new state: " + (hs100.readState() == HS100.STATE_ON ? "on" : "off"));
System.out.println();
Thread.sleep(2000);
System.out.println("Switch Off");
hs100.switchOff();
System.out.println("new state: " + (hs100.readState() == HS100.STATE_ON ? "on" : "off"));
System.out.println();
Thread.sleep(2000);
System.out.println("Switch On");
hs100.switchOn();
System.out.println("new state: " + (hs100.readState() == HS100.STATE_ON ? "on" : "off"));
System.out.println();
System.out.println("Energy Data:");
HS110.EnergyData energyData = hs100.getEnergyData();
System.out.println("Voltage: " + energyData.getNowVoltage() + "V");
System.out.println("Current: " + energyData.getNowCurrent() + "A");
System.out.println("Power: " + energyData.getNowPower() + "W");
System.out.println("Energy: " + energyData.getEnergy() + "kWh");
System.out.println();
System.out.println("System Data:");
Map<String, String> sysinfo = hs100.getInfo();
for(Map.Entry<String, String> entry : sysinfo.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
}
}
@rjahn
Copy link

rjahn commented Jul 17, 2017

Could you use an OpenSource license for your code?

@PredatH0r
Copy link

Thank you very much for providing this code. Having an API like this was the reason I bought a HS110 and wrote a C# implementation.
I noticed that the responses from the HS110 looks just like the requests, starting with an int32 in network byte order (big endian, highest bytes first, unlike x86). After reading these first 4 bytes, the rest is the JSON data with an initial decryption key value of 0xAB.

@agent4788
Copy link
Author

This code can be used under the MIT License

@holzkopp81
Copy link

Hallo agent4788.
Ich bastel nun schon ein paar Stunden mit Hilfe deines quell codes an einem Widget. Aber was mir irgendwie fehlt, ist die Connection vom HS110 ins WLAN. Dafür braucht es doch eine Methode, der die SSID und das Passwort übergeben wird. Oder übersehe ich da was?

MfG

holzkopp

@agent4788
Copy link
Author

Bei der Inbetriebnahme baut die Steckdose ein eigenes Wlan auf. Darin kannst du dich mit dem Smartphone oder Tablet mit der TP-Link APP anmelden und dann die Zugangsdaten zu deinem Wlan festlegen. Danach startet die Steckdose neu und loggt sich in deinem Wlan ein.
Sobald die Steckdose in deinem Wlan ist, kannst du über die API darauf zugreifen.

@jens-234
Copy link

Basically this worked for me. I had just one weird problem: After a command or query was sent and the response had entirely come in the channel wasn't shutdown. So the while((in = inputStream.read()) != -1) fired only after about a minute in my case. To change that I adapted the function so it returns once as many closing brackets have come in as have opening (-1 the one that is only added at the end).

private String decrypt(InputStream inputStream) throws IOException
{
int in;
int key = 0x2B;
int nextKey;
StringBuilder sb = new StringBuilder();
try
{
boolean allBracketsClosed = false;
while(!allBracketsClosed && (in = inputStream.read()) != -1)
{
nextKey = in;
in = in ^ key;
key = nextKey;
sb.append((char) in);
// System.out.println((char) in + " " + sb.chars().filter(num -> num == '{').count() + " " + sb.chars().filter(num -> num == '}').count());
if(sb.length() > 0)
{
allBracketsClosed = sb.chars().filter(num -> num == '{').count() > 0
&&
(
sb.chars().filter(num -> num == '{').count()
==
sb.chars().filter(num -> num == '}').count() -1
);
}
}
}
catch(SocketTimeoutException e)
{}
// System.out.println("done");
return "{" + sb.toString().substring(5);
}

@4thFourthHorseman
Copy link

Hallo,
funktioniert der Code mit der aktuellen Firmware der Hs100 noch?

@agent4788
Copy link
Author

kann ich nicht genau sagen, ich habe schon eweig kein FW Update mehr gemacht. Bei den letzten Updates hat es aber immer funktioniert.

@timkn
Copy link

timkn commented Mar 13, 2022

Zumindest switch on/off funktioniert noch mit der aktuellsten Version (bei mir 1.5.6).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment