/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.core.plugin.telemetry.handlers;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.kv.*;
import org.thingsboard.server.extensions.api.exception.UnauthorizedException;
import org.thingsboard.server.extensions.api.plugins.PluginCallback;
import org.thingsboard.server.extensions.api.plugins.PluginContext;
import org.thingsboard.server.extensions.api.plugins.handlers.DefaultWebsocketMsgHandler;
import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
import org.thingsboard.server.extensions.api.plugins.ws.WsSessionMetaData;
import org.thingsboard.server.extensions.api.plugins.ws.msg.BinaryPluginWebSocketMsg;
import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
import org.thingsboard.server.extensions.api.plugins.ws.msg.TextPluginWebSocketMsg;
import org.thingsboard.server.extensions.core.plugin.telemetry.SubscriptionManager;
import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.*;
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionErrorCode;
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState;
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionUpdate;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author Andrew Shvayka
*/
@Slf4j
public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
private static final int UNKNOWN_SUBSCRIPTION_ID = 0;
public static final int DEFAULT_LIMIT = 100;
public static final Aggregation DEFAULT_AGGREGATION = Aggregation.NONE;
private final SubscriptionManager subscriptionManager;
public TelemetryWebsocketMsgHandler(SubscriptionManager subscriptionManager) {
this.subscriptionManager = subscriptionManager;
}
@Override
protected void handleWebSocketMsg(PluginContext ctx, PluginWebsocketSessionRef sessionRef, PluginWebsocketMsg<?> wsMsg) {
try {
TelemetryPluginCmdsWrapper cmdsWrapper = null;
if (wsMsg instanceof TextPluginWebSocketMsg) {
TextPluginWebSocketMsg textMsg = (TextPluginWebSocketMsg) wsMsg;
cmdsWrapper = jsonMapper.readValue(textMsg.getPayload(), TelemetryPluginCmdsWrapper.class);
} else if (wsMsg instanceof BinaryPluginWebSocketMsg) {
throw new IllegalStateException("Not Implemented!");
// TODO: add support of BSON here based on
// https://github.com/michel-kraemer/bson4jackson
}
if (cmdsWrapper != null) {
if (cmdsWrapper.getAttrSubCmds() != null) {
cmdsWrapper.getAttrSubCmds().forEach(cmd -> handleWsAttributesSubscriptionCmd(ctx, sessionRef, cmd));
}
if (cmdsWrapper.getTsSubCmds() != null) {
cmdsWrapper.getTsSubCmds().forEach(cmd -> handleWsTimeseriesSubscriptionCmd(ctx, sessionRef, cmd));
}
if (cmdsWrapper.getHistoryCmds() != null) {
cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(ctx, sessionRef, cmd));
}
}
} catch (IOException e) {
log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e);
SubscriptionUpdate update = new SubscriptionUpdate(UNKNOWN_SUBSCRIPTION_ID, SubscriptionErrorCode.INTERNAL_ERROR,
"Session meta-data not found!");
sendWsMsg(ctx, sessionRef, update);
}
}
@Override
protected void cleanupWebSocketSession(PluginContext ctx, String sessionId) {
subscriptionManager.cleanupLocalWsSessionSubscriptions(ctx, sessionId);
}
private void handleWsAttributesSubscriptionCmd(PluginContext ctx, PluginWebsocketSessionRef sessionRef, AttributesSubscriptionCmd cmd) {
String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd);
if (validateSessionMetadata(ctx, sessionRef, cmd, sessionId)) {
if (cmd.isUnsubscribe()) {
unsubscribe(ctx, cmd, sessionId);
} else if (validateSubscriptionCmd(ctx, sessionRef, cmd)) {
log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), cmd.getDeviceId());
DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
Optional<Set<String>> keysOptional = getKeys(cmd);
SubscriptionState sub;
if (keysOptional.isPresent()) {
List<String> keys = new ArrayList<>(keysOptional.get());
PluginCallback<List<AttributeKvEntry>> callback = new PluginCallback<List<AttributeKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<AttributeKvEntry> data) {
List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
Map<String, Long> subState = new HashMap<>(keys.size());
keys.forEach(key -> subState.put(key, 0L));
attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, false, subState);
subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to fetch attributes!", e);
SubscriptionUpdate update;
if (UnauthorizedException.class.isInstance(e)) {
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
} else {
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
"Failed to fetch attributes!");
}
sendWsMsg(ctx, sessionRef, update);
}
};
if (StringUtils.isEmpty(cmd.getScope())) {
ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), keys, callback);
} else {
ctx.loadAttributes(deviceId, cmd.getScope(), keys, callback);
}
} else {
PluginCallback<List<AttributeKvEntry>> callback = new PluginCallback<List<AttributeKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<AttributeKvEntry> data) {
List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
Map<String, Long> subState = new HashMap<>(attributesData.size());
attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, true, subState);
subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to fetch attributes!", e);
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
"Failed to fetch attributes!");
sendWsMsg(ctx, sessionRef, update);
}
};
if (StringUtils.isEmpty(cmd.getScope())) {
ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), callback);
} else {
ctx.loadAttributes(deviceId, cmd.getScope(), callback);
}
}
}
}
}
private void handleWsTimeseriesSubscriptionCmd(PluginContext ctx, PluginWebsocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd) {
String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd);
if (validateSessionMetadata(ctx, sessionRef, cmd, sessionId)) {
if (cmd.isUnsubscribe()) {
unsubscribe(ctx, cmd, sessionId);
} else if (validateSubscriptionCmd(ctx, sessionRef, cmd)) {
DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
Optional<Set<String>> keysOptional = getKeys(cmd);
if (keysOptional.isPresent()) {
long startTs;
if (cmd.getTimeWindow() > 0) {
List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), cmd.getDeviceId());
startTs = cmd.getStartTs();
long endTs = cmd.getStartTs() + cmd.getTimeWindow();
List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
ctx.loadTimeseries(deviceId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys));
} else {
List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
startTs = System.currentTimeMillis();
log.debug("[{}] fetching latest timeseries data for keys: ({}) for device : {}", sessionId, cmd.getKeys(), cmd.getDeviceId());
ctx.loadLatestTimeseries(deviceId, keys, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys));
}
} else {
ctx.loadLatestTimeseries(deviceId, new PluginCallback<List<TsKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
Map<String, Long> subState = new HashMap<>(data.size());
data.forEach(v -> subState.put(v.getKey(), v.getTs()));
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, true, subState);
subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
SubscriptionUpdate update;
if (UnauthorizedException.class.isInstance(e)) {
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
} else {
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
"Failed to fetch data!");
}
sendWsMsg(ctx, sessionRef, update);
}
});
}
}
}
}
private PluginCallback<List<TsKvEntry>> getSubscriptionCallback(final PluginWebsocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final DeviceId deviceId, final long startTs, final List<String> keys) {
return new PluginCallback<List<TsKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
Map<String, Long> subState = new HashMap<>(keys.size());
keys.forEach(key -> subState.put(key, startTs));
data.forEach(v -> subState.put(v.getKey(), v.getTs()));
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, false, subState);
subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to fetch data!", e);
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
"Failed to fetch data!");
sendWsMsg(ctx, sessionRef, update);
}
};
}
private void handleWsHistoryCmd(PluginContext ctx, PluginWebsocketSessionRef sessionRef, GetHistoryCmd cmd) {
String sessionId = sessionRef.getSessionId();
WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
if (sessionMD == null) {
log.warn("[{}] Session meta data not found. ", sessionId);
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
"Session meta-data not found!");
sendWsMsg(ctx, sessionRef, update);
return;
}
if (cmd.getDeviceId() == null || cmd.getDeviceId().isEmpty()) {
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Device id is empty!");
sendWsMsg(ctx, sessionRef, update);
return;
}
if (cmd.getKeys() == null || cmd.getKeys().isEmpty()) {
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Keys are empty!");
sendWsMsg(ctx, sessionRef, update);
return;
}
DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
SubscriptionUpdate update;
if (UnauthorizedException.class.isInstance(e)) {
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
} else {
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
"Failed to fetch data!");
}
sendWsMsg(ctx, sessionRef, update);
}
});
}
private static Aggregation getAggregation(String agg) {
return StringUtils.isEmpty(agg) ? DEFAULT_AGGREGATION : Aggregation.valueOf(agg);
}
private int getLimit(int limit) {
return limit == 0 ? DEFAULT_LIMIT : limit;
}
private boolean validateSessionMetadata(PluginContext ctx, PluginWebsocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
if (sessionMD == null) {
log.warn("[{}] Session meta data not found. ", sessionId);
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
"Session meta-data not found!");
sendWsMsg(ctx, sessionRef, update);
return false;
} else {
return true;
}
}
private void unsubscribe(PluginContext ctx, SubscriptionCmd cmd, String sessionId) {
if (cmd.getDeviceId() == null || cmd.getDeviceId().isEmpty()) {
cleanupWebSocketSession(ctx, sessionId);
} else {
subscriptionManager.removeSubscription(ctx, sessionId, cmd.getCmdId());
}
}
private boolean validateSubscriptionCmd(PluginContext ctx, PluginWebsocketSessionRef sessionRef, SubscriptionCmd cmd) {
if (cmd.getDeviceId() == null || cmd.getDeviceId().isEmpty()) {
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Device id is empty!");
sendWsMsg(ctx, sessionRef, update);
return false;
}
return true;
}
private void sendWsMsg(PluginContext ctx, PluginWebsocketSessionRef sessionRef, SubscriptionUpdate update) {
TextPluginWebSocketMsg reply;
try {
reply = new TextPluginWebSocketMsg(sessionRef, jsonMapper.writeValueAsString(update));
ctx.send(reply);
} catch (JsonProcessingException e) {
log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e);
} catch (IOException e) {
log.warn("[{}] Failed to send reply: {}", sessionRef.getSessionId(), update, e);
}
}
public static Optional<Set<String>> getKeys(TelemetryPluginCmd cmd) {
if (!StringUtils.isEmpty(cmd.getKeys())) {
Set<String> keys = new HashSet<>();
for (String key : cmd.getKeys().split(",")) {
keys.add(key);
}
return Optional.of(keys);
} else {
return Optional.empty();
}
}
public void sendWsMsg(PluginContext ctx, String sessionId, SubscriptionUpdate update) {
WsSessionMetaData md = wsSessionsMap.get(sessionId);
if (md != null) {
sendWsMsg(ctx, md.getSessionRef(), update);
}
}
}