/** * 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.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.Function; import lombok.extern.slf4j.Slf4j; import org.eclipse.paho.client.mqttv3.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.gateway.service.data.*; import org.thingsboard.gateway.service.conf.TbConnectionConfiguration; import org.thingsboard.gateway.service.conf.TbPersistenceConfiguration; import org.thingsboard.gateway.service.conf.TbReportingConfiguration; import org.thingsboard.gateway.util.JsonTools; import org.thingsboard.server.common.data.kv.*; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.stream.Collectors; import static org.thingsboard.gateway.util.JsonTools.*; /** * Created by ashvayka on 16.01.17. */ @Service @Slf4j public class MqttGatewayService implements GatewayService, MqttCallback, IMqttMessageListener { private static final long CLIENT_RECONNECT_CHECK_INTERVAL = 1; public static final String DEVICE_TELEMETRY_TOPIC = "v1/devices/me/telemetry"; public static final String GATEWAY_RPC_TOPIC = "v1/gateway/rpc"; public static final String GATEWAY_ATTRIBUTES_TOPIC = "v1/gateway/attributes"; public static final String GATEWAY_TELEMETRY_TOPIC = "v1/gateway/telemetry"; public static final String GATEWAY_REQUESTS_ATTRIBUTES_TOPIC = "v1/gateway/attributes/request"; public static final String GATEWAY_RESPONSES_ATTRIBUTES_TOPIC = "v1/gateway/attributes/response"; public static final String GATEWAY_CONNECT_TOPIC = "v1/gateway/connect"; public static final String GATEWAY_DISCONNECT_TOPIC = "v1/gateway/disconnect"; private final ConcurrentMap<String, DeviceInfo> devices = new ConcurrentHashMap<>(); private final AtomicLong attributesCount = new AtomicLong(); private final AtomicLong telemetryCount = new AtomicLong(); private final AtomicInteger msgIdSeq = new AtomicInteger(); private final Set<AttributesUpdateSubscription> attributeUpdateSubs = ConcurrentHashMap.newKeySet(); private final Set<RpcCommandSubscription> rpcCommandSubs = ConcurrentHashMap.newKeySet(); private volatile ObjectNode error; @Autowired private TbConnectionConfiguration connection; @Autowired private TbReportingConfiguration reporting; @Autowired private TbPersistenceConfiguration persistence; private MqttAsyncClient tbClient; private MqttConnectOptions tbClientOptions; private Object connectLock = new Object(); private ScheduledExecutorService scheduler; private ExecutorService callbackExecutor = Executors.newCachedThreadPool(); private Map<AttributeRequestKey, AttributeRequestListener> pendingAttrRequestsMap = new ConcurrentHashMap<>(); @PostConstruct public void init() throws Exception { scheduler = Executors.newSingleThreadScheduledExecutor(); tbClientOptions = new MqttConnectOptions(); tbClientOptions.setCleanSession(false); tbClientOptions.setMaxInflight(connection.getMaxInFlight()); tbClientOptions.setAutomaticReconnect(true); MqttGatewaySecurityConfiguration security = connection.getSecurity(); security.setupSecurityOptions(tbClientOptions); tbClient = new MqttAsyncClient((security.isSsl() ? "ssl" : "tcp") + "://" + connection.getHost() + ":" + connection.getPort(), security.getClientId(), persistence.getPersistence()); tbClient.setCallback(this); if (persistence.getBufferSize() > 0) { DisconnectedBufferOptions options = new DisconnectedBufferOptions(); options.setBufferSize(persistence.getBufferSize()); options.setBufferEnabled(true); options.setPersistBuffer(true); tbClient.setBufferOpts(options); } connect(); scheduler.scheduleAtFixedRate(this::reportStats, 0, reporting.getInterval(), TimeUnit.MILLISECONDS); } @PreDestroy public void preDestroy() throws Exception { scheduler.shutdownNow(); callbackExecutor.shutdownNow(); tbClient.disconnect(); } @Override public MqttDeliveryFuture onDeviceConnect(final String deviceName) { final int msgId = msgIdSeq.incrementAndGet(); byte[] msgData = toBytes(newNode().put("device", deviceName)); MqttMessage msg = new MqttMessage(msgData); msg.setId(msgId); log.info("[{}] Device Connected!", deviceName); devices.putIfAbsent(deviceName, new DeviceInfo(deviceName)); return publishAsync(GATEWAY_CONNECT_TOPIC, msg, token -> { log.info("[{}][{}] Device connect event is reported to Thingsboard!", deviceName, msgId); }, error -> log.warn("[{}][{}] Failed to report device connection!", deviceName, msgId, error)); } @Override public Optional<MqttDeliveryFuture> onDeviceDisconnect(String deviceName) { if (devices.remove(deviceName) != null) { final int msgId = msgIdSeq.incrementAndGet(); byte[] msgData = toBytes(newNode().put("device", deviceName)); MqttMessage msg = new MqttMessage(msgData); msg.setId(msgId); log.info("[{}][{}] Device Disconnected!", deviceName, msgId); MqttDeliveryFuture future = publishAsync(GATEWAY_DISCONNECT_TOPIC, msg, token -> { log.info("[{}][{}] Device disconnect event is delivered!", deviceName, msgId); }, error -> log.warn("[{}][{}] Failed to report device disconnect!", deviceName, msgId, error)); return Optional.of(future); } else { log.debug("[{}] Device was disconnected before. Nothing is going to happened.", deviceName); return Optional.empty(); } } @Override public MqttDeliveryFuture onDeviceAttributesUpdate(String deviceName, List<KvEntry> attributes) { final int msgId = msgIdSeq.incrementAndGet(); log.trace("[{}][{}] Updating device attributes: {}", deviceName, msgId, attributes); checkDeviceConnected(deviceName); ObjectNode node = newNode(); ObjectNode deviceNode = node.putObject(deviceName); attributes.forEach(kv -> putToNode(deviceNode, kv)); final int packSize = attributes.size(); MqttMessage msg = new MqttMessage(toBytes(node)); msg.setId(msgId); return publishAsync(GATEWAY_ATTRIBUTES_TOPIC, msg, token -> { log.debug("[{}][{}] Device attributes were delivered!", deviceName, msgId); attributesCount.addAndGet(packSize); }, error -> log.warn("[{}][{}] Failed to report device attributes!", deviceName, msgId, error)); } @Override public MqttDeliveryFuture onDeviceTelemetry(String deviceName, List<TsKvEntry> telemetry) { final int msgId = msgIdSeq.incrementAndGet(); log.trace("[{}][{}] Updating device telemetry: {}", deviceName, msgId, telemetry); checkDeviceConnected(deviceName); ObjectNode node = newNode(); Map<Long, List<TsKvEntry>> tsMap = telemetry.stream().collect(Collectors.groupingBy(v -> v.getTs())); ArrayNode deviceNode = node.putArray(deviceName); tsMap.entrySet().forEach(kv -> { Long ts = kv.getKey(); ObjectNode tsNode = deviceNode.addObject(); tsNode.put("ts", ts); ObjectNode valuesNode = tsNode.putObject("values"); kv.getValue().forEach(v -> putToNode(valuesNode, v)); }); final int packSize = telemetry.size(); MqttMessage msg = new MqttMessage(toBytes(node)); msg.setId(msgId); return publishAsync(GATEWAY_TELEMETRY_TOPIC, msg, token -> { log.debug("[{}][{}] Device telemetry published to Thingsboard!", msgId, deviceName); telemetryCount.addAndGet(packSize); }, error -> log.warn("[{}][{}] Failed to publish device telemetry!", deviceName, msgId, error)); } @Override public void onDeviceAttributeRequest(AttributeRequest request, Consumer<AttributeResponse> listener) { final int msgId = msgIdSeq.incrementAndGet(); String deviceName = request.getDeviceName(); AttributeRequestKey requestKey = new AttributeRequestKey(request.getRequestId(), request.getDeviceName()); log.trace("[{}][{}] Requesting {} attribute: {}", deviceName, msgId, request.isClientScope() ? "client" : "shared", request.getAttributeKey()); checkDeviceConnected(deviceName); ObjectNode node = newNode(); node.put("id", request.getRequestId()); node.put("client", request.isClientScope()); node.put("device", request.getDeviceName()); node.put("key", request.getAttributeKey()); MqttMessage msg = new MqttMessage(toBytes(node)); msg.setId(msgId); pendingAttrRequestsMap.put(requestKey, new AttributeRequestListener(request, listener)); publishAsync(GATEWAY_REQUESTS_ATTRIBUTES_TOPIC, msg, token -> { log.debug("[{}][{}] Device attributes request was delivered!", deviceName, msgId); }, error -> { log.warn("[{}][{}] Failed to report device attributes!", deviceName, msgId, error); pendingAttrRequestsMap.remove(requestKey); }); } @Override public void onDeviceRpcResponse(RpcCommandResponse response) { final int msgId = msgIdSeq.incrementAndGet(); int requestId = response.getRequestId(); String deviceName = response.getDeviceName(); String data = response.getData(); checkDeviceConnected(deviceName); ObjectNode node = newNode(); node.put("id", requestId); node.put("device", deviceName); node.put("data", data); MqttMessage msg = new MqttMessage(toBytes(node)); msg.setId(msgId); publishAsync(GATEWAY_RPC_TOPIC, msg, token -> { log.debug("[{}][{}] RPC response from device was delivered!", deviceName, requestId); }, error -> { log.warn("[{}][{}] Failed to report RPC response from device!", deviceName, requestId, error); }); } @Override public boolean subscribe(AttributesUpdateSubscription subscription) { return subscribe(attributeUpdateSubs::add, subscription); } @Override public boolean subscribe(RpcCommandSubscription subscription) { return subscribe(rpcCommandSubs::add, subscription); } @Override public boolean unsubscribe(AttributesUpdateSubscription subscription) { return unsubscribe(attributeUpdateSubs::remove, subscription); } @Override public boolean unsubscribe(RpcCommandSubscription subscription) { return unsubscribe(rpcCommandSubs::remove, subscription); } @Override public void onError(Exception e) { onError(null, e); } @Override public void onError(String deviceName, Exception e) { ObjectNode node = newNode(); node.put("ts", System.currentTimeMillis()); if (deviceName != null) { node.put("device", deviceName); } node.put("error", toString(e)); error = node; } private void connect() { if (!tbClient.isConnected()) { synchronized (connectLock) { while (!tbClient.isConnected()) { log.debug("Attempt to connect to Thingsboard!"); try { tbClient.connect(tbClientOptions, null, new IMqttActionListener() { @Override public void onSuccess(IMqttToken iMqttToken) { log.info("Connected to Thingsboard!"); } @Override public void onFailure(IMqttToken iMqttToken, Throwable e) { } }).waitForCompletion(); // tbClient.subscribe(GATEWAY_ATTRIBUTES_TOPIC, 1, (IMqttMessageListener) this); // tbClient.subscribe(GATEWAY_RPC_TOPIC, 1, (IMqttMessageListener) this); devices.forEach((k, v) -> onDeviceConnect(k)); } catch (MqttException e) { log.warn("Failed to connect to Thingsboard!", e); if (!tbClient.isConnected()) { try { Thread.sleep(connection.getRetryInterval()); } catch (InterruptedException e1) { log.trace("Failed to wait for retry interval!", e); } } } } } } } private void checkDeviceConnected(String deviceName) { if (!devices.containsKey(deviceName)) { onDeviceConnect(deviceName); } } private MqttDeliveryFuture publishAsync(final String topic, MqttMessage msg, Consumer<IMqttToken> onSuccess, Consumer<Throwable> onFailure) { try { IMqttDeliveryToken token = tbClient.publish(topic, msg, null, new IMqttActionListener() { @Override public void onSuccess(IMqttToken asyncActionToken) { onSuccess.accept(asyncActionToken); } @Override public void onFailure(IMqttToken asyncActionToken, Throwable e) { onFailure.accept(e); } }); return new MqttDeliveryFuture(token); } catch (MqttException e) { onFailure.accept(e); return new MqttDeliveryFuture(e); } } private void reportStats() { if (tbClient == null) { log.info("Can't report stats because client was not initialized yet!"); return; } ObjectNode node = newNode(); node.put("ts", System.currentTimeMillis()); ObjectNode valuesNode = node.putObject("values"); valuesNode.put("devicesOnline", devices.size()); valuesNode.put("attributesUploaded", attributesCount.getAndSet(0)); valuesNode.put("telemetryUploaded", telemetryCount.getAndSet(0)); if (error != null) { valuesNode.put("latestError", JsonTools.toString(error)); error = null; } MqttMessage msg = new MqttMessage(toBytes(node)); msg.setId(msgIdSeq.incrementAndGet()); publishAsync(DEVICE_TELEMETRY_TOPIC, msg, token -> log.info("Gateway statistics {} reported!", node), error -> log.warn("Failed to report gateway statistics!", error)); } @Override public void connectionLost(Throwable cause) { //TODO: reply with error pendingAttrRequestsMap.clear(); scheduler.schedule(this::checkClientReconnected, CLIENT_RECONNECT_CHECK_INTERVAL, TimeUnit.SECONDS); } @Override public void messageArrived(String topic, MqttMessage message) throws Exception { log.trace("Message arrived [{}] {}", topic, message.getId()); if (topic.equals(GATEWAY_ATTRIBUTES_TOPIC)) { onAttributesUpdate(message); } else if (topic.equals(GATEWAY_RESPONSES_ATTRIBUTES_TOPIC)) { onDeviceAttributesResponse(message); } else if (topic.equals(GATEWAY_RPC_TOPIC)) { onRpcCommand(message); } } @Override public void deliveryComplete(IMqttDeliveryToken token) { log.trace("Delivery complete [{}]", token); } private void onAttributesUpdate(MqttMessage message) { JsonNode payload = fromString(new String(message.getPayload(), StandardCharsets.UTF_8)); String deviceName = payload.get("device").asText(); Set<AttributesUpdateListener> listeners = attributeUpdateSubs.stream() .filter(sub -> sub.matches(deviceName)).map(sub -> sub.getListener()) .collect(Collectors.toSet()); if (!listeners.isEmpty()) { JsonNode data = payload.get("data"); List<KvEntry> attributes = getKvEntries(data); listeners.forEach(listener -> callbackExecutor.submit(() -> { try { listener.onAttributesUpdated(deviceName, attributes); } catch (Exception e) { log.error("[{}] Failed to process attributes update", deviceName, e); } })); } } private void onRpcCommand(MqttMessage message) { JsonNode payload = fromString(new String(message.getPayload(), StandardCharsets.UTF_8)); String deviceName = payload.get("device").asText(); Set<RpcCommandListener> listeners = rpcCommandSubs.stream() .filter(sub -> sub.matches(deviceName)).map(sub -> sub.getListener()) .collect(Collectors.toSet()); if (!listeners.isEmpty()) { JsonNode data = payload.get("data"); RpcCommandData rpcCommand = new RpcCommandData(); rpcCommand.setRequestId(data.get("id").asInt()); rpcCommand.setMethod(data.get("method").asText()); rpcCommand.setParams(JsonTools.toString(data.get("params"))); listeners.forEach(listener -> callbackExecutor.submit(() -> { try { listener.onRpcCommand(deviceName, rpcCommand); } catch (Exception e) { log.error("[{}][{}] Failed to process rpc command", deviceName, rpcCommand.getRequestId(), e); } })); } else { log.warn("No listener registered for RPC command to device {}!", deviceName); } } private void onDeviceAttributesResponse(MqttMessage message) { JsonNode payload = fromString(new String(message.getPayload(), StandardCharsets.UTF_8)); AttributeRequestKey requestKey = new AttributeRequestKey(payload.get("id").asInt(), payload.get("device").asText()); AttributeRequestListener listener = pendingAttrRequestsMap.get(requestKey); if (listener == null) { log.warn("[{}][{}] Can't find listener for request", requestKey.getDeviceName(), requestKey.getRequestId()); return; } AttributeRequest request = listener.getRequest(); AttributeResponse.AttributeResponseBuilder response = AttributeResponse.builder(); response.requestId(request.getRequestId()); response.deviceName(request.getDeviceName()); response.key(request.getAttributeKey()); response.clientScope(request.isClientScope()); response.topicExpression(request.getTopicExpression()); response.valueExpression(request.getValueExpression()); String key = listener.getRequest().getAttributeKey(); JsonNode value = payload.get("value"); if (value == null) { response.data(Optional.empty()); } else if (value.isBoolean()) { response.data(Optional.of(new BooleanDataEntry(key, value.asBoolean()))); } else if (value.isLong()) { response.data(Optional.of(new LongDataEntry(key, value.asLong()))); } else if (value.isDouble()) { response.data(Optional.of(new DoubleDataEntry(key, value.asDouble()))); } else { response.data(Optional.of(new StringDataEntry(key, value.asText()))); } callbackExecutor.submit(() -> { try { listener.getListener().accept(response.build()); } catch (Exception e) { log.error("[{}][{}] Failed to process attributes response", requestKey.getDeviceName(), requestKey.getRequestId(), e); } }); } private void checkClientReconnected() { if (tbClient.isConnected()) { devices.forEach((k, v) -> onDeviceConnect(k)); } else { scheduler.schedule(this::checkClientReconnected, CLIENT_RECONNECT_CHECK_INTERVAL, TimeUnit.SECONDS); } } private static String toString(Exception e) { StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); return sw.toString(); } private <T> boolean subscribe(Function<T, Boolean> f, T sub) { if (f.apply(sub)) { log.info("Subscription added: {}", sub); return true; } else { log.warn("Subscription was already added: {}", sub); return false; } } private <T> boolean unsubscribe(Function<T, Boolean> f, T sub) { if (f.apply(sub)) { log.info("Subscription removed: {}", sub); return true; } else { log.warn("Subscription was already removed: {}", sub); return false; } } }