Skip to content

Commit

Permalink
Add major improvements
Browse files Browse the repository at this point in the history
Enable gateway password configuration
Introduce pushing data to devices
Improve update handling
Add new statuses for XiaomiSwitchButton
Add on/off functionality to XiaomiSocket
Bump version to 0.2
  • Loading branch information
valashko committed Sep 7, 2018
1 parent 8e21847 commit 70a3585
Show file tree
Hide file tree
Showing 18 changed files with 200 additions and 50 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
}

group 'com.valashko.xaapi'
version '0.1'
version '0.2'

sourceCompatibility = 1.8

Expand Down
23 changes: 23 additions & 0 deletions src/main/java/com/valashko/xaapi/command/WriteCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.valashko.xaapi.command;

import com.google.gson.JsonObject;
import com.valashko.xaapi.device.SlaveDevice;

import java.nio.charset.StandardCharsets;

public class WriteCommand implements ICommand {
private SlaveDevice device;
private JsonObject data;

public WriteCommand(SlaveDevice device, JsonObject data, String key) {
this.device = device;
this.data = data;
data.addProperty("key", key);
}

@Override
public byte[] toBytes() {
String what = "{{\"cmd\":\"write\", \"sid\":\""+ device.getSid() +"\", \"data\":" + data + "}}";
return what.getBytes(StandardCharsets.US_ASCII);
}
}
1 change: 0 additions & 1 deletion src/main/java/com/valashko/xaapi/device/BuiltinDevice.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.google.gson.JsonParser;
import com.valashko.xaapi.XaapiException;
import sun.reflect.generics.reflectiveObjects.NotImplementedException;

public abstract class BuiltinDevice {

Expand Down
4 changes: 3 additions & 1 deletion src/main/java/com/valashko/xaapi/device/SlaveDevice.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ public enum Type {
}

protected static JsonParser JSON_PARSER = new JsonParser();
protected XiaomiGateway gateway;
private String sid;
private Type type;

public SlaveDevice(String sid, Type type) {
public SlaveDevice(XiaomiGateway gateway, String sid, Type type) {
this.gateway = gateway;
this.sid = sid;
this.type = type;
}
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/valashko/xaapi/device/Utility.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.valashko.xaapi.device;

public class Utility {

public static String toHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X", b));
}
return sb.toString();
}
}
4 changes: 2 additions & 2 deletions src/main/java/com/valashko/xaapi/device/XiaomiCube.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ public enum Action {
private HashMap<SubscriptionToken, Consumer<String>> actionsCallbacks = new HashMap<>();
private HashMap<SubscriptionToken, Consumer<Double>> rotationCallbacks = new HashMap<>();

public XiaomiCube(String sid) {
super(sid, Type.XiaomiCube);
public XiaomiCube(XiaomiGateway gateway, String sid) {
super(gateway, sid, Type.XiaomiCube);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ public enum Status {

private Status lastStatus;

public XiaomiDoorWindowSensor(String sid) {
super(sid, Type.XiaomiCube);
public XiaomiDoorWindowSensor(XiaomiGateway gateway, String sid) {
super(gateway, sid, Type.XiaomiCube);
}

@Override
Expand Down
124 changes: 99 additions & 25 deletions src/main/java/com/valashko/xaapi/device/XiaomiGateway.java
Original file line number Diff line number Diff line change
@@ -1,31 +1,43 @@
package com.valashko.xaapi.device;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.valashko.xaapi.XaapiException;
import com.valashko.xaapi.channel.DirectChannel;
import com.valashko.xaapi.channel.IncomingMulticastChannel;
import com.valashko.xaapi.command.WhoisCommand;
import com.valashko.xaapi.command.GetIdListCommand;
import com.valashko.xaapi.command.ReadCommand;
import com.valashko.xaapi.reply.GetIdListReply;
import com.valashko.xaapi.reply.ReadReply;
import com.valashko.xaapi.reply.WhoisReply;
import com.valashko.xaapi.channel.*;
import com.valashko.xaapi.command.*;
import com.valashko.xaapi.reply.*;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Executor;

public class XiaomiGateway {

private static final String GROUP = "224.0.0.50";
private static final int PORT = 9898;
private static final int PORT_DISCOVERY = 4321;
private static final byte[] IV =
{ 0x17, (byte)0x99, 0x6d, 0x09, 0x3d, 0x28, (byte)0xdd, (byte)0xb3,
(byte)0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58, 0x56, 0x2e};

private static final Gson GSON = new Gson();

private String sid;
private Optional<String> key = Optional.empty();
private Cipher cipher;
private IncomingMulticastChannel incomingMulticastChannel;
private DirectChannel directChannel;
private XiaomiGatewayLight builtinLight;
Expand All @@ -51,12 +63,33 @@ public XiaomiGateway(String ip) throws IOException, XaapiException {
configureBuiltinDevices();
queryDevices();
}
public XiaomiGateway(String ip, String password) throws IOException, XaapiException {
this(ip);
configureCipher(password);
}

private void configureBuiltinDevices() {
builtinLight = new XiaomiGatewayLight();
builtinIlluminationSensor = new XiaomiGatewayIlluminationSensor();
}

private void configureCipher(String password) throws XaapiException {
try {
cipher = Cipher.getInstance("AES/CBC/NoPadding");
final SecretKeySpec keySpec = new SecretKeySpec(password.getBytes(), "AES");
final IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
} catch (NoSuchAlgorithmException e) {
throw new XaapiException("Cipher error: " + e.getMessage());
} catch (NoSuchPaddingException e) {
throw new XaapiException("Cipher error: " + e.getMessage());
} catch (InvalidAlgorithmParameterException e) {
throw new XaapiException("Cipher error: " + e.getMessage());
} catch (InvalidKeyException e) {
throw new XaapiException("Cipher error: " + e.getMessage());
}
}

private void queryDevices() throws XaapiException {
try {
directChannel.send(new GetIdListCommand().toBytes());
Expand Down Expand Up @@ -93,6 +126,34 @@ private boolean isMyself(String sid) {
return sid.equals(this.sid);
}

private void updateKey(String token) throws XaapiException {
if(cipher != null) {
try {
String keyAsHexString = Utility.toHexString(cipher.doFinal(token.getBytes(StandardCharsets.US_ASCII)));
key = Optional.of(keyAsHexString);
} catch (IllegalBlockSizeException e) {
throw new XaapiException("Cipher error: " + e.getMessage());
} catch (BadPaddingException e) {
throw new XaapiException("Cipher error: " + e.getMessage());
}
} else {
throw new XaapiException("Unable to update key without a cipher. Did you forget to set a password?");
}
}

void sendDataToDevice(SlaveDevice device, JsonObject data) throws XaapiException {
if(key.isPresent()) {
try {
directChannel.send(new WriteCommand(device, data, key.get()).toBytes());
// TODO add handling for expired key
} catch (IOException e) {
throw new XaapiException("Network error: " + e.getMessage());
}
} else {
throw new XaapiException("Unable to control device without a key. Did you forget to set a password?");
}
}

private SlaveDevice readDevice(String sid) throws XaapiException {
try {
directChannel.send(new ReadCommand(sid).toBytes());
Expand All @@ -101,23 +162,23 @@ private SlaveDevice readDevice(String sid) throws XaapiException {

switch(reply.model) {
case "cube":
XiaomiCube cube = new XiaomiCube(sid);
XiaomiCube cube = new XiaomiCube(this, sid);
cube.update(reply.data);
return cube;
case "magnet":
XiaomiDoorWindowSensor magnet = new XiaomiDoorWindowSensor(sid);
XiaomiDoorWindowSensor magnet = new XiaomiDoorWindowSensor(this, sid);
magnet.update(reply.data);
return magnet;
case "plug":
XiaomiSocket plug = new XiaomiSocket(sid);
XiaomiSocket plug = new XiaomiSocket(this, sid);
plug.update(reply.data);
return plug;
case "motion":
XiaomiMotionSensor motion = new XiaomiMotionSensor(sid);
XiaomiMotionSensor motion = new XiaomiMotionSensor(this, sid);
motion.update(reply.data);
return motion;
case "switch":
XiaomiSwitchButton button = new XiaomiSwitchButton(sid);
XiaomiSwitchButton button = new XiaomiSwitchButton(this, sid);
button.update(reply.data);
return button;
default:
Expand All @@ -133,7 +194,8 @@ public void startReceivingUpdates(Executor executor) {
executor.execute(() -> {
while (continueReceivingUpdates) {
try {
handleUpdate(GSON.fromJson(new String(incomingMulticastChannel.receive()), ReadReply.class));
String received = new String(incomingMulticastChannel.receive());
handleUpdate(GSON.fromJson(received, ReadReply.class), received);
} catch (SocketTimeoutException e) {
// ignore
} catch (IOException e) {
Expand All @@ -151,34 +213,46 @@ public void stopReceivingUpdates() {
continueReceivingUpdates = false;
}

private void handleUpdate(ReadReply update) throws XaapiException {
private void handleUpdate(Reply update, String received) throws XaapiException {
switch(update.cmd) {
case "report":
Report report = GSON.fromJson(received, Report.class);
if(isMyself(update.sid)) {
handleBuiltinReport(update);
handleBuiltinReport(report);
} else {
handleReport(update);
handleReport(report);
}
break;
case "heartbeat":
handleHeartbeat(update);
if(isMyself(update.sid)) {
GatewayHeartbeat gatewayHeartbeat = GSON.fromJson(received, GatewayHeartbeat.class);
handleGatewayHeartbeat(gatewayHeartbeat);
} else {
SlaveDeviceHeartbeat slaveDeviceHeartbeat = GSON.fromJson(received, SlaveDeviceHeartbeat.class);
handleSlaveDeviceHeartbeat(slaveDeviceHeartbeat);
}
break;
default:
throw new XaapiException("Unexpected update command: " + update.cmd);
}
}

private void handleReport(ReadReply update) {
// TODO handle updates about gateway itself
getDevice(update.sid).update(update.data);
private void handleReport(Report report) {
getDevice(report.sid).update(report.data);
}

private void handleBuiltinReport(Report report) {
builtinLight.update(report.data);
builtinIlluminationSensor.update(report.data);
}

private void handleBuiltinReport(ReadReply update) {
builtinLight.update(update.data);
builtinIlluminationSensor.update(update.data);
private void handleGatewayHeartbeat(GatewayHeartbeat gatewayHeartbeat) throws XaapiException {
if(cipher != null) {
updateKey(gatewayHeartbeat.token);
}
}

private void handleHeartbeat(ReadReply update) {
private void handleSlaveDeviceHeartbeat(SlaveDeviceHeartbeat slaveDeviceHeartbeat) {
// TODO implement
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ public enum Status {

private Status lastStatus;

public XiaomiMotionSensor(String sid) {
super(sid, Type.XiaomiMotionSensor);
public XiaomiMotionSensor(XiaomiGateway gateway, String sid) {
super(gateway, sid, Type.XiaomiMotionSensor);
}

@Override
Expand Down
16 changes: 14 additions & 2 deletions src/main/java/com/valashko/xaapi/device/XiaomiSocket.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ public enum Status {

private Status lastStatus;

public XiaomiSocket(String sid) {
super(sid, Type.XiaomiSocket);
public XiaomiSocket(XiaomiGateway gateway, String sid) {
super(gateway, sid, Type.XiaomiSocket);
}

@Override
Expand Down Expand Up @@ -48,4 +48,16 @@ void update(String data) {
public Status getLastStatus() {
return lastStatus;
}

public void turnOn() throws XaapiException {
JsonObject on = new JsonObject();
on.addProperty("status", "on");
gateway.sendDataToDevice(this, on);
}

public void turnOff() throws XaapiException {
JsonObject off = new JsonObject();
off.addProperty("status", "off");
gateway.sendDataToDevice(this, off);
}
}
14 changes: 11 additions & 3 deletions src/main/java/com/valashko/xaapi/device/XiaomiSwitchButton.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ public class XiaomiSwitchButton extends SlaveDevice implements IInteractiveDevic

public enum Action {
Click,
DoubleClick
DoubleClick,
LongClickPress,
LongClickRelease
}

private Action lastAction;
private HashMap<SubscriptionToken, Consumer<String>> actionsCallbacks = new HashMap<>();

public XiaomiSwitchButton(String sid) {
super(sid, Type.XiaomiSwitchButton);
public XiaomiSwitchButton(XiaomiGateway gateway, String sid) {
super(gateway, sid, Type.XiaomiSwitchButton);
}

@Override
Expand Down Expand Up @@ -53,6 +55,12 @@ private void updateWithAction(String action) throws XaapiException {
case "double_click":
lastAction = Action.DoubleClick;
break;
case "long_click_press":
lastAction = Action.LongClickPress;
break;
case "long_click_release":
lastAction = Action.LongClickRelease;
break;
default:
throw new XaapiException("Unknown action: " + action);
}
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/valashko/xaapi/reply/GatewayHeartbeat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.valashko.xaapi.reply;

public class GatewayHeartbeat extends Reply {
public String model;
public String short_id; // NB: sometimes it is a string and sometimes a number
public String token;
public String data;
}
Loading

0 comments on commit 70a3585

Please sign in to comment.