
io.relayr.java.websocket.WebSocketClient Maven / Gradle / Ivy
package io.relayr.java.websocket;
import com.google.gson.Gson;
import org.eclipse.paho.client.mqttv3.MqttException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Singleton;
import io.relayr.java.api.ChannelApi;
import io.relayr.java.model.DataPackage;
import io.relayr.java.model.Device;
import io.relayr.java.model.action.Action;
import io.relayr.java.model.action.Command;
import io.relayr.java.model.action.Configuration;
import io.relayr.java.model.action.Reading;
import io.relayr.java.model.channel.ChannelDefinition;
import io.relayr.java.model.channel.DataChannel;
import io.relayr.java.model.channel.PublishChannel;
import io.relayr.java.websocket.error.MqttDisconnectException;
import rx.Observable;
import rx.Observer;
import rx.Subscriber;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
import rx.subjects.AsyncSubject;
import rx.subjects.PublishSubject;
import rx.subjects.ReplaySubject;
import rx.subjects.Subject;
import static io.relayr.java.model.channel.ChannelTransport.MQTT;
@Singleton
public class WebSocketClient {
private final ChannelApi channelApi;
private final WebSocket webSocket;
private final Map subChannels = new HashMap<>();
final Map> subObservers = new HashMap<>();
final Map pubChannels = new HashMap<>();
final Map> pubObservers = new HashMap<>();
final Map> channelCreation = new HashMap<>();
final Map actionObservers = new HashMap<>();
@Inject
public WebSocketClient(ChannelApi api, WebSocketFactory factory) {
channelApi = api;
webSocket = factory.createWebSocket();
}
/**
* Subscribe to get {@link Reading} from device.
* @param device device to subscribe to
*/
public Observable subscribe(Device device) {
if (device == null || device.getId() == null) return Observable.empty();
return subscribe(device.getId());
}
/**
* Subscribe to get {@link Reading} from device.
* @param deviceId to subscribe to
*/
public Observable subscribe(String deviceId) {
if (deviceId == null) return Observable.empty();
if (!subObservers.containsKey(deviceId)) return createSubObserver(deviceId);
else return subObservers.get(deviceId);
}
private synchronized Observable createSubObserver(final String deviceId) {
final PublishSubject subject = PublishSubject.create();
subObservers.put(deviceId, subject);
channelApi.create(new ChannelDefinition(deviceId, MQTT))
.flatMap(new Func1>() {
@Override public Observable call(final DataChannel channel) {
return webSocket.createClient(channel);
}
})
.subscribeOn(Schedulers.newThread())
.subscribe(new Subscriber() {
@Override public void onCompleted() {}
@Override public void onError(Throwable e) {
System.out.printf("Failed to create MQT client.");
subObservers.remove(deviceId);
subject.onError(e);
}
@Override public void onNext(DataChannel channel) {
subscribeToChannel(channel, deviceId, subject);
}
});
return subject.doOnError(new Action1() {
@Override public void call(Throwable throwable) {
unSubscribe(deviceId);
}
});
}
private void subscribeToChannel(final DataChannel channel, final String deviceId,
final PublishSubject subject) {
webSocket.subscribe(channel.getTopic(), channel.getId(), new WebSocketCallback() {
@Override public void connectCallback(Object message) {
if (!subChannels.containsKey(deviceId))
subChannels.put(deviceId, channel);
}
@Override public void disconnectCallback(Object message) {
subject.onError((Throwable) message);
subChannels.remove(deviceId);
subObservers.remove(deviceId);
}
@Override public void successCallback(Object message) {
DataPackage dataPackage = new Gson().fromJson(message.toString(), DataPackage.class);
for (DataPackage.Data dataPoint : dataPackage.readings) {
subject.onNext(new Reading(dataPackage.received, dataPoint.recorded,
dataPoint.meaning, dataPoint.path, dataPoint.value));
}
}
@Override
public void errorCallback(Throwable e) {
subject.onError(e);
subChannels.remove(deviceId);
subObservers.remove(deviceId);
}
});
}
public void unSubscribe(final String deviceId) {
if (subObservers.containsKey(deviceId)) {
subObservers.get(deviceId).onCompleted();
subObservers.remove(deviceId);
}
if (!subChannels.isEmpty() && subChannels.containsKey(deviceId))
if (webSocket.unSubscribe(subChannels.get(deviceId).getCredentials().getTopic()))
subChannels.remove(deviceId);
}
public synchronized Observable publish(final String deviceId, final Reading payload) {
final AsyncSubject publishSubject = pubObservers.get(deviceId);
if (publishSubject != null) {
publishData(deviceId, payload, publishSubject);
} else {
if (channelCreation.get(deviceId) != null) return Observable.empty();
final AsyncSubject subject = AsyncSubject.create();
createPubChannel(deviceId)
.subscribeOn(Schedulers.newThread())
.subscribe(new Observer() {
@Override public void onCompleted() {}
@Override public void onError(Throwable e) {
pubChannels.remove(deviceId);
pubObservers.remove(deviceId);
channelCreation.remove(deviceId);
subject.onError(e);
}
@Override public void onNext(DataChannel channel) {
if (!pubObservers.containsKey(deviceId))
pubObservers.put(deviceId, subject);
if (!pubChannels.containsKey(deviceId))
pubChannels.put(deviceId, channel);
channelCreation.remove(deviceId);
publishData(deviceId, payload, subject);
}
});
return clearIfError(subject, deviceId);
}
return publishSubject;
}
private void publishData(String deviceId, Reading payload, AsyncSubject subject) {
try {
final boolean success = webSocket.publish(deviceId, pubChannels.get(deviceId).getCredentials().getTopic() + "data",
new Gson().toJson(payload));
subject.onNext(success);
subject.onCompleted(); //Called because async observable is used
} catch (MqttException e) {
removePublisher(deviceId);
subject.onError(new Exception("WebSocketClient - MqttException", e));
} catch (Exception ae) {
removePublisher(deviceId);
subject.onError(new Exception("WebSocketClient - Unknown exception", ae));
}
}
/**
* Subscribe to get {@link Action} from device.
* {@link Action} can be {@link Command} or {@link Configuration}
* @param deviceId to subscribe to
*/
public Observable subscribeToCommands(String deviceId) {
if (deviceId == null) return Observable.empty();
if (!actionObservers.containsKey(deviceId)) {
final PublishSubject subject = PublishSubject.create();
return createActionObserver(deviceId, subject, Command.class);
} else {
return actionObservers.get(deviceId);
}
}
/**
* Subscribe to get {@link Action} from device.
* {@link Action} can be {@link Command} or {@link Configuration}
* @param deviceId to subscribe to
*/
public Observable subscribeToConfigurations(String deviceId) {
if (deviceId == null) return Observable.empty();
if (!actionObservers.containsKey(deviceId)) {
final PublishSubject subject = PublishSubject.create();
return createActionObserver(deviceId, subject, Configuration.class);
} else {
return actionObservers.get(deviceId);
}
}
synchronized Observable createActionObserver(final String deviceId,
final PublishSubject subject,
final Class type) {
actionObservers.put(deviceId, subject);
createPubChannel(deviceId)
.subscribeOn(Schedulers.newThread())
.subscribe(new Subscriber() {
@Override public void onCompleted() {}
@Override public void onError(Throwable e) {
System.err.println("Failed to create publish channel");
subject.onError(e);
pubChannels.remove(deviceId);
actionObservers.remove(deviceId);
channelCreation.remove(deviceId);
}
@Override public void onNext(DataChannel channel) {
if (!pubChannels.containsKey(deviceId)) pubChannels.put(deviceId, channel);
channelCreation.remove(deviceId);
final String topic = channel.getTopic() + (type == Command.class ? "cmd" : "conf");
final boolean subscribe = webSocket.subscribeAction(topic,
channel.getCredentials().getUser(), channel.getId(),
new WebSocketCallback() {
@Override public void connectCallback(Object message) {}
@Override public void disconnectCallback(Object message) {
actionObservers.remove(deviceId);
subject.onError((Throwable) message);
}
@Override public void successCallback(Object message) {
subject.onNext(new Gson().fromJson(message.toString(), type));
}
@Override
public void errorCallback(Throwable e) {
e.printStackTrace();
actionObservers.remove(deviceId);
subject.onError(e);
}
});
if (!subscribe) {
actionObservers.remove(deviceId);
subject.onError(new Throwable("Failed to subscribe to MQTT"));
}
}
});
return clearIfError(subject, deviceId);
}
synchronized Observable createPubChannel(final String deviceId) {
ReplaySubject subject = channelCreation.get(deviceId);
if (subject == null) {
subject = ReplaySubject.create();
channelCreation.put(deviceId, subject);
channelApi.createForDevice(new ChannelDefinition(deviceId, MQTT), deviceId)
.timeout(7, TimeUnit.SECONDS)
.subscribeOn(Schedulers.newThread())
.flatMap(new Func1>() {
@Override public Observable call(PublishChannel channel) {
return webSocket.createPublishClient(channel);
}
})
.subscribe(new Action1() {
@Override public void call(DataChannel dataChannel) {
channelCreation.get(deviceId).onNext(dataChannel);
}
}, new Action1() {
@Override public void call(Throwable e) {
System.err.println("Failed to create publish MQTT client");
channelCreation.get(deviceId).onError(e);
removePublisher(deviceId);
}
});
}
return channelCreation.get(deviceId);
}
private Observable clearIfError(Subject subject, final String deviceId) {
return subject.doOnError(new Action1() {
@Override public void call(Throwable throwable) {
pubChannels.remove(deviceId);
pubObservers.remove(deviceId);
}
});
}
public void clean() {
if (webSocket != null) webSocket.reset();
if (subChannels != null) subChannels.clear();
if (subObservers != null) subObservers.clear();
if (pubChannels != null) pubChannels.clear();
if (pubObservers != null) pubObservers.clear();
if (channelCreation != null) channelCreation.clear();
if (actionObservers != null) actionObservers.clear();
}
public void removePublisher(String deviceId) {
if (pubChannels.get(deviceId) != null) pubChannels.remove(deviceId);
if (pubObservers.get(deviceId) != null) pubObservers.remove(deviceId);
if (channelCreation.get(deviceId) != null) channelCreation.remove(deviceId);
if (webSocket != null) webSocket.removePublishClient(deviceId);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy