/**
* Copyright © 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.gateway.extensions.mqtt.client;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.internal.security.SSLSocketFactoryFactory;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.util.StringUtils;
import org.thingsboard.gateway.extensions.mqtt.client.conf.MqttBrokerConfiguration;
import org.thingsboard.gateway.extensions.mqtt.client.conf.mapping.*;
import org.thingsboard.gateway.extensions.mqtt.client.listener.MqttAttributeRequestsMessageListener;
import org.thingsboard.gateway.extensions.mqtt.client.listener.MqttDeviceStateChangeMessageListener;
import org.thingsboard.gateway.extensions.mqtt.client.listener.MqttRpcResponseMessageListener;
import org.thingsboard.gateway.extensions.mqtt.client.listener.MqttTelemetryMessageListener;
import org.thingsboard.gateway.service.AttributesUpdateListener;
import org.thingsboard.gateway.service.RpcCommandListener;
import org.thingsboard.gateway.service.data.*;
import org.thingsboard.gateway.service.GatewayService;
import org.thingsboard.server.common.data.kv.KvEntry;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
* Created by ashvayka on 24.01.17.
*/
@Slf4j
public class MqttBrokerMonitor implements MqttCallback, AttributesUpdateListener, RpcCommandListener {
private final UUID clientId = UUID.randomUUID();
private final GatewayService gateway;
private final MqttBrokerConfiguration configuration;
private final Set<String> devices;
private final AtomicInteger msgIdSeq = new AtomicInteger();
private MqttAsyncClient client;
private MqttConnectOptions clientOptions;
private Object connectLock = new Object();
//TODO: probably use newScheduledThreadPool(int threadSize) to improve performance in heavy load cases
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final Map<String, ScheduledFuture<?>> deviceKeepAliveTimers = new ConcurrentHashMap<>();
public MqttBrokerMonitor(GatewayService gateway, MqttBrokerConfiguration configuration) {
this.gateway = gateway;
this.configuration = configuration;
this.devices = new HashSet<>();
}
public void connect() {
try {
client = new MqttAsyncClient((configuration.isSsl() ? "ssl" : "tcp") + "://" + configuration.getHost() + ":" + configuration.getPort(),
getClientId(), new MemoryPersistence());
client.setCallback(this);
clientOptions = new MqttConnectOptions();
clientOptions.setCleanSession(true);
if (configuration.isSsl() && !StringUtils.isEmpty(configuration.getTruststore())) {
Properties sslProperties = new Properties();
sslProperties.put(SSLSocketFactoryFactory.TRUSTSTORE, configuration.getTruststore());
sslProperties.put(SSLSocketFactoryFactory.TRUSTSTOREPWD, configuration.getTruststorePassword());
sslProperties.put(SSLSocketFactoryFactory.TRUSTSTORETYPE, "JKS");
sslProperties.put(SSLSocketFactoryFactory.CLIENTAUTH, false);
clientOptions.setSSLProperties(sslProperties);
}
configuration.getCredentials().configure(clientOptions);
checkConnection();
if (configuration.getAttributeUpdates() != null) {
configuration.getAttributeUpdates().forEach(mapping ->
gateway.subscribe(new AttributesUpdateSubscription(mapping.getDeviceNameFilter(), this))
);
}
if (configuration.getServerSideRpc() != null) {
configuration.getServerSideRpc().forEach(mapping ->
gateway.subscribe(new RpcCommandSubscription(mapping.getDeviceNameFilter(), this))
);
}
} catch (MqttException e) {
log.error("[{}:{}] MQTT broker connection failed!", configuration.getHost(), configuration.getPort(), e);
throw new RuntimeException("MQTT broker connection failed!", e);
}
}
private String getClientId() {
return StringUtils.isEmpty(configuration.getClientId()) ? clientId.toString() : configuration.getClientId();
}
public void disconnect() {
devices.forEach(gateway::onDeviceDisconnect);
scheduler.shutdownNow();
}
private void checkConnection() {
if (!client.isConnected()) {
synchronized (connectLock) {
while (!client.isConnected()) {
log.debug("[{}:{}] MQTT broker connection attempt!", configuration.getHost(), configuration.getPort());
try {
client.connect(clientOptions, null, new IMqttActionListener() {
@Override
public void onSuccess(IMqttToken iMqttToken) {
log.info("[{}:{}] MQTT broker connection established!", configuration.getHost(), configuration.getPort());
}
@Override
public void onFailure(IMqttToken iMqttToken, Throwable e) {
}
}).waitForCompletion();
subscribeToTopics();
} catch (MqttException e) {
log.warn("[{}:{}] MQTT broker connection failed!", configuration.getHost(), configuration.getPort(), e);
if (!client.isConnected()) {
try {
Thread.sleep(configuration.getRetryInterval());
} catch (InterruptedException e1) {
log.trace("Failed to wait for retry interval!", e);
}
}
}
}
}
}
}
private void subscribeToTopics() throws MqttException {
List<IMqttToken> tokens = new ArrayList<>();
for (MqttTopicMapping mapping : configuration.getMapping()) {
tokens.add(client.subscribe(mapping.getTopicFilter(), 1, new MqttTelemetryMessageListener(this::onDeviceData, mapping.getConverter())));
}
if (configuration.getConnectRequests() != null) {
for (DeviceStateChangeMapping mapping : configuration.getConnectRequests()) {
tokens.add(client.subscribe(mapping.getTopicFilter(), 1, new MqttDeviceStateChangeMessageListener(mapping, this::onDeviceConnect)));
}
}
if (configuration.getDisconnectRequests() != null) {
for (DeviceStateChangeMapping mapping : configuration.getDisconnectRequests()) {
tokens.add(client.subscribe(mapping.getTopicFilter(), 1, new MqttDeviceStateChangeMessageListener(mapping, this::onDeviceDisconnect)));
}
}
if (configuration.getAttributeRequests() != null) {
for (AttributeRequestsMapping mapping : configuration.getAttributeRequests()) {
tokens.add(client.subscribe(mapping.getTopicFilter(), 1, new MqttAttributeRequestsMessageListener(this::onAttributeRequest, mapping)));
}
}
for (IMqttToken token : tokens) {
token.waitForCompletion();
}
}
private void onDeviceConnect(String deviceName) {
log.info("[{}] Device connected!", deviceName);
gateway.onDeviceConnect(deviceName);
}
private void onDeviceDisconnect(String deviceName) {
log.info("[{}] Device disconnected!", deviceName);
gateway.onDeviceDisconnect(deviceName);
log.debug("[{}] Will Topic Msg Received. Disconnecting device...", deviceName);
cleanUpKeepAliveTimes(deviceName);
}
private void onDeviceData(List<DeviceData> data) {
for (DeviceData dd : data) {
if (devices.add(dd.getName())) {
gateway.onDeviceConnect(dd.getName());
}
if (!dd.getAttributes().isEmpty()) {
gateway.onDeviceAttributesUpdate(dd.getName(), dd.getAttributes());
}
if (!dd.getTelemetry().isEmpty()) {
gateway.onDeviceTelemetry(dd.getName(), dd.getTelemetry());
}
if (dd.getTimeout() != 0) {
ScheduledFuture<?> future = deviceKeepAliveTimers.get(dd.getName());
if (future != null) {
log.debug("Re-scheduling keep alive timer for device {} with timeout = {}", dd.getName(), dd.getTimeout());
future.cancel(true);
deviceKeepAliveTimers.remove(dd.getName());
scheduleDeviceKeepAliveTimer(dd);
} else {
log.debug("Scheduling keep alive timer for device {} with timeout = {}", dd.getName(), dd.getTimeout());
scheduleDeviceKeepAliveTimer(dd);
}
}
}
}
private void onAttributeRequest(AttributeRequest attributeRequest) {
gateway.onDeviceAttributeRequest(attributeRequest, this::onAttributeResponse);
}
private void onAttributeResponse(AttributeResponse response) {
if (response.getData().isPresent()) {
KvEntry attribute = response.getData().get();
String topic = replace(response.getTopicExpression(), Integer.toString(response.getRequestId()), response.getDeviceName(), attribute);
String body = replace(response.getValueExpression(), Integer.toString(response.getRequestId()), response.getDeviceName(), attribute);
publish(response.getDeviceName(), topic, new MqttMessage(body.getBytes(StandardCharsets.UTF_8)));
} else {
log.warn("[{}] {} attribute [{}] not found", response.getDeviceName(), response.isClientScope() ? "Client" : "Shared", response.getKey());
}
}
private void cleanUpKeepAliveTimes(String deviceName) {
ScheduledFuture<?> future = deviceKeepAliveTimers.get(deviceName);
if (future != null) {
future.cancel(true);
deviceKeepAliveTimers.remove(deviceName);
}
}
private void scheduleDeviceKeepAliveTimer(DeviceData dd) {
ScheduledFuture<?> f = scheduler.schedule(
() -> {
log.warn("[{}] Device is going to be disconnected because of timeout! timeout = {} milliseconds", dd.getName(), dd.getTimeout());
deviceKeepAliveTimers.remove(dd.getName());
gateway.onDeviceDisconnect(dd.getName());
},
dd.getTimeout(),
TimeUnit.MILLISECONDS
);
deviceKeepAliveTimers.put(dd.getName(), f);
}
@Override
public void onAttributesUpdated(String deviceName, List<KvEntry> attributes) {
List<AttributeUpdatesMapping> mappings = configuration.getAttributeUpdates().stream()
.filter(mapping -> deviceName.matches(mapping.getDeviceNameFilter())).collect(Collectors.toList());
for (AttributeUpdatesMapping mapping : mappings) {
List<KvEntry> affected = attributes.stream().filter(attribute -> attribute.getKey()
.matches(mapping.getAttributeFilter())).collect(Collectors.toList());
for (KvEntry attribute : affected) {
String topic = replace(mapping.getTopicExpression(), deviceName, attribute);
String body = replace(mapping.getValueExpression(), deviceName, attribute);
MqttMessage msg = new MqttMessage(body.getBytes(StandardCharsets.UTF_8));
publish(deviceName, topic, msg);
}
}
}
@Override
public void onRpcCommand(String deviceName, RpcCommandData command) {
int requestId = command.getRequestId();
List<ServerSideRpcMapping> mappings = configuration.getServerSideRpc().stream()
.filter(mapping -> deviceName.matches(mapping.getDeviceNameFilter()))
.filter(mapping -> command.getMethod().matches(mapping.getMethodFilter())).collect(Collectors.toList());
mappings.forEach(mapping -> {
String requestTopic = replace(mapping.getRequestTopicExpression(), deviceName, command);
String body = replace(mapping.getValueExpression(), deviceName, command);
boolean oneway = StringUtils.isEmpty(mapping.getResponseTopicExpression());
if (oneway) {
publish(deviceName, requestTopic, new MqttMessage(body.getBytes(StandardCharsets.UTF_8)));
} else {
String responseTopic = replace(mapping.getResponseTopicExpression(), deviceName, command);
try {
log.info("[{}] Temporary subscribe to RPC response topic [{}]", deviceName, responseTopic);
client.subscribe(responseTopic, 1,
new MqttRpcResponseMessageListener(requestId, deviceName, this::onRpcCommandResponse)
).waitForCompletion();
scheduler.schedule(() -> {
unsubscribe(deviceName, requestId, responseTopic);
}, mapping.getResponseTimeout(), TimeUnit.MILLISECONDS);
publish(deviceName, requestTopic, new MqttMessage(body.getBytes(StandardCharsets.UTF_8)));
} catch (MqttException e) {
log.warn("[{}] Failed to subscribe to response topic and push RPC command [{}]", deviceName, requestId, e);
}
}
});
}
private void onRpcCommandResponse(String topic, RpcCommandResponse rpcResponse) {
log.info("[{}] Un-subscribe from RPC response topic [{}]", rpcResponse.getDeviceName(), topic);
gateway.onDeviceRpcResponse(rpcResponse);
unsubscribe(rpcResponse.getDeviceName(), rpcResponse.getRequestId(), topic);
}
private void unsubscribe(String deviceName, int requestId, String topic) {
try {
client.unsubscribe(topic);
} catch (MqttException e) {
log.warn("[{}][{}] Failed to unsubscribe from RPC reply topic [{}]", deviceName, requestId, topic, e);
}
}
private void publish(final String deviceName, String topic, MqttMessage msg) {
try {
client.publish(topic, msg, null, new IMqttActionListener() {
@Override
public void onSuccess(IMqttToken iMqttToken) {
log.info("[{}] Successfully published to topic [{}]", deviceName, topic);
}
@Override
public void onFailure(IMqttToken iMqttToken, Throwable e) {
log.warn("[{}] Failed to publish to topic [{}]", deviceName, topic, e);
}
});
} catch (MqttException e) {
log.warn("[{}] Failed to publish to topic [{}] ", deviceName, topic, e);
}
}
private static String replace(String expression, String deviceName, KvEntry attribute) {
return replace(expression, "", deviceName, attribute);
}
private static String replace(String expression, String deviceName, RpcCommandData command) {
return expression.replace("${deviceName}", deviceName)
.replace("${methodName}", command.getMethod())
.replace("${requestId}", Integer.toString(command.getRequestId()))
.replace("${params}", command.getParams());
}
private static String replace(String expression, String requestId, String deviceName, KvEntry attribute) {
return expression.replace("${deviceName}", deviceName)
.replace("${requestId}", requestId)
.replace("${attributeKey}", attribute.getKey())
.replace("${attributeValue}", attribute.getValueAsString());
}
@Override
public void connectionLost(Throwable cause) {
log.warn("[{}:{}] MQTT broker connection lost!", configuration.getHost(), configuration.getPort());
devices.forEach(gateway::onDeviceDisconnect);
checkConnection();
}
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
}
}