committed by
GitHub
50 changed files with 4734 additions and 852 deletions
@ -0,0 +1,462 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.sparkplug; |
|||
|
|||
import com.fasterxml.jackson.databind.node.ArrayNode; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.eclipse.paho.mqttv5.client.IMqttToken; |
|||
import org.eclipse.paho.mqttv5.client.MqttCallback; |
|||
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions; |
|||
import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse; |
|||
import org.eclipse.paho.mqttv5.common.MqttException; |
|||
import org.eclipse.paho.mqttv5.common.MqttMessage; |
|||
import org.eclipse.paho.mqttv5.common.packet.MqttConnAck; |
|||
import org.eclipse.paho.mqttv5.common.packet.MqttProperties; |
|||
import org.eclipse.paho.mqttv5.common.packet.MqttReturnCode; |
|||
import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; |
|||
import org.junit.Assert; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.TransportPayloadType; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
|||
import org.thingsboard.server.common.data.kv.BooleanDataEntry; |
|||
import org.thingsboard.server.common.data.kv.DoubleDataEntry; |
|||
import org.thingsboard.server.common.data.kv.JsonDataEntry; |
|||
import org.thingsboard.server.common.data.kv.LongDataEntry; |
|||
import org.thingsboard.server.common.data.kv.StringDataEntry; |
|||
import org.thingsboard.server.common.data.kv.TsKvEntry; |
|||
import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; |
|||
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; |
|||
import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; |
|||
import org.thingsboard.server.transport.mqtt.mqttv5.MqttV5TestClient; |
|||
import org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType; |
|||
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.Calendar; |
|||
import java.util.List; |
|||
import java.util.Optional; |
|||
import java.util.Set; |
|||
import java.util.concurrent.ThreadLocalRandom; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.concurrent.atomic.AtomicReference; |
|||
|
|||
import static org.awaitility.Awaitility.await; |
|||
import static org.eclipse.paho.mqttv5.common.packet.MqttWireMessage.MESSAGE_TYPE_CONNACK; |
|||
import static org.thingsboard.common.util.JacksonUtil.newArrayNode; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Bytes; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int16; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int32; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int64; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int8; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt16; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt32; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt64; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt8; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.createMetric; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; |
|||
|
|||
/** |
|||
* Created by nickAS21 on 12.01.23 |
|||
*/ |
|||
@Slf4j |
|||
public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttIntegrationTest { |
|||
|
|||
protected MqttV5TestClient client; |
|||
protected SparkplugMqttCallback mqttCallback; |
|||
protected Calendar calendar = Calendar.getInstance(); |
|||
protected ThreadLocalRandom random = ThreadLocalRandom.current(); |
|||
|
|||
protected static final String groupId = "SparkplugBGroupId"; |
|||
protected static final String edgeNode = "SparkpluBNode"; |
|||
protected static final String keysBdSeq = "bdSeq"; |
|||
protected static final String alias = "Failed Telemetry/Attribute proto sparkplug payload. SparkplugMessageType "; |
|||
protected String deviceId = "Test Sparkplug B Device"; |
|||
protected int bdSeq = 0; |
|||
protected int seq = 0; |
|||
protected static final long PUBLISH_TS_DELTA_MS = 86400000;// Publish start TS <-> 24h
|
|||
|
|||
// NBIRTH
|
|||
protected static final String keyNodeRebirth = "Node Control/Rebirth"; |
|||
|
|||
//*BIRTH
|
|||
protected static final MetricDataType metricBirthDataType_Int32 = Int32; |
|||
protected static final String metricBirthName_Int32 = "Device Metric int32"; |
|||
protected Set<String> sparkplugAttributesMetricNames; |
|||
|
|||
public void beforeSparkplugTest() throws Exception { |
|||
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() |
|||
.gatewayName("Test Connect Sparkplug client node") |
|||
.isSparkplug(true) |
|||
.sparkplugAttributesMetricNames(sparkplugAttributesMetricNames) |
|||
.transportPayloadType(TransportPayloadType.PROTOBUF) |
|||
.build(); |
|||
processBeforeTest(configProperties); |
|||
} |
|||
|
|||
public void clientWithCorrectNodeAccessTokenWithNDEATH() throws Exception { |
|||
long ts = calendar.getTimeInMillis(); |
|||
long value = bdSeq = 0; |
|||
clientWithCorrectNodeAccessTokenWithNDEATH(ts, value); |
|||
} |
|||
|
|||
public void clientWithCorrectNodeAccessTokenWithNDEATH(long ts, long value) throws Exception { |
|||
IMqttToken connectionResult = clientConnectWithNDEATH(ts, value); |
|||
MqttWireMessage response = connectionResult.getResponse(); |
|||
Assert.assertEquals(MESSAGE_TYPE_CONNACK, response.getType()); |
|||
MqttConnAck connAckMsg = (MqttConnAck) response; |
|||
Assert.assertEquals(MqttReturnCode.RETURN_CODE_SUCCESS, connAckMsg.getReturnCode()); |
|||
} |
|||
|
|||
public IMqttToken clientConnectWithNDEATH(long ts, long value, String... nameSpaceBad) throws Exception { |
|||
String key = keysBdSeq; |
|||
MetricDataType metricDataType = Int64; |
|||
SparkplugBProto.Payload.Builder deathPayload = SparkplugBProto.Payload.newBuilder() |
|||
.setTimestamp(calendar.getTimeInMillis()); |
|||
deathPayload.addMetrics(createMetric(value, ts, key, metricDataType)); |
|||
byte[] deathBytes = deathPayload.build().toByteArray(); |
|||
this.client = new MqttV5TestClient(); |
|||
this.mqttCallback = new SparkplugMqttCallback(); |
|||
this.client.setCallback(this.mqttCallback); |
|||
MqttConnectionOptions options = new MqttConnectionOptions(); |
|||
options.setUserName(gatewayAccessToken); |
|||
String nameSpace = nameSpaceBad.length == 0 ? NAMESPACE : nameSpaceBad[0]; |
|||
String topic = nameSpace + "/" + groupId + "/" + SparkplugMessageType.NDEATH.name() + "/" + edgeNode; |
|||
MqttMessage msg = new MqttMessage(); |
|||
msg.setId(0); |
|||
msg.setPayload(deathBytes); |
|||
options.setWill(topic, msg); |
|||
return client.connect(options); |
|||
} |
|||
|
|||
protected List<Device> connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(int cntDevices, long ts) throws Exception { |
|||
List<Device> devices = new ArrayList<>(); |
|||
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|||
MetricDataType metricDataType = Int32; |
|||
String key = "Node Metric int32"; |
|||
int valueDeviceInt32 = 1024; |
|||
SparkplugBProto.Payload.Metric metric = createMetric(valueDeviceInt32, ts, key, metricDataType); |
|||
SparkplugBProto.Payload.Builder payloadBirthNode = SparkplugBProto.Payload.newBuilder() |
|||
.setTimestamp(ts) |
|||
.setSeq(getBdSeqNum()); |
|||
payloadBirthNode.addMetrics(metric); |
|||
payloadBirthNode.setTimestamp(ts); |
|||
if (client.isConnected()) { |
|||
client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode, |
|||
payloadBirthNode.build().toByteArray(), 0, false); |
|||
} |
|||
|
|||
valueDeviceInt32 = 4024; |
|||
metric = createMetric(valueDeviceInt32, ts, metricBirthName_Int32, metricBirthDataType_Int32); |
|||
for (int i = 0; i < cntDevices; i++) { |
|||
SparkplugBProto.Payload.Builder payloadBirthDevice = SparkplugBProto.Payload.newBuilder() |
|||
.setTimestamp(ts) |
|||
.setSeq(getSeqNum()); |
|||
String deviceName = deviceId + "_" + i; |
|||
|
|||
payloadBirthDevice.addMetrics(metric); |
|||
if (client.isConnected()) { |
|||
client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.DBIRTH.name() + "/" + edgeNode + "/" + deviceName, |
|||
payloadBirthDevice.build().toByteArray(), 0, false); |
|||
AtomicReference<Device> device = new AtomicReference<>(); |
|||
await(alias + "find device [" + deviceName + "] after created") |
|||
.atMost(200, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class)); |
|||
return device.get() != null; |
|||
}); |
|||
devices.add(device.get()); |
|||
} |
|||
|
|||
} |
|||
|
|||
Assert.assertEquals(cntDevices, devices.size()); |
|||
return devices; |
|||
} |
|||
|
|||
protected long getBdSeqNum() throws Exception { |
|||
if (bdSeq == 256) { |
|||
bdSeq = 0; |
|||
} |
|||
return bdSeq++; |
|||
} |
|||
|
|||
protected long getSeqNum() throws Exception { |
|||
if (seq == 256) { |
|||
seq = 0; |
|||
} |
|||
return seq++; |
|||
} |
|||
|
|||
protected List<String> connectionWithNBirth(MetricDataType metricDataType, String metricKey, Object metricValue) throws Exception { |
|||
List<String> listKeys = new ArrayList<>(); |
|||
SparkplugBProto.Payload.Builder payloadBirthNode = SparkplugBProto.Payload.newBuilder() |
|||
.setTimestamp(calendar.getTimeInMillis()); |
|||
long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; |
|||
long valueBdSec = getBdSeqNum(); |
|||
payloadBirthNode.addMetrics(createMetric(valueBdSec, ts, keysBdSeq, Int64)); |
|||
listKeys.add(SparkplugMessageType.NBIRTH.name() + " " + keysBdSeq); |
|||
payloadBirthNode.addMetrics(createMetric(false, ts, keyNodeRebirth, MetricDataType.Boolean)); |
|||
listKeys.add(keyNodeRebirth); |
|||
|
|||
payloadBirthNode.addMetrics(createMetric(metricValue, ts, metricKey, metricDataType)); |
|||
listKeys.add(metricKey); |
|||
|
|||
if (client.isConnected()) { |
|||
client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode, |
|||
payloadBirthNode.build().toByteArray(), 0, false); |
|||
} |
|||
return listKeys; |
|||
} |
|||
|
|||
protected void createdAddMetricValuePrimitiveTsKv(List<TsKvEntry> listTsKvEntry, List<String> listKeys, |
|||
SparkplugBProto.Payload.Builder dataPayload, long ts) throws ThingsboardException { |
|||
|
|||
String keys = "MyInt8"; |
|||
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextInt8(), ts, Int8)); |
|||
listKeys.add(keys); |
|||
|
|||
keys = "MyInt16"; |
|||
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextInt16(), ts, Int16)); |
|||
listKeys.add(keys); |
|||
|
|||
keys = "MyInt32"; |
|||
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextInt32(), ts, Int32)); |
|||
listKeys.add(keys); |
|||
|
|||
keys = "MyInt64"; |
|||
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextInt64(), ts, Int64)); |
|||
listKeys.add(keys); |
|||
|
|||
keys = "MyUInt8"; |
|||
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextUInt8(), ts, UInt8)); |
|||
listKeys.add(keys); |
|||
|
|||
keys = "MyUInt16"; |
|||
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextUInt16(), ts, UInt16)); |
|||
listKeys.add(keys); |
|||
|
|||
keys = "MyUInt32"; |
|||
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextUInt32(), ts, UInt32)); |
|||
listKeys.add(keys); |
|||
|
|||
keys = "MyUInt64"; |
|||
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextUInt64(), ts, UInt64)); |
|||
listKeys.add(keys); |
|||
|
|||
keys = "MyFloat"; |
|||
listTsKvEntry.add(createdAddMetricTsKvFloat(dataPayload, keys, nextFloat(0, 100), ts, MetricDataType.Float)); |
|||
listKeys.add(keys); |
|||
|
|||
keys = "MyDateTime"; |
|||
listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextDateTime(), ts, MetricDataType.DateTime)); |
|||
listKeys.add(keys); |
|||
|
|||
keys = "MyDouble"; |
|||
listTsKvEntry.add(createdAddMetricTsKvDouble(dataPayload, keys, nextDouble(), ts, MetricDataType.Double)); |
|||
listKeys.add(keys); |
|||
|
|||
keys = "MyBoolean"; |
|||
listTsKvEntry.add(createdAddMetricTsKvBoolean(dataPayload, keys, nextBoolean(), ts, MetricDataType.Boolean)); |
|||
listKeys.add(keys); |
|||
|
|||
keys = "MyString"; |
|||
listTsKvEntry.add(createdAddMetricTsKvString(dataPayload, keys, nextString(), ts, MetricDataType.String)); |
|||
listKeys.add(keys); |
|||
|
|||
keys = "MyText"; |
|||
listTsKvEntry.add(createdAddMetricTsKvString(dataPayload, keys, nextString(), ts, MetricDataType.Text)); |
|||
listKeys.add(keys); |
|||
|
|||
keys = "MyUUID"; |
|||
listTsKvEntry.add(createdAddMetricTsKvString(dataPayload, keys, nextString(), ts, MetricDataType.UUID)); |
|||
listKeys.add(keys); |
|||
|
|||
} |
|||
|
|||
protected void createdAddMetricValueArraysPrimitiveTsKv(List<TsKvEntry> listTsKvEntry, List<String> listKeys, |
|||
SparkplugBProto.Payload.Builder dataPayload, long ts) throws ThingsboardException { |
|||
String keys = "MyBytesArray"; |
|||
byte[] bytes = {nextInt8(), nextInt8(), nextInt8()}; |
|||
createdAddMetricTsKvJson(dataPayload, keys, bytes, ts, Bytes, listTsKvEntry, listKeys); |
|||
} |
|||
|
|||
private TsKvEntry createdAddMetricTsKvLong(SparkplugBProto.Payload.Builder dataPayload, String key, Object value, |
|||
long ts, MetricDataType metricDataType) throws ThingsboardException { |
|||
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(key, Long.valueOf(String.valueOf(value)))); |
|||
dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); |
|||
return tsKvEntry; |
|||
} |
|||
|
|||
private TsKvEntry createdAddMetricTsKvFloat(SparkplugBProto.Payload.Builder dataPayload, String key, float value, |
|||
long ts, MetricDataType metricDataType) throws ThingsboardException { |
|||
Double dd = Double.parseDouble(Float.toString(value)); |
|||
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new DoubleDataEntry(key, dd)); |
|||
dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); |
|||
return tsKvEntry; |
|||
} |
|||
|
|||
private TsKvEntry createdAddMetricTsKvDouble(SparkplugBProto.Payload.Builder dataPayload, String key, double value, |
|||
long ts, MetricDataType metricDataType) throws ThingsboardException { |
|||
Long l = Double.valueOf(value).longValue(); |
|||
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(key, l)); |
|||
dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); |
|||
return tsKvEntry; |
|||
} |
|||
|
|||
private TsKvEntry createdAddMetricTsKvBoolean(SparkplugBProto.Payload.Builder dataPayload, String key, boolean value, |
|||
long ts, MetricDataType metricDataType) throws ThingsboardException { |
|||
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new BooleanDataEntry(key, value)); |
|||
dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); |
|||
return tsKvEntry; |
|||
} |
|||
|
|||
private TsKvEntry createdAddMetricTsKvString(SparkplugBProto.Payload.Builder dataPayload, String key, String value, |
|||
long ts, MetricDataType metricDataType) throws ThingsboardException { |
|||
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(key, value)); |
|||
dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); |
|||
return tsKvEntry; |
|||
} |
|||
|
|||
private void createdAddMetricTsKvJson(SparkplugBProto.Payload.Builder dataPayload, String key, |
|||
Object values, long ts, MetricDataType metricDataType, |
|||
List<TsKvEntry> listTsKvEntry, |
|||
List<String> listKeys) throws ThingsboardException { |
|||
ArrayNode nodeArray = newArrayNode(); |
|||
switch (metricDataType) { |
|||
case Bytes: |
|||
for (byte b : (byte[]) values) { |
|||
nodeArray.add(b); |
|||
} |
|||
break; |
|||
default: |
|||
throw new IllegalStateException("Unexpected value: " + metricDataType); |
|||
} |
|||
if (nodeArray.size() > 0) { |
|||
Optional<TsKvEntry> tsKvEntryOptional = Optional.of(new BasicTsKvEntry(ts, new JsonDataEntry(key, nodeArray.toString()))); |
|||
if (tsKvEntryOptional.isPresent()) { |
|||
dataPayload.addMetrics(createMetric(values, ts, key, metricDataType)); |
|||
listTsKvEntry.add(tsKvEntryOptional.get()); |
|||
listKeys.add(key); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private byte nextInt8() { |
|||
return (byte) random.nextInt(Byte.MIN_VALUE, Byte.MAX_VALUE); |
|||
} |
|||
|
|||
private short nextUInt8() { |
|||
return (short) random.nextInt(0, Byte.MAX_VALUE * 2 + 1); |
|||
} |
|||
|
|||
private short nextInt16() { |
|||
return (short) random.nextInt(Short.MIN_VALUE, Short.MAX_VALUE); |
|||
} |
|||
|
|||
private int nextUInt16() { |
|||
return random.nextInt(0, Short.MAX_VALUE * 2 + 1); |
|||
} |
|||
|
|||
protected int nextInt32() { |
|||
return random.nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE); |
|||
} |
|||
|
|||
protected long nextUInt32() { |
|||
long l = Integer.MAX_VALUE; |
|||
return random.nextLong(0, l * 2 + 1); |
|||
} |
|||
|
|||
private long nextInt64() { |
|||
return random.nextLong(Long.MIN_VALUE, Long.MAX_VALUE); |
|||
} |
|||
|
|||
private long nextUInt64() { |
|||
double d = Long.MAX_VALUE; |
|||
return random.nextLong(0, (long) (d * 2 + 1)); |
|||
} |
|||
|
|||
protected double nextDouble() { |
|||
return random.nextDouble(Long.MIN_VALUE, Long.MAX_VALUE); |
|||
} |
|||
|
|||
private long nextDateTime() { |
|||
long min = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; |
|||
long max = calendar.getTimeInMillis(); |
|||
return random.nextLong(min, max); |
|||
} |
|||
|
|||
protected float nextFloat(float min, float max) { |
|||
if (min >= max) |
|||
throw new IllegalArgumentException("max must be greater than min"); |
|||
float result = ThreadLocalRandom.current().nextFloat() * (max - min) + min; |
|||
if (result >= max) // correct for rounding
|
|||
result = Float.intBitsToFloat(Float.floatToIntBits(max) - 1); |
|||
return result; |
|||
} |
|||
|
|||
protected boolean nextBoolean() { |
|||
return random.nextBoolean(); |
|||
} |
|||
|
|||
protected String nextString() { |
|||
return java.util.UUID.randomUUID().toString(); |
|||
} |
|||
|
|||
public class SparkplugMqttCallback implements MqttCallback { |
|||
private final List<SparkplugBProto.Payload.Metric> messageArrivedMetrics = new ArrayList<>(); |
|||
|
|||
@Override |
|||
public void disconnected(MqttDisconnectResponse mqttDisconnectResponse) { |
|||
|
|||
} |
|||
|
|||
@Override |
|||
public void mqttErrorOccurred(MqttException e) { |
|||
|
|||
} |
|||
|
|||
@Override |
|||
public void messageArrived(String topic, MqttMessage mqttMsg) throws Exception { |
|||
SparkplugBProto.Payload sparkplugBProtoNode = SparkplugBProto.Payload.parseFrom(mqttMsg.getPayload()); |
|||
messageArrivedMetrics.addAll(sparkplugBProtoNode.getMetricsList()); |
|||
} |
|||
|
|||
@Override |
|||
public void deliveryComplete(IMqttToken iMqttToken) { |
|||
|
|||
} |
|||
|
|||
@Override |
|||
public void connectComplete(boolean b, String s) { |
|||
|
|||
} |
|||
|
|||
@Override |
|||
public void authPacketArrived(int i, MqttProperties mqttProperties) { |
|||
|
|||
} |
|||
|
|||
public List<SparkplugBProto.Payload.Metric> getMessageArrivedMetrics() { |
|||
return messageArrivedMetrics; |
|||
} |
|||
|
|||
public void deleteMessageArrivedMetrics(int id) { |
|||
messageArrivedMetrics.remove(id); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,432 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.sparkplug.attributes; |
|||
|
|||
import com.fasterxml.jackson.core.type.TypeReference; |
|||
import io.netty.handler.codec.mqtt.MqttQoS; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.junit.Assert; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.transport.mqtt.sparkplug.AbstractMqttV5ClientSparkplugTest; |
|||
import org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType; |
|||
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; |
|||
|
|||
import java.util.List; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.concurrent.atomic.AtomicReference; |
|||
|
|||
import static org.awaitility.Awaitility.await; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|||
import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt32; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NCMD; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; |
|||
|
|||
/** |
|||
* Created by nickAS21 on 12.01.23 |
|||
*/ |
|||
@Slf4j |
|||
public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends AbstractMqttV5ClientSparkplugTest { |
|||
|
|||
protected void processClientWithCorrectAccessTokenPublishNCMDReBirth() throws Exception { |
|||
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|||
List<String> listKeys = connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); |
|||
// Shared attribute "Node Control/Rebirth" = true. type = NCMD.
|
|||
boolean value = true; |
|||
Assert.assertTrue(listKeys.contains(keyNodeRebirth)); |
|||
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + keyNodeRebirth + "\":" + value + "}"; |
|||
Assert.assertTrue("Connection node is failed", client.isConnected()); |
|||
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(keyNodeRebirth, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertTrue(mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue()); |
|||
} |
|||
|
|||
/** |
|||
* If boolean - send long 0 or 1 |
|||
* If String - try to parse long |
|||
* If double - cast long |
|||
* If we can't parse, cast, or JSON there - debug the message with the id of the devise/node, tenant, |
|||
* the name and type of the attribute into an error and don't send anything. |
|||
*/ |
|||
protected void processClientWithCorrectAccessTokenPublishNCMD_BooleanType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { |
|||
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|||
MetricDataType metricDataType = MetricDataType.Boolean; |
|||
String metricKey = "MyBoolean"; |
|||
Object metricValue = nextBoolean(); |
|||
connectionWithNBirth(metricDataType, metricKey, metricValue); |
|||
Assert.assertTrue("Connection node is failed", client.isConnected()); |
|||
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); |
|||
|
|||
// Boolean <-> String
|
|||
boolean expectedValue = true; |
|||
String valueStr = "123"; |
|||
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue()); |
|||
mqttCallback.deleteMessageArrivedMetrics(0); |
|||
|
|||
expectedValue = false; |
|||
valueStr = "0"; |
|||
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue()); |
|||
mqttCallback.deleteMessageArrivedMetrics(0); |
|||
|
|||
// Boolean <-> Integer
|
|||
expectedValue = true; |
|||
Integer valueInt = nextInt32(); |
|||
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueInt + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue()); |
|||
mqttCallback.deleteMessageArrivedMetrics(0); |
|||
|
|||
expectedValue = false; |
|||
valueInt = 0; |
|||
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueInt + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue()); |
|||
} |
|||
|
|||
protected void processClientWithCorrectAccessTokenPublishNCMD_LongType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { |
|||
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|||
MetricDataType metricDataType = UInt32; |
|||
String metricKey = "MyLong"; |
|||
Object metricValue = nextUInt32(); |
|||
connectionWithNBirth(metricDataType, metricKey, metricValue); |
|||
Assert.assertTrue("Connection node is failed", client.isConnected()); |
|||
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); |
|||
|
|||
// Long <-> String
|
|||
String valueStr = "123"; |
|||
long expectedValue = Long.valueOf(valueStr); |
|||
|
|||
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getLongValue()); |
|||
mqttCallback.deleteMessageArrivedMetrics(0); |
|||
|
|||
// Long <-> Boolean
|
|||
Boolean valueBoolean = true; |
|||
expectedValue = 1L; |
|||
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getLongValue()); |
|||
mqttCallback.deleteMessageArrivedMetrics(0); |
|||
|
|||
valueBoolean = false; |
|||
expectedValue = 0L; |
|||
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getLongValue()); |
|||
} |
|||
|
|||
protected void processClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { |
|||
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|||
MetricDataType metricDataType = MetricDataType.Float; |
|||
String metricKey = "MyFloat"; |
|||
Object metricValue = nextFloat(30, 400); |
|||
connectionWithNBirth(metricDataType, metricKey, metricValue); |
|||
Assert.assertTrue("Connection node is failed", client.isConnected()); |
|||
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); |
|||
|
|||
// Float <-> String
|
|||
String valueStr = "123.345"; |
|||
float expectedValue = Float.valueOf(valueStr); |
|||
|
|||
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getFloatValue()); |
|||
mqttCallback.deleteMessageArrivedMetrics(0); |
|||
|
|||
// Float <-> Boolean
|
|||
Boolean valueBoolean = true; |
|||
expectedValue = 1f; |
|||
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getFloatValue()); |
|||
mqttCallback.deleteMessageArrivedMetrics(0); |
|||
|
|||
valueBoolean = false; |
|||
expectedValue = 0f; |
|||
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getFloatValue()); |
|||
} |
|||
|
|||
protected void processClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { |
|||
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|||
MetricDataType metricDataType = MetricDataType.Double; |
|||
String metricKey = "MyDouble"; |
|||
Object metricValue = nextDouble(); |
|||
connectionWithNBirth(metricDataType, metricKey, metricValue); |
|||
Assert.assertTrue("Connection node is failed", client.isConnected()); |
|||
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); |
|||
|
|||
// Double <-> String
|
|||
String valueStr = "123345456"; |
|||
double expectedValue = Double.valueOf(valueStr); |
|||
|
|||
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getDoubleValue()); |
|||
mqttCallback.deleteMessageArrivedMetrics(0); |
|||
|
|||
// Double <-> Boolean
|
|||
Boolean valueBoolean = true; |
|||
expectedValue = 1d; |
|||
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getDoubleValue()); |
|||
mqttCallback.deleteMessageArrivedMetrics(0); |
|||
|
|||
valueBoolean = false; |
|||
expectedValue = 0d; |
|||
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getDoubleValue()); |
|||
} |
|||
|
|||
protected void processClientWithCorrectAccessTokenPublishNCMD_StringType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { |
|||
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|||
MetricDataType metricDataType = MetricDataType.String; |
|||
String metricKey = "MyString"; |
|||
Object metricValue = nextString(); |
|||
connectionWithNBirth(metricDataType, metricKey, metricValue); |
|||
Assert.assertTrue("Connection node is failed", client.isConnected()); |
|||
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); |
|||
|
|||
// String <-> Long
|
|||
long valueLong = 123345456L; |
|||
String expectedValue = String.valueOf(valueLong); |
|||
|
|||
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueLong + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getStringValue()); |
|||
mqttCallback.deleteMessageArrivedMetrics(0); |
|||
|
|||
// String <-> Boolean
|
|||
Boolean valueBoolean = true; |
|||
expectedValue = "true"; |
|||
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getStringValue()); |
|||
mqttCallback.deleteMessageArrivedMetrics(0); |
|||
|
|||
valueBoolean = false; |
|||
expectedValue = "false"; |
|||
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getStringValue()); |
|||
} |
|||
|
|||
protected void processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttribute() throws Exception { |
|||
long ts = calendar.getTimeInMillis(); |
|||
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts); |
|||
|
|||
// Integer <-> Integer
|
|||
int expectedValueInt = 123456; |
|||
|
|||
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricBirthName_Int32 + "\":" + expectedValueInt + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.DBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertEquals(expectedValueInt, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); |
|||
} |
|||
|
|||
protected void processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttributes_LongType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { |
|||
long ts = calendar.getTimeInMillis(); |
|||
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts); |
|||
|
|||
// Int <-> String
|
|||
String valueStr = "123"; |
|||
long expectedValue = Long.valueOf(valueStr); |
|||
|
|||
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricBirthName_Int32 + "\":" + valueStr + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.DBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); |
|||
mqttCallback.deleteMessageArrivedMetrics(0); |
|||
|
|||
// Int <-> Boolean
|
|||
Boolean valueBoolean = true; |
|||
expectedValue = 1; |
|||
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricBirthName_Int32 + "\":" + valueBoolean + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); |
|||
mqttCallback.deleteMessageArrivedMetrics(0); |
|||
|
|||
valueBoolean = false; |
|||
expectedValue = 0; |
|||
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricBirthName_Int32 + "\":" + valueBoolean + "}"; |
|||
doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); |
|||
} |
|||
|
|||
protected void processClientNodeWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception { |
|||
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|||
connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); |
|||
String urlTemplate = "/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/keys/attributes/" + CLIENT_SCOPE; |
|||
AtomicReference<List<String>> actualKeys = new AtomicReference<>(); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
actualKeys.set(doGetAsyncTyped(urlTemplate, new TypeReference<>() { |
|||
})); |
|||
return actualKeys.get().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricBirthName_Int32, actualKeys.get().get(0)); |
|||
} |
|||
|
|||
protected void processClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception { |
|||
long ts = calendar.getTimeInMillis(); |
|||
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts); |
|||
String urlTemplate = "/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/keys/attributes/" + CLIENT_SCOPE; |
|||
AtomicReference<List<String>> actualKeys = new AtomicReference<>(); |
|||
await(alias + SparkplugMessageType.DBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
actualKeys.set(doGetAsyncTyped(urlTemplate, new TypeReference<>() { |
|||
})); |
|||
return actualKeys.get().size() == 1; |
|||
}); |
|||
Assert.assertEquals(metricBirthName_Int32, actualKeys.get().get(0)); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.sparkplug.attributes; |
|||
|
|||
import org.eclipse.paho.mqttv5.common.MqttException; |
|||
import org.junit.After; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
|
|||
import java.util.HashSet; |
|||
|
|||
/** |
|||
* Created by nickAS21 on 12.01.23 |
|||
*/ |
|||
@DaoSqlTest |
|||
public class MqttV5ClientSparkplugBAttributesInProfileTest extends AbstractMqttV5ClientSparkplugAttributesTest { |
|||
|
|||
@Before |
|||
public void beforeTest() throws Exception { |
|||
sparkplugAttributesMetricNames = new HashSet<>(); |
|||
sparkplugAttributesMetricNames.add(metricBirthName_Int32); |
|||
beforeSparkplugTest(); |
|||
} |
|||
|
|||
@After |
|||
public void afterTest () throws MqttException { |
|||
if (client.isConnected()) { |
|||
client.disconnect(); } |
|||
} |
|||
|
|||
@Test |
|||
public void testClientNodeWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception { |
|||
processClientNodeWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes(); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception { |
|||
processClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,81 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.sparkplug.attributes; |
|||
|
|||
import org.eclipse.paho.mqttv5.common.MqttException; |
|||
import org.junit.After; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
|
|||
/** |
|||
* Created by nickAS21 on 12.01.23 |
|||
*/ |
|||
@DaoSqlTest |
|||
public class MqttV5ClientSparkplugBAttributesTest extends AbstractMqttV5ClientSparkplugAttributesTest { |
|||
|
|||
@Before |
|||
public void beforeTest() throws Exception { |
|||
beforeSparkplugTest(); |
|||
} |
|||
|
|||
@After |
|||
public void afterTest () throws MqttException { |
|||
if (client.isConnected()) { |
|||
client.disconnect(); } |
|||
} |
|||
|
|||
@Test |
|||
public void testClientWithCorrectAccessTokenPublishNCMDReBirth() throws Exception { |
|||
processClientWithCorrectAccessTokenPublishNCMDReBirth(); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientWithCorrectAccessTokenPublishNCMD_BooleanType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { |
|||
processClientWithCorrectAccessTokenPublishNCMD_BooleanType_IfMetricFailedTypeCheck_SendValueOk(); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientWithCorrectAccessTokenPublishNCMD_LongType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { |
|||
processClientWithCorrectAccessTokenPublishNCMD_LongType_IfMetricFailedTypeCheck_SendValueOk(); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { |
|||
processClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk(); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { |
|||
processClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk(); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientWithCorrectAccessTokenPublishNCMD_StringType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { |
|||
processClientWithCorrectAccessTokenPublishNCMD_StringType_IfMetricFailedTypeCheck_SendValueOk(); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttribute() throws Exception { |
|||
processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttribute(); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttributes_LongType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { |
|||
processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttributes_LongType_IfMetricFailedTypeCheck_SendValueOk(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,178 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.sparkplug.connection; |
|||
|
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.eclipse.paho.mqttv5.common.MqttException; |
|||
import org.junit.Assert; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
|||
import org.thingsboard.server.common.data.kv.LongDataEntry; |
|||
import org.thingsboard.server.common.data.kv.StringDataEntry; |
|||
import org.thingsboard.server.common.data.kv.TsKvEntry; |
|||
import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; |
|||
import org.thingsboard.server.transport.mqtt.mqttv5.MqttV5TestClient; |
|||
import org.thingsboard.server.transport.mqtt.sparkplug.AbstractMqttV5ClientSparkplugTest; |
|||
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.Optional; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.concurrent.atomic.AtomicReference; |
|||
|
|||
import static org.awaitility.Awaitility.await; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.OFFLINE; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.ONLINE; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.messageName; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; |
|||
|
|||
/** |
|||
* Created by nickAS21 on 12.01.23 |
|||
*/ |
|||
@Slf4j |
|||
public abstract class AbstractMqttV5ClientSparkplugConnectionTest extends AbstractMqttV5ClientSparkplugTest { |
|||
|
|||
protected void processClientWithCorrectNodeAccessTokenWithNDEATH_Test() throws Exception { |
|||
long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; |
|||
long value = bdSeq = 0; |
|||
clientWithCorrectNodeAccessTokenWithNDEATH(ts, value); |
|||
|
|||
String keys = SparkplugMessageType.NDEATH.name() + " " + keysBdSeq; |
|||
TsKvEntry expectedTsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(keys, value)); |
|||
AtomicReference<ListenableFuture<Optional<TsKvEntry>>> finalFuture = new AtomicReference<>(); |
|||
await(alias + SparkplugMessageType.NDEATH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
finalFuture.set(tsService.findLatest(tenantId, savedGateway.getId(), keys)); |
|||
return finalFuture.get().get().isPresent(); |
|||
}); |
|||
TsKvEntry actualTsKvEntry = finalFuture.get().get().get(); |
|||
Assert.assertEquals(expectedTsKvEntry, actualTsKvEntry); |
|||
} |
|||
|
|||
protected void processClientWithCorrectNodeAccessTokenWithoutNDEATH_Test() throws Exception { |
|||
this.client = new MqttV5TestClient(); |
|||
MqttException actualException = Assert.assertThrows(MqttException.class, () -> client.connectAndWait(gatewayAccessToken)); |
|||
String expectedMessage = "Server unavailable."; |
|||
int expectedReasonCode = 136; |
|||
Assert.assertEquals(expectedMessage, actualException.getMessage()); |
|||
Assert.assertEquals(expectedReasonCode, actualException.getReasonCode()); |
|||
} |
|||
|
|||
protected void processClientWithCorrectNodeAccessTokenNameSpaceInvalid_Test() throws Exception { |
|||
long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; |
|||
long value = bdSeq = 0; |
|||
MqttException actualException = Assert.assertThrows(MqttException.class, () -> clientConnectWithNDEATH(ts, value, "spBv1.2")); |
|||
String expectedMessage = "Server unavailable."; |
|||
int expectedReasonCode = 136; |
|||
Assert.assertEquals(expectedMessage, actualException.getMessage()); |
|||
Assert.assertEquals(expectedReasonCode, actualException.getReasonCode()); |
|||
} |
|||
|
|||
protected void processClientWithCorrectAccessTokenWithNDEATHCreatedDevices(int cntDevices) throws Exception { |
|||
long ts = calendar.getTimeInMillis(); |
|||
connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts); |
|||
} |
|||
|
|||
protected void processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_ALL(int cntDevices) throws Exception { |
|||
long ts = calendar.getTimeInMillis(); |
|||
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts); |
|||
|
|||
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(messageName(STATE), ONLINE.name())); |
|||
AtomicReference<ListenableFuture<List<TsKvEntry>>> finalFuture = new AtomicReference<>(); |
|||
await(alias + messageName(STATE) + ", device: " + savedGateway.getName()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); |
|||
return finalFuture.get().get().contains(tsKvEntry); |
|||
}); |
|||
|
|||
for (Device device : devices) { |
|||
await(alias + messageName(STATE) + ", device: " + device.getName()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
finalFuture.set(tsService.findAllLatest(tenantId, device.getId())); |
|||
return finalFuture.get().get().contains(tsKvEntry); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
protected void processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OneDeviceOFFLINE(int cntDevices, int indexDeviceDisconnect) throws Exception { |
|||
long ts = calendar.getTimeInMillis(); |
|||
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts); |
|||
|
|||
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(messageName(STATE), OFFLINE.name())); |
|||
AtomicReference<ListenableFuture<List<TsKvEntry>>> finalFuture = new AtomicReference<>(); |
|||
|
|||
SparkplugBProto.Payload.Builder payloadDeathDevice = SparkplugBProto.Payload.newBuilder() |
|||
.setTimestamp(ts) |
|||
.setSeq(getSeqNum()); |
|||
if (client.isConnected()) { |
|||
List<Device> devicesList = new ArrayList<>(devices); |
|||
Device device = devicesList.get(indexDeviceDisconnect); |
|||
client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.DDEATH.name() + "/" + edgeNode + "/" + device.getName(), |
|||
payloadDeathDevice.build().toByteArray(), 0, false); |
|||
await(alias + messageName(STATE) + ", device: " + device.getName()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
finalFuture.set(tsService.findAllLatest(tenantId, device.getId())); |
|||
return findEqualsKeyValueInKvEntrys(finalFuture.get().get(), tsKvEntry); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
protected void processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OFFLINE_All(int cntDevices) throws Exception { |
|||
long ts = calendar.getTimeInMillis(); |
|||
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts); |
|||
|
|||
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(messageName(STATE), OFFLINE.name())); |
|||
AtomicReference<ListenableFuture<List<TsKvEntry>>> finalFuture = new AtomicReference<>(); |
|||
|
|||
if (client.isConnected()) { |
|||
client.disconnect(); |
|||
|
|||
await(alias + messageName(STATE) + ", device: " + savedGateway.getName()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); |
|||
return findEqualsKeyValueInKvEntrys(finalFuture.get().get(), tsKvEntry); |
|||
}); |
|||
|
|||
List<Device> devicesList = new ArrayList<>(devices); |
|||
for (Device device : devicesList) { |
|||
await(alias + messageName(STATE) + ", device: " + device.getName()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
finalFuture.set(tsService.findAllLatest(tenantId, device.getId())); |
|||
return findEqualsKeyValueInKvEntrys(finalFuture.get().get(), tsKvEntry); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private boolean findEqualsKeyValueInKvEntrys(List<TsKvEntry> finalFuture, TsKvEntry tsKvEntry) { |
|||
for (TsKvEntry kvEntry : finalFuture) { |
|||
if (kvEntry.getKey().equals(tsKvEntry.getKey()) && kvEntry.getValue().equals(tsKvEntry.getValue())) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.sparkplug.connection; |
|||
|
|||
import org.eclipse.paho.mqttv5.common.MqttException; |
|||
import org.junit.After; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
|
|||
/** |
|||
* Created by nickAS21 on 12.01.23 |
|||
*/ |
|||
@DaoSqlTest |
|||
public class MqttV5ClientSparkplugBConnectionTest extends AbstractMqttV5ClientSparkplugConnectionTest { |
|||
|
|||
@Before |
|||
public void beforeTest() throws Exception { |
|||
beforeSparkplugTest(); |
|||
} |
|||
|
|||
@After |
|||
public void afterTest() throws MqttException { |
|||
if (client.isConnected()) { |
|||
client.disconnect(); |
|||
} |
|||
} |
|||
|
|||
@Test |
|||
public void testClientWithCorrectAccessTokenWithNDEATH() throws Exception { |
|||
processClientWithCorrectNodeAccessTokenWithNDEATH_Test(); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientWithCorrectNodeAccessTokenWithoutNDEATH() throws Exception { |
|||
processClientWithCorrectNodeAccessTokenWithoutNDEATH_Test(); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientWithCorrectNodeAccessTokenNameSpaceInvalid() throws Exception { |
|||
processClientWithCorrectNodeAccessTokenNameSpaceInvalid_Test(); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientWithCorrectAccessTokenWithNDEATHCreatedOneDevice() throws Exception { |
|||
processClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientWithCorrectAccessTokenWithNDEATHCreatedTwoDevice() throws Exception { |
|||
processClientWithCorrectAccessTokenWithNDEATHCreatedDevices(2); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_ALL() throws Exception { |
|||
processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_ALL(3); |
|||
} |
|||
|
|||
@Test |
|||
public void testConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OneDeviceOFFLINE() throws Exception { |
|||
processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OneDeviceOFFLINE(3, 1); |
|||
} |
|||
|
|||
@Test |
|||
public void testConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OFFLINE_All() throws Exception { |
|||
processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OFFLINE_All(3); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.sparkplug.rpc; |
|||
|
|||
import io.netty.handler.codec.mqtt.MqttQoS; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.junit.Assert; |
|||
import org.junit.Test; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.transport.mqtt.sparkplug.AbstractMqttV5ClientSparkplugTest; |
|||
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; |
|||
|
|||
import java.util.List; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
import static org.awaitility.Awaitility.await; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|||
import static org.thingsboard.server.common.data.exception.ThingsboardErrorCode.INVALID_ARGUMENTS; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DCMD; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NCMD; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; |
|||
|
|||
@Slf4j |
|||
public abstract class AbstractMqttV5RpcSparkplugTest extends AbstractMqttV5ClientSparkplugTest { |
|||
|
|||
private static final int metricBirthValue_Int32 = 123456; |
|||
private static final String sparkplugRpcRequest = "{\"metricName\":\"" + metricBirthName_Int32 + "\",\"value\":" + metricBirthValue_Int32 + "}"; |
|||
|
|||
@Test |
|||
public void processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception { |
|||
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|||
connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); |
|||
Assert.assertTrue("Connection node is failed", client.isConnected()); |
|||
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); |
|||
String expected = "{\"result\":\"Success: " + SparkplugMessageType.NCMD.name() + "\"}"; |
|||
String actual = sendRPCSparkplug(NCMD.name(), sparkplugRpcRequest, savedGateway); |
|||
await(alias + SparkplugMessageType.NCMD.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(expected, actual); |
|||
Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertTrue(metricBirthValue_Int32 == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); |
|||
} |
|||
|
|||
@Test |
|||
public void processClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception { |
|||
long ts = calendar.getTimeInMillis(); |
|||
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts); |
|||
String expected = "{\"result\":\"Success: " + DCMD.name() + "\"}"; |
|||
String actual = sendRPCSparkplug(DCMD.name() , sparkplugRpcRequest, devices.get(0)); |
|||
await(alias + NCMD.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
return mqttCallback.getMessageArrivedMetrics().size() == 1; |
|||
}); |
|||
Assert.assertEquals(expected, actual); |
|||
Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); |
|||
Assert.assertTrue(metricBirthValue_Int32 == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); |
|||
} |
|||
|
|||
@Test |
|||
public void processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS() throws Exception { |
|||
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|||
connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); |
|||
Assert.assertTrue("Connection node is failed", client.isConnected()); |
|||
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); |
|||
String invalidateTypeMessageName = "RCMD"; |
|||
String expected = "{\"result\":\"" + INVALID_ARGUMENTS + "\",\"error\":\"Failed to convert device RPC command to MQTT msg: " + |
|||
invalidateTypeMessageName + "{\\\"metricName\\\":\\\"" + metricBirthName_Int32 + "\\\",\\\"value\\\":" + metricBirthValue_Int32 + "}\"}"; |
|||
String actual = sendRPCSparkplug(invalidateTypeMessageName, sparkplugRpcRequest, savedGateway); |
|||
Assert.assertEquals(expected, actual); |
|||
} |
|||
|
|||
@Test |
|||
public void processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InBirthNotHaveMetric_BAD_REQUEST_PARAMS() throws Exception { |
|||
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|||
connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); |
|||
Assert.assertTrue("Connection node is failed", client.isConnected()); |
|||
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); |
|||
String metricNameBad = metricBirthName_Int32 + "_Bad"; |
|||
String sparkplugRpcRequestBad = "{\"metricName\":\"" + metricNameBad + "\",\"value\":" + metricBirthValue_Int32 + "}"; |
|||
String expected = "{\"result\":\"BAD_REQUEST_PARAMS\",\"error\":\"Failed send To Node Rpc Request: " + |
|||
DCMD.name() + ". This node does not have a metricName: [" + metricNameBad + "]\"}"; |
|||
String actual = sendRPCSparkplug(DCMD.name(), sparkplugRpcRequestBad, savedGateway); |
|||
Assert.assertEquals(expected, actual); |
|||
} |
|||
|
|||
private String sendRPCSparkplug(String nameTypeMessage, String keyValue, Device device) throws Exception { |
|||
String setRpcRequest = "{\"method\": \"" + nameTypeMessage + "\", \"params\": " + keyValue + "}"; |
|||
return doPostAsync("/api/plugins/rpc/twoway/" + device.getId().getId(), setRpcRequest, String.class, status().isOk()); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.sparkplug.rpc; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.eclipse.paho.mqttv5.common.MqttException; |
|||
import org.junit.After; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
|
|||
@DaoSqlTest |
|||
@Slf4j |
|||
public class MqttV5RpcSparkplugTest extends AbstractMqttV5RpcSparkplugTest { |
|||
|
|||
@Before |
|||
public void beforeTest() throws Exception { |
|||
beforeSparkplugTest(); |
|||
} |
|||
|
|||
@After |
|||
public void afterTest() throws MqttException { |
|||
if (client.isConnected()) { |
|||
client.disconnect(); |
|||
} |
|||
} |
|||
|
|||
@Test |
|||
public void testClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception { |
|||
processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_Success(); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception { |
|||
processClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success(); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS() throws Exception { |
|||
processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS(); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InBirthNotHaveMetric_BAD_REQUEST_PARAMS() throws Exception { |
|||
processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,113 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.sparkplug.timeseries; |
|||
|
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.junit.Assert; |
|||
import org.thingsboard.server.common.data.kv.TsKvEntry; |
|||
import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; |
|||
import org.thingsboard.server.transport.mqtt.sparkplug.AbstractMqttV5ClientSparkplugTest; |
|||
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.concurrent.atomic.AtomicReference; |
|||
|
|||
import static org.awaitility.Awaitility.await; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; |
|||
|
|||
/** |
|||
* Created by nickAS21 on 12.01.23 |
|||
*/ |
|||
@Slf4j |
|||
public abstract class AbstractMqttV5ClientSparkplugTelemetryTest extends AbstractMqttV5ClientSparkplugTest { |
|||
|
|||
protected void processClientWithCorrectAccessTokenPublishNBIRTH() throws Exception { |
|||
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|||
List<String> listKeys = connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); |
|||
Assert.assertTrue("Connection node is failed", client.isConnected()); |
|||
AtomicReference<ListenableFuture<List<TsKvEntry>>> finalFuture = new AtomicReference<>(); |
|||
await(alias + SparkplugMessageType.NBIRTH.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
finalFuture.set(tsService.findLatest(tenantId, savedGateway.getId(), listKeys)); |
|||
return !finalFuture.get().get().isEmpty(); |
|||
}); |
|||
Assert.assertEquals(listKeys.size(), finalFuture.get().get().size()); |
|||
} |
|||
|
|||
protected void processClientWithCorrectAccessTokenPushNodeMetricBuildPrimitiveSimple() throws Exception { |
|||
List<String> listKeys = new ArrayList<>(); |
|||
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|||
|
|||
String messageTypeName = SparkplugMessageType.NDATA.name(); |
|||
|
|||
List<TsKvEntry> listTsKvEntry = new ArrayList<>(); |
|||
|
|||
SparkplugBProto.Payload.Builder ndataPayload = SparkplugBProto.Payload.newBuilder() |
|||
.setTimestamp(calendar.getTimeInMillis()) |
|||
.setSeq(getSeqNum()); |
|||
long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; |
|||
|
|||
createdAddMetricValuePrimitiveTsKv(listTsKvEntry, listKeys, ndataPayload, ts); |
|||
|
|||
if (client.isConnected()) { |
|||
client.publish(NAMESPACE + "/" + groupId + "/" + messageTypeName + "/" + edgeNode, |
|||
ndataPayload.build().toByteArray(), 0, false); |
|||
} |
|||
|
|||
AtomicReference<ListenableFuture<List<TsKvEntry>>> finalFuture = new AtomicReference<>(); |
|||
await(alias + SparkplugMessageType.NDATA.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); |
|||
return finalFuture.get().get().size() == (listTsKvEntry.size() + 1); |
|||
}); |
|||
Assert.assertTrue("Actual tsKvEntrys is not containsAll Expected tsKvEntrys", finalFuture.get().get().containsAll(listTsKvEntry)); |
|||
} |
|||
|
|||
protected void processClientWithCorrectAccessTokenPushNodeMetricBuildArraysPrimitiveSimple() throws Exception { |
|||
clientWithCorrectNodeAccessTokenWithNDEATH(); |
|||
|
|||
String messageTypeName = SparkplugMessageType.NDATA.name(); |
|||
List<String> listKeys = new ArrayList<>(); |
|||
List<TsKvEntry> listTsKvEntry = new ArrayList<>(); |
|||
|
|||
SparkplugBProto.Payload.Builder ndataPayload = SparkplugBProto.Payload.newBuilder() |
|||
.setTimestamp(calendar.getTimeInMillis()) |
|||
.setSeq(getSeqNum()); |
|||
long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; |
|||
|
|||
createdAddMetricValueArraysPrimitiveTsKv(listTsKvEntry, listKeys, ndataPayload, ts); |
|||
|
|||
if (client.isConnected()) { |
|||
client.publish(NAMESPACE + "/" + groupId + "/" + messageTypeName + "/" + edgeNode, |
|||
ndataPayload.build().toByteArray(), 0, false); |
|||
} |
|||
|
|||
AtomicReference<ListenableFuture<List<TsKvEntry>>> finalFuture = new AtomicReference<>(); |
|||
await(alias + SparkplugMessageType.NDATA.name()) |
|||
.atMost(40, TimeUnit.SECONDS) |
|||
.until(() -> { |
|||
finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); |
|||
return finalFuture.get().get().size() == (listTsKvEntry.size() + 1); |
|||
}); |
|||
Assert.assertTrue("Actual tsKvEntrys is not containsAll Expected tsKvEntrys", finalFuture.get().get().containsAll(listTsKvEntry)); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.sparkplug.timeseries; |
|||
|
|||
import org.eclipse.paho.mqttv5.common.MqttException; |
|||
import org.junit.After; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
|
|||
/** |
|||
* Created by nickAS21 on 12.01.23 |
|||
*/ |
|||
@DaoSqlTest |
|||
public class MqttV5ClientSparkplugBTelemetryTest extends AbstractMqttV5ClientSparkplugTelemetryTest { |
|||
|
|||
@Before |
|||
public void beforeTest() throws Exception { |
|||
beforeSparkplugTest(); |
|||
} |
|||
|
|||
@After |
|||
public void afterTest () throws MqttException { |
|||
if (client.isConnected()) { |
|||
client.disconnect(); } |
|||
} |
|||
|
|||
@Test |
|||
public void testClientWithCorrectAccessTokenPublishNBIRTH() throws Exception { |
|||
processClientWithCorrectAccessTokenPublishNBIRTH(); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientWithCorrectAccessTokenPushNodeMetricBuildPrimitiveSimple() throws Exception { |
|||
processClientWithCorrectAccessTokenPushNodeMetricBuildPrimitiveSimple(); |
|||
} |
|||
|
|||
@Test |
|||
public void testClientWithCorrectAccessTokenPushNodeMetricBuildPArraysPrimitiveSimple() throws Exception { |
|||
processClientWithCorrectAccessTokenPushNodeMetricBuildArraysPrimitiveSimple(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,767 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.session; |
|||
|
|||
|
|||
import com.google.common.util.concurrent.FutureCallback; |
|||
import com.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import com.google.common.util.concurrent.SettableFuture; |
|||
import com.google.gson.JsonArray; |
|||
import com.google.gson.JsonElement; |
|||
import com.google.gson.JsonNull; |
|||
import com.google.gson.JsonObject; |
|||
import com.google.gson.JsonSyntaxException; |
|||
import com.google.protobuf.InvalidProtocolBufferException; |
|||
import com.google.protobuf.ProtocolStringList; |
|||
import io.netty.buffer.ByteBuf; |
|||
import io.netty.channel.ChannelFuture; |
|||
import io.netty.channel.ChannelHandlerContext; |
|||
import io.netty.handler.codec.mqtt.MqttMessage; |
|||
import io.netty.handler.codec.mqtt.MqttPublishMessage; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.util.CollectionUtils; |
|||
import org.springframework.util.ConcurrentReferenceHashMap; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.transport.TransportService; |
|||
import org.thingsboard.server.common.transport.TransportServiceCallback; |
|||
import org.thingsboard.server.common.transport.adaptor.AdaptorException; |
|||
import org.thingsboard.server.common.transport.adaptor.JsonConverter; |
|||
import org.thingsboard.server.common.transport.adaptor.ProtoConverter; |
|||
import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; |
|||
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; |
|||
import org.thingsboard.server.gen.transport.TransportApiProtos; |
|||
import org.thingsboard.server.gen.transport.TransportProtos; |
|||
import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; |
|||
import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; |
|||
import org.thingsboard.server.transport.mqtt.MqttTransportContext; |
|||
import org.thingsboard.server.transport.mqtt.MqttTransportHandler; |
|||
import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; |
|||
import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; |
|||
import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; |
|||
import org.thingsboard.server.transport.mqtt.util.ReturnCode; |
|||
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState; |
|||
|
|||
import javax.annotation.Nullable; |
|||
import java.util.ArrayList; |
|||
import java.util.Collections; |
|||
import java.util.Date; |
|||
import java.util.HashSet; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Set; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.concurrent.ConcurrentMap; |
|||
import java.util.concurrent.locks.Lock; |
|||
import java.util.concurrent.locks.ReentrantLock; |
|||
|
|||
import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; |
|||
import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_CLOSED; |
|||
import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_OPEN; |
|||
import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG; |
|||
import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_RPC_ASYNC_MSG; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.OFFLINE; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.messageName; |
|||
|
|||
/** |
|||
* Created by ashvayka on 19.01.17. |
|||
*/ |
|||
@Slf4j |
|||
public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDeviceSessionContext> { |
|||
|
|||
protected static final String DEFAULT_DEVICE_TYPE = "default"; |
|||
private static final String CAN_T_PARSE_VALUE = "Can't parse value: "; |
|||
private static final String DEVICE_PROPERTY = "device"; |
|||
|
|||
protected final MqttTransportContext context; |
|||
protected final TransportService transportService; |
|||
protected final TransportDeviceInfo gateway; |
|||
protected final UUID sessionId; |
|||
private final ConcurrentMap<String, Lock> deviceCreationLockMap; |
|||
private final ConcurrentMap<String, T> devices; |
|||
private final ConcurrentMap<String, ListenableFuture<T>> deviceFutures; |
|||
protected final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap; |
|||
protected final ChannelHandlerContext channel; |
|||
protected final DeviceSessionCtx deviceSessionCtx; |
|||
|
|||
public AbstractGatewaySessionHandler(DeviceSessionCtx deviceSessionCtx, UUID sessionId) { |
|||
this.context = deviceSessionCtx.getContext(); |
|||
this.transportService = context.getTransportService(); |
|||
this.deviceSessionCtx = deviceSessionCtx; |
|||
this.gateway = deviceSessionCtx.getDeviceInfo(); |
|||
this.sessionId = sessionId; |
|||
this.devices = new ConcurrentHashMap<>(); |
|||
this.deviceFutures = new ConcurrentHashMap<>(); |
|||
this.deviceCreationLockMap = createWeakMap(); |
|||
this.mqttQoSMap = deviceSessionCtx.getMqttQoSMap(); |
|||
this.channel = deviceSessionCtx.getChannel(); |
|||
} |
|||
|
|||
ConcurrentReferenceHashMap<String, Lock> createWeakMap() { |
|||
return new ConcurrentReferenceHashMap<>(16, ReferenceType.WEAK); |
|||
} |
|||
|
|||
public void onDeviceDisconnect(MqttPublishMessage mqttMsg) throws AdaptorException { |
|||
if (isJsonPayloadType()) { |
|||
onDeviceDisconnectJson(mqttMsg); |
|||
} else { |
|||
onGatewayDeviceDisconnectProto(mqttMsg); |
|||
} |
|||
} |
|||
|
|||
public void onDeviceClaim(MqttPublishMessage mqttMsg) throws AdaptorException { |
|||
int msgId = getMsgId(mqttMsg); |
|||
ByteBuf payload = mqttMsg.payload(); |
|||
if (isJsonPayloadType()) { |
|||
onDeviceClaimJson(msgId, payload); |
|||
} else { |
|||
onDeviceClaimProto(msgId, payload); |
|||
} |
|||
} |
|||
|
|||
public void onDeviceAttributes(MqttPublishMessage mqttMsg) throws AdaptorException { |
|||
int msgId = getMsgId(mqttMsg); |
|||
ByteBuf payload = mqttMsg.payload(); |
|||
if (isJsonPayloadType()) { |
|||
onDeviceAttributesJson(msgId, payload); |
|||
} else { |
|||
onDeviceAttributesProto(msgId, payload); |
|||
} |
|||
} |
|||
|
|||
public void onDeviceAttributesRequest(MqttPublishMessage mqttMsg) throws AdaptorException { |
|||
if (isJsonPayloadType()) { |
|||
onDeviceAttributesRequestJson(mqttMsg); |
|||
} else { |
|||
onDeviceAttributesRequestProto(mqttMsg); |
|||
} |
|||
} |
|||
|
|||
public void onDeviceRpcResponse(MqttPublishMessage mqttMsg) throws AdaptorException { |
|||
int msgId = getMsgId(mqttMsg); |
|||
ByteBuf payload = mqttMsg.payload(); |
|||
if (isJsonPayloadType()) { |
|||
onDeviceRpcResponseJson(msgId, payload); |
|||
} else { |
|||
onDeviceRpcResponseProto(msgId, payload); |
|||
} |
|||
} |
|||
|
|||
public void onDevicesDisconnect() { |
|||
devices.forEach(this::deregisterSession); |
|||
} |
|||
|
|||
public void onDeviceDeleted(String deviceName) { |
|||
deregisterSession(deviceName); |
|||
} |
|||
|
|||
public String getNodeId() { |
|||
return context.getNodeId(); |
|||
} |
|||
|
|||
public UUID getSessionId() { |
|||
return sessionId; |
|||
} |
|||
|
|||
public MqttTransportAdaptor getPayloadAdaptor() { |
|||
return deviceSessionCtx.getPayloadAdaptor(); |
|||
} |
|||
|
|||
void deregisterSession(String deviceName) { |
|||
MqttDeviceAwareSessionContext deviceSessionCtx = devices.remove(deviceName); |
|||
if (deviceSessionCtx != null) { |
|||
deregisterSession(deviceName, deviceSessionCtx); |
|||
} else { |
|||
log.debug("[{}] Device [{}] was already removed from the gateway session", sessionId, deviceName); |
|||
} |
|||
} |
|||
|
|||
public ChannelFuture writeAndFlush(MqttMessage mqttMessage) { |
|||
return channel.writeAndFlush(mqttMessage); |
|||
} |
|||
|
|||
int nextMsgId() { |
|||
return deviceSessionCtx.nextMsgId(); |
|||
} |
|||
|
|||
protected boolean isJsonPayloadType() { |
|||
return deviceSessionCtx.isJsonPayloadType(); |
|||
} |
|||
|
|||
protected void processOnConnect(MqttPublishMessage msg, String deviceName, String deviceType) { |
|||
log.trace("[{}] onDeviceConnect: {}", sessionId, deviceName); |
|||
Futures.addCallback(onDeviceConnect(deviceName, deviceType), new FutureCallback<>() { |
|||
@Override |
|||
public void onSuccess(@Nullable T result) { |
|||
ack(msg, ReturnCode.SUCCESS); |
|||
log.trace("[{}] onDeviceConnectOk: {}", sessionId, deviceName); |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(Throwable t) { |
|||
log.warn("[{}] Failed to process device connect command: {}", sessionId, deviceName, t); |
|||
|
|||
} |
|||
}, context.getExecutor()); |
|||
} |
|||
|
|||
ListenableFuture<T> onDeviceConnect(String deviceName, String deviceType) { |
|||
T result = devices.get(deviceName); |
|||
if (result == null) { |
|||
Lock deviceCreationLock = deviceCreationLockMap.computeIfAbsent(deviceName, s -> new ReentrantLock()); |
|||
deviceCreationLock.lock(); |
|||
try { |
|||
result = devices.get(deviceName); |
|||
if (result == null) { |
|||
return getDeviceCreationFuture(deviceName, deviceType); |
|||
} else { |
|||
return Futures.immediateFuture(result); |
|||
} |
|||
} finally { |
|||
deviceCreationLock.unlock(); |
|||
} |
|||
} else { |
|||
return Futures.immediateFuture(result); |
|||
} |
|||
} |
|||
|
|||
private ListenableFuture<T> getDeviceCreationFuture(String deviceName, String deviceType) { |
|||
final SettableFuture<T> futureToSet = SettableFuture.create(); |
|||
ListenableFuture<T> future = deviceFutures.putIfAbsent(deviceName, futureToSet); |
|||
if (future != null) { |
|||
return future; |
|||
} |
|||
try { |
|||
transportService.process(GetOrCreateDeviceFromGatewayRequestMsg.newBuilder() |
|||
.setDeviceName(deviceName) |
|||
.setDeviceType(deviceType) |
|||
.setGatewayIdMSB(gateway.getDeviceId().getId().getMostSignificantBits()) |
|||
.setGatewayIdLSB(gateway.getDeviceId().getId().getLeastSignificantBits()) |
|||
.setSparkplug(this.deviceSessionCtx.isSparkplug()) |
|||
.build(), |
|||
new TransportServiceCallback<>() { |
|||
@Override |
|||
public void onSuccess(GetOrCreateDeviceFromGatewayResponse msg) { |
|||
T deviceSessionCtx = newDeviceSessionCtx(msg); |
|||
if (devices.putIfAbsent(deviceName, deviceSessionCtx) == null) { |
|||
log.trace("[{}] First got or created device [{}], type [{}] for the gateway session", sessionId, deviceName, deviceType); |
|||
SessionInfoProto deviceSessionInfo = deviceSessionCtx.getSessionInfo(); |
|||
transportService.registerAsyncSession(deviceSessionInfo, deviceSessionCtx); |
|||
transportService.process(TransportProtos.TransportToDeviceActorMsg.newBuilder() |
|||
.setSessionInfo(deviceSessionInfo) |
|||
.setSessionEvent(SESSION_EVENT_MSG_OPEN) |
|||
.setSubscribeToAttributes(SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG) |
|||
.setSubscribeToRPC(SUBSCRIBE_TO_RPC_ASYNC_MSG) |
|||
.build(), null); |
|||
} |
|||
futureToSet.set(devices.get(deviceName)); |
|||
deviceFutures.remove(deviceName); |
|||
} |
|||
|
|||
@Override |
|||
public void onError(Throwable e) { |
|||
log.warn("[{}] Failed to process device connect command: {}", sessionId, deviceName, e); |
|||
futureToSet.setException(e); |
|||
deviceFutures.remove(deviceName); |
|||
} |
|||
}); |
|||
return futureToSet; |
|||
} catch (Throwable e) { |
|||
deviceFutures.remove(deviceName); |
|||
throw e; |
|||
} |
|||
} |
|||
|
|||
protected abstract T newDeviceSessionCtx(GetOrCreateDeviceFromGatewayResponse msg); |
|||
|
|||
protected int getMsgId(MqttPublishMessage mqttMsg) { |
|||
return mqttMsg.variableHeader().packetId(); |
|||
} |
|||
|
|||
protected void onDeviceConnectJson(MqttPublishMessage mqttMsg) throws AdaptorException { |
|||
JsonElement json = getJson(mqttMsg); |
|||
String deviceName = checkDeviceName(getDeviceName(json)); |
|||
String deviceType = getDeviceType(json); |
|||
processOnConnect(mqttMsg, deviceName, deviceType); |
|||
} |
|||
|
|||
protected void onDeviceConnectProto(MqttPublishMessage mqttMsg) throws AdaptorException { |
|||
try { |
|||
TransportApiProtos.ConnectMsg connectProto = TransportApiProtos.ConnectMsg.parseFrom(getBytes(mqttMsg.payload())); |
|||
String deviceName = checkDeviceName(connectProto.getDeviceName()); |
|||
String deviceType = StringUtils.isEmpty(connectProto.getDeviceType()) ? DEFAULT_DEVICE_TYPE : connectProto.getDeviceType(); |
|||
processOnConnect(mqttMsg, deviceName, deviceType); |
|||
} catch (RuntimeException | InvalidProtocolBufferException e) { |
|||
throw new AdaptorException(e); |
|||
} |
|||
} |
|||
|
|||
private void onDeviceDisconnectJson(MqttPublishMessage msg) throws AdaptorException { |
|||
String deviceName = checkDeviceName(getDeviceName(getJson(msg))); |
|||
processOnDisconnect(msg, deviceName); |
|||
} |
|||
|
|||
protected void onGatewayDeviceDisconnectProto(MqttPublishMessage mqttMsg) throws AdaptorException { |
|||
try { |
|||
TransportApiProtos.DisconnectMsg connectProto = TransportApiProtos.DisconnectMsg.parseFrom(getBytes(mqttMsg.payload())); |
|||
String deviceName = checkDeviceName(connectProto.getDeviceName()); |
|||
processOnDisconnect(mqttMsg, deviceName); |
|||
} catch (RuntimeException | InvalidProtocolBufferException e) { |
|||
throw new AdaptorException(e); |
|||
} |
|||
} |
|||
|
|||
void processOnDisconnect(MqttPublishMessage msg, String deviceName) { |
|||
deregisterSession(deviceName); |
|||
ack(msg, ReturnCode.SUCCESS); |
|||
} |
|||
|
|||
protected void onDeviceTelemetryJson(int msgId, ByteBuf payload) throws AdaptorException { |
|||
JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); |
|||
if (json.isJsonObject()) { |
|||
JsonObject jsonObj = json.getAsJsonObject(); |
|||
for (Map.Entry<String, JsonElement> deviceEntry : jsonObj.entrySet()) { |
|||
String deviceName = deviceEntry.getKey(); |
|||
Futures.addCallback(checkDeviceConnected(deviceName), |
|||
new FutureCallback<>() { |
|||
@Override |
|||
public void onSuccess(@Nullable T deviceCtx) { |
|||
if (!deviceEntry.getValue().isJsonArray()) { |
|||
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); |
|||
} |
|||
try { |
|||
TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(deviceEntry.getValue().getAsJsonArray()); |
|||
processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); |
|||
} catch (Throwable e) { |
|||
log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); |
|||
channel.close(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(Throwable t) { |
|||
log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); |
|||
} |
|||
}, context.getExecutor()); |
|||
} |
|||
} else { |
|||
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); |
|||
} |
|||
} |
|||
|
|||
protected void onDeviceTelemetryProto(int msgId, ByteBuf payload) throws AdaptorException { |
|||
try { |
|||
TransportApiProtos.GatewayTelemetryMsg telemetryMsgProto = TransportApiProtos.GatewayTelemetryMsg.parseFrom(getBytes(payload)); |
|||
List<TransportApiProtos.TelemetryMsg> deviceMsgList = telemetryMsgProto.getMsgList(); |
|||
if (!CollectionUtils.isEmpty(deviceMsgList)) { |
|||
deviceMsgList.forEach(telemetryMsg -> { |
|||
String deviceName = checkDeviceName(telemetryMsg.getDeviceName()); |
|||
Futures.addCallback(checkDeviceConnected(deviceName), |
|||
new FutureCallback<>() { |
|||
@Override |
|||
public void onSuccess(@Nullable T deviceCtx) { |
|||
TransportProtos.PostTelemetryMsg msg = telemetryMsg.getMsg(); |
|||
try { |
|||
TransportProtos.PostTelemetryMsg postTelemetryMsg = ProtoConverter.validatePostTelemetryMsg(msg.toByteArray()); |
|||
processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); |
|||
} catch (Throwable e) { |
|||
log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, msg, e); |
|||
channel.close(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(Throwable t) { |
|||
log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); |
|||
} |
|||
}, context.getExecutor()); |
|||
}); |
|||
} else { |
|||
log.debug("[{}] Devices telemetry messages is empty for: [{}]", sessionId, gateway.getDeviceId()); |
|||
throw new IllegalArgumentException("[" + sessionId + "] Devices telemetry messages is empty for [" + gateway.getDeviceId() + "]"); |
|||
} |
|||
} catch (RuntimeException | InvalidProtocolBufferException e) { |
|||
throw new AdaptorException(e); |
|||
} |
|||
} |
|||
|
|||
public void processPostTelemetryMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostTelemetryMsg postTelemetryMsg, String deviceName, int msgId) { |
|||
transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg)); |
|||
} |
|||
|
|||
public TransportProtos.PostTelemetryMsg postTelemetryMsgCreated(TransportProtos.KeyValueProto keyValueProto, long ts) { |
|||
List<TransportProtos.KeyValueProto> result = new ArrayList<>(); |
|||
result.add(keyValueProto); |
|||
TransportProtos.PostTelemetryMsg.Builder request = TransportProtos.PostTelemetryMsg.newBuilder(); |
|||
TransportProtos.TsKvListProto.Builder builder = TransportProtos.TsKvListProto.newBuilder(); |
|||
builder.setTs(ts); |
|||
builder.addAllKv(result); |
|||
request.addTsKvList(builder.build()); |
|||
return request.build(); |
|||
} |
|||
|
|||
private void onDeviceClaimJson(int msgId, ByteBuf payload) throws AdaptorException { |
|||
JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); |
|||
if (json.isJsonObject()) { |
|||
JsonObject jsonObj = json.getAsJsonObject(); |
|||
for (Map.Entry<String, JsonElement> deviceEntry : jsonObj.entrySet()) { |
|||
String deviceName = deviceEntry.getKey(); |
|||
Futures.addCallback(checkDeviceConnected(deviceName), |
|||
new FutureCallback<>() { |
|||
@Override |
|||
public void onSuccess(@Nullable T deviceCtx) { |
|||
if (!deviceEntry.getValue().isJsonObject()) { |
|||
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); |
|||
} |
|||
try { |
|||
DeviceId deviceId = deviceCtx.getDeviceId(); |
|||
TransportProtos.ClaimDeviceMsg claimDeviceMsg = JsonConverter.convertToClaimDeviceProto(deviceId, deviceEntry.getValue()); |
|||
processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); |
|||
} catch (Throwable e) { |
|||
log.warn("[{}][{}] Failed to convert claim message: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(Throwable t) { |
|||
log.debug("[{}] Failed to process device claiming command: {}", sessionId, deviceName, t); |
|||
} |
|||
}, context.getExecutor()); |
|||
} |
|||
} else { |
|||
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); |
|||
} |
|||
} |
|||
|
|||
private void onDeviceClaimProto(int msgId, ByteBuf payload) throws AdaptorException { |
|||
try { |
|||
TransportApiProtos.GatewayClaimMsg claimMsgProto = TransportApiProtos.GatewayClaimMsg.parseFrom(getBytes(payload)); |
|||
List<TransportApiProtos.ClaimDeviceMsg> claimMsgList = claimMsgProto.getMsgList(); |
|||
if (!CollectionUtils.isEmpty(claimMsgList)) { |
|||
claimMsgList.forEach(claimDeviceMsg -> { |
|||
String deviceName = checkDeviceName(claimDeviceMsg.getDeviceName()); |
|||
Futures.addCallback(checkDeviceConnected(deviceName), |
|||
new FutureCallback<>() { |
|||
@Override |
|||
public void onSuccess(@Nullable T deviceCtx) { |
|||
TransportApiProtos.ClaimDevice claimRequest = claimDeviceMsg.getClaimRequest(); |
|||
if (claimRequest == null) { |
|||
throw new IllegalArgumentException("Claim request for device: " + deviceName + " is null!"); |
|||
} |
|||
try { |
|||
DeviceId deviceId = deviceCtx.getDeviceId(); |
|||
TransportProtos.ClaimDeviceMsg claimDeviceMsg = ProtoConverter.convertToClaimDeviceProto(deviceId, claimRequest.toByteArray()); |
|||
processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); |
|||
} catch (Throwable e) { |
|||
log.warn("[{}][{}] Failed to convert claim message: {}", gateway.getDeviceId(), deviceName, claimRequest, e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(Throwable t) { |
|||
log.debug("[{}] Failed to process device claiming command: {}", sessionId, deviceName, t); |
|||
} |
|||
}, context.getExecutor()); |
|||
}); |
|||
} else { |
|||
log.debug("[{}] Devices claim messages is empty for: [{}]", sessionId, gateway.getDeviceId()); |
|||
throw new IllegalArgumentException("[" + sessionId + "] Devices claim messages is empty for [" + gateway.getDeviceId() + "]"); |
|||
} |
|||
} catch (RuntimeException | InvalidProtocolBufferException e) { |
|||
throw new AdaptorException(e); |
|||
} |
|||
} |
|||
|
|||
private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.ClaimDeviceMsg claimDeviceMsg, String deviceName, int msgId) { |
|||
transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); |
|||
} |
|||
|
|||
private void onDeviceAttributesJson(int msgId, ByteBuf payload) throws AdaptorException { |
|||
JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); |
|||
if (json.isJsonObject()) { |
|||
JsonObject jsonObj = json.getAsJsonObject(); |
|||
for (Map.Entry<String, JsonElement> deviceEntry : jsonObj.entrySet()) { |
|||
String deviceName = deviceEntry.getKey(); |
|||
Futures.addCallback(checkDeviceConnected(deviceName), |
|||
new FutureCallback<>() { |
|||
@Override |
|||
public void onSuccess(@Nullable T deviceCtx) { |
|||
if (!deviceEntry.getValue().isJsonObject()) { |
|||
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); |
|||
} |
|||
TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(deviceEntry.getValue().getAsJsonObject()); |
|||
processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(Throwable t) { |
|||
log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); |
|||
} |
|||
}, context.getExecutor()); |
|||
} |
|||
} else { |
|||
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); |
|||
} |
|||
} |
|||
|
|||
private void onDeviceAttributesProto(int msgId, ByteBuf payload) throws AdaptorException { |
|||
try { |
|||
TransportApiProtos.GatewayAttributesMsg attributesMsgProto = TransportApiProtos.GatewayAttributesMsg.parseFrom(getBytes(payload)); |
|||
List<TransportApiProtos.AttributesMsg> attributesMsgList = attributesMsgProto.getMsgList(); |
|||
if (!CollectionUtils.isEmpty(attributesMsgList)) { |
|||
attributesMsgList.forEach(attributesMsg -> { |
|||
String deviceName = checkDeviceName(attributesMsg.getDeviceName()); |
|||
Futures.addCallback(checkDeviceConnected(deviceName), |
|||
new FutureCallback<>() { |
|||
@Override |
|||
public void onSuccess(@Nullable T deviceCtx) { |
|||
TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg(); |
|||
if (kvListProto == null) { |
|||
throw new IllegalArgumentException("Attributes List for device: " + deviceName + " is empty!"); |
|||
} |
|||
try { |
|||
TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto.toByteArray()); |
|||
processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); |
|||
} catch (Throwable e) { |
|||
log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(Throwable t) { |
|||
log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); |
|||
} |
|||
}, context.getExecutor()); |
|||
}); |
|||
} else { |
|||
log.debug("[{}] Devices attributes keys list is empty for: [{}]", sessionId, gateway.getDeviceId()); |
|||
throw new IllegalArgumentException("[" + sessionId + "] Devices attributes keys list is empty for [" + gateway.getDeviceId() + "]"); |
|||
} |
|||
} catch (RuntimeException | InvalidProtocolBufferException e) { |
|||
throw new AdaptorException(e); |
|||
} |
|||
} |
|||
|
|||
protected void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostAttributeMsg postAttributeMsg, String deviceName, int msgId) { |
|||
transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); |
|||
} |
|||
|
|||
private void onDeviceAttributesRequestJson(MqttPublishMessage msg) throws AdaptorException { |
|||
JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, msg.payload()); |
|||
if (json.isJsonObject()) { |
|||
JsonObject jsonObj = json.getAsJsonObject(); |
|||
int requestId = jsonObj.get("id").getAsInt(); |
|||
String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); |
|||
boolean clientScope = jsonObj.get("client").getAsBoolean(); |
|||
Set<String> keys; |
|||
if (jsonObj.has("key")) { |
|||
keys = Collections.singleton(jsonObj.get("key").getAsString()); |
|||
} else { |
|||
JsonArray keysArray = jsonObj.get("keys").getAsJsonArray(); |
|||
keys = new HashSet<>(); |
|||
for (JsonElement keyObj : keysArray) { |
|||
keys.add(keyObj.getAsString()); |
|||
} |
|||
} |
|||
TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); |
|||
processGetAttributeRequestMessage(msg, deviceName, requestMsg); |
|||
} else { |
|||
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); |
|||
} |
|||
} |
|||
|
|||
private void onDeviceAttributesRequestProto(MqttPublishMessage mqttMsg) throws AdaptorException { |
|||
try { |
|||
TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = TransportApiProtos.GatewayAttributesRequestMsg.parseFrom(getBytes(mqttMsg.payload())); |
|||
String deviceName = checkDeviceName(gatewayAttributesRequestMsg.getDeviceName()); |
|||
int requestId = gatewayAttributesRequestMsg.getId(); |
|||
boolean clientScope = gatewayAttributesRequestMsg.getClient(); |
|||
ProtocolStringList keysList = gatewayAttributesRequestMsg.getKeysList(); |
|||
Set<String> keys = new HashSet<>(keysList); |
|||
TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); |
|||
processGetAttributeRequestMessage(mqttMsg, deviceName, requestMsg); |
|||
} catch (RuntimeException | InvalidProtocolBufferException e) { |
|||
throw new AdaptorException(e); |
|||
} |
|||
} |
|||
|
|||
private void onDeviceRpcResponseJson(int msgId, ByteBuf payload) throws AdaptorException { |
|||
JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); |
|||
if (json.isJsonObject()) { |
|||
JsonObject jsonObj = json.getAsJsonObject(); |
|||
String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); |
|||
Futures.addCallback(checkDeviceConnected(deviceName), |
|||
new FutureCallback<>() { |
|||
@Override |
|||
public void onSuccess(@Nullable T deviceCtx) { |
|||
Integer requestId = jsonObj.get("id").getAsInt(); |
|||
String data = jsonObj.get("data").toString(); |
|||
TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() |
|||
.setRequestId(requestId).setPayload(data).build(); |
|||
processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(Throwable t) { |
|||
log.debug("[{}] Failed to process device Rpc response command: {}", sessionId, deviceName, t); |
|||
} |
|||
}, context.getExecutor()); |
|||
} else { |
|||
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); |
|||
} |
|||
} |
|||
|
|||
private void onDeviceRpcResponseProto(int msgId, ByteBuf payload) throws AdaptorException { |
|||
try { |
|||
TransportApiProtos.GatewayRpcResponseMsg gatewayRpcResponseMsg = TransportApiProtos.GatewayRpcResponseMsg.parseFrom(getBytes(payload)); |
|||
String deviceName = checkDeviceName(gatewayRpcResponseMsg.getDeviceName()); |
|||
Futures.addCallback(checkDeviceConnected(deviceName), |
|||
new FutureCallback<>() { |
|||
@Override |
|||
public void onSuccess(@Nullable T deviceCtx) { |
|||
Integer requestId = gatewayRpcResponseMsg.getId(); |
|||
String data = gatewayRpcResponseMsg.getData(); |
|||
TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() |
|||
.setRequestId(requestId).setPayload(data).build(); |
|||
processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(Throwable t) { |
|||
log.debug("[{}] Failed to process device Rpc response command: {}", sessionId, deviceName, t); |
|||
} |
|||
}, context.getExecutor()); |
|||
} catch (RuntimeException | InvalidProtocolBufferException e) { |
|||
throw new AdaptorException(e); |
|||
} |
|||
} |
|||
|
|||
private void processRpcResponseMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg, String deviceName, int msgId) { |
|||
transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(channel, deviceName, msgId, rpcResponseMsg)); |
|||
} |
|||
|
|||
private void processGetAttributeRequestMessage(MqttPublishMessage mqttMsg, String deviceName, TransportProtos.GetAttributeRequestMsg requestMsg) { |
|||
int msgId = getMsgId(mqttMsg); |
|||
Futures.addCallback(checkDeviceConnected(deviceName), |
|||
new FutureCallback<>() { |
|||
@Override |
|||
public void onSuccess(@Nullable T deviceCtx) { |
|||
transportService.process(deviceCtx.getSessionInfo(), requestMsg, getPubAckCallback(channel, deviceName, msgId, requestMsg)); |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(Throwable t) { |
|||
ack(mqttMsg, ReturnCode.IMPLEMENTATION_SPECIFIC); |
|||
log.debug("[{}] Failed to process device attributes request command: {}", sessionId, deviceName, t); |
|||
} |
|||
}, context.getExecutor()); |
|||
} |
|||
|
|||
private TransportProtos.GetAttributeRequestMsg toGetAttributeRequestMsg(int requestId, boolean clientScope, Set<String> keys) { |
|||
TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); |
|||
result.setRequestId(requestId); |
|||
|
|||
if (clientScope) { |
|||
result.addAllClientAttributeNames(keys); |
|||
} else { |
|||
result.addAllSharedAttributeNames(keys); |
|||
} |
|||
return result.build(); |
|||
} |
|||
|
|||
protected ListenableFuture<T> checkDeviceConnected(String deviceName) { |
|||
T ctx = devices.get(deviceName); |
|||
if (ctx == null) { |
|||
log.debug("[{}] Missing device [{}] for the gateway session", sessionId, deviceName); |
|||
return onDeviceConnect(deviceName, DEFAULT_DEVICE_TYPE); |
|||
} else { |
|||
return Futures.immediateFuture(ctx); |
|||
} |
|||
} |
|||
|
|||
protected String checkDeviceName(String deviceName) { |
|||
if (StringUtils.isEmpty(deviceName)) { |
|||
throw new RuntimeException("Device name is empty!"); |
|||
} else { |
|||
return deviceName; |
|||
} |
|||
} |
|||
|
|||
private String getDeviceName(JsonElement json) { |
|||
return json.getAsJsonObject().get(DEVICE_PROPERTY).getAsString(); |
|||
} |
|||
|
|||
private String getDeviceType(JsonElement json) { |
|||
JsonElement type = json.getAsJsonObject().get("type"); |
|||
return type == null || type instanceof JsonNull ? DEFAULT_DEVICE_TYPE : type.getAsString(); |
|||
} |
|||
|
|||
private JsonElement getJson(MqttPublishMessage mqttMsg) throws AdaptorException { |
|||
return JsonMqttAdaptor.validateJsonPayload(sessionId, mqttMsg.payload()); |
|||
} |
|||
|
|||
protected byte[] getBytes(ByteBuf payload) { |
|||
return ProtoMqttAdaptor.toBytes(payload); |
|||
} |
|||
|
|||
protected void ack(MqttPublishMessage msg, ReturnCode returnCode) { |
|||
int msgId = getMsgId(msg); |
|||
if (msgId > 0) { |
|||
writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, returnCode)); |
|||
} |
|||
} |
|||
|
|||
private void deregisterSession(String deviceName, MqttDeviceAwareSessionContext deviceSessionCtx) { |
|||
if (this.deviceSessionCtx.isSparkplug()) { |
|||
sendSparkplugStateOnTelemetry(deviceSessionCtx.getSessionInfo(), |
|||
deviceSessionCtx.getDeviceInfo().getDeviceName(), OFFLINE, new Date().getTime()); |
|||
} |
|||
transportService.deregisterSession(deviceSessionCtx.getSessionInfo()); |
|||
transportService.process(deviceSessionCtx.getSessionInfo(), SESSION_EVENT_MSG_CLOSED, null); |
|||
log.debug("[{}] Removed device [{}] from the gateway session", sessionId, deviceName); |
|||
} |
|||
|
|||
public void sendSparkplugStateOnTelemetry(TransportProtos.SessionInfoProto sessionInfo, String deviceName, SparkplugConnectionState connectionState, long ts) { |
|||
TransportProtos.KeyValueProto.Builder keyValueProtoBuilder = TransportProtos.KeyValueProto.newBuilder(); |
|||
keyValueProtoBuilder.setKey(messageName(STATE)); |
|||
keyValueProtoBuilder.setType(TransportProtos.KeyValueType.STRING_V); |
|||
keyValueProtoBuilder.setStringV(connectionState.name()); |
|||
TransportProtos.PostTelemetryMsg postTelemetryMsg = postTelemetryMsgCreated(keyValueProtoBuilder.build(), ts); |
|||
transportService.process(sessionInfo, postTelemetryMsg, getPubAckCallback(channel, deviceName, -1, postTelemetryMsg)); |
|||
} |
|||
|
|||
private <T> TransportServiceCallback<Void> getPubAckCallback(final ChannelHandlerContext ctx, final String deviceName, final int msgId, final T msg) { |
|||
return new TransportServiceCallback<Void>() { |
|||
@Override |
|||
public void onSuccess(Void dummy) { |
|||
log.trace("[{}][{}] Published msg: {}", sessionId, deviceName, msg); |
|||
if (msgId > 0) { |
|||
ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, ReturnCode.SUCCESS)); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onError(Throwable e) { |
|||
log.trace("[{}] Failed to publish msg: {} for device: {}", sessionId, msg, deviceName, e); |
|||
ctx.close(); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.session; |
|||
|
|||
import org.thingsboard.server.common.data.DeviceProfile; |
|||
import org.thingsboard.server.common.transport.TransportService; |
|||
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; |
|||
|
|||
import java.util.concurrent.ConcurrentMap; |
|||
|
|||
/** |
|||
* Created by nickAS21 on 26.12.22 |
|||
*/ |
|||
public class GatewayDeviceSessionContext extends AbstractGatewayDeviceSessionContext<GatewaySessionHandler> { |
|||
|
|||
public GatewayDeviceSessionContext(GatewaySessionHandler parent, |
|||
TransportDeviceInfo deviceInfo, |
|||
DeviceProfile deviceProfile, |
|||
ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap, |
|||
TransportService transportService) { |
|||
super(parent, deviceInfo, deviceProfile, mqttQoSMap, transportService); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,106 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.session; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.DeviceProfile; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.transport.TransportService; |
|||
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; |
|||
import org.thingsboard.server.gen.transport.TransportProtos; |
|||
import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; |
|||
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; |
|||
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugRpcRequestHeader; |
|||
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic; |
|||
|
|||
import java.util.Date; |
|||
import java.util.Map; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.concurrent.ConcurrentMap; |
|||
import java.util.stream.Collectors; |
|||
|
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.getTsKvProto; |
|||
|
|||
@Slf4j |
|||
public class SparkplugDeviceSessionContext extends AbstractGatewayDeviceSessionContext<SparkplugNodeSessionHandler> { |
|||
|
|||
private final Map<String, SparkplugBProto.Payload.Metric> deviceBirthMetrics = new ConcurrentHashMap<>(); |
|||
|
|||
public SparkplugDeviceSessionContext(SparkplugNodeSessionHandler parent, |
|||
TransportDeviceInfo deviceInfo, |
|||
DeviceProfile deviceProfile, |
|||
ConcurrentMap<MqttTopicMatcher, |
|||
Integer> mqttQoSMap, |
|||
TransportService transportService) { |
|||
super(parent, deviceInfo, deviceProfile, mqttQoSMap, transportService); |
|||
} |
|||
|
|||
public Map<String, SparkplugBProto.Payload.Metric> getDeviceBirthMetrics() { |
|||
return deviceBirthMetrics; |
|||
} |
|||
|
|||
public void setDeviceBirthMetrics(java.util.List<org.thingsboard.server.gen.transport.mqtt.SparkplugBProto.Payload.Metric> metrics) { |
|||
this.deviceBirthMetrics.putAll(metrics.stream() |
|||
.collect(Collectors.toMap(SparkplugBProto.Payload.Metric::getName, metric -> metric))); |
|||
} |
|||
|
|||
|
|||
@Override |
|||
public void onAttributeUpdate(UUID sessionId, TransportProtos.AttributeUpdateNotificationMsg notification) { |
|||
log.trace("[{}] Received attributes update notification to sparkplug device", sessionId); |
|||
notification.getSharedUpdatedList().forEach(tsKvProto -> { |
|||
if (getDeviceBirthMetrics().containsKey(tsKvProto.getKv().getKey())) { |
|||
SparkplugTopic sparkplugTopic = new SparkplugTopic(parent.getSparkplugTopicNode(), |
|||
SparkplugMessageType.DCMD, deviceInfo.getDeviceName()); |
|||
parent.createSparkplugMqttPublishMsg(tsKvProto, |
|||
sparkplugTopic.toString(), |
|||
getDeviceBirthMetrics().get(tsKvProto.getKv().getKey())) |
|||
.ifPresent(this.parent::writeAndFlush); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
@Override |
|||
public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { |
|||
log.trace("[{}] Received RPC Request notification to sparkplug device", sessionId); |
|||
try { |
|||
SparkplugMessageType messageType = SparkplugMessageType.parseMessageType(rpcRequest.getMethodName()); |
|||
SparkplugRpcRequestHeader header = JacksonUtil.fromString(rpcRequest.getParams(), SparkplugRpcRequestHeader.class); |
|||
header.setMessageType(messageType.name()); |
|||
TransportProtos.TsKvProto tsKvProto = getTsKvProto(header.getMetricName(), header.getValue(), new Date().getTime()); |
|||
if (getDeviceBirthMetrics().containsKey(tsKvProto.getKv().getKey())) { |
|||
SparkplugTopic sparkplugTopic = new SparkplugTopic(parent.getSparkplugTopicNode(), |
|||
messageType, deviceInfo.getDeviceName()); |
|||
parent.createSparkplugMqttPublishMsg(tsKvProto, |
|||
sparkplugTopic.toString(), |
|||
getDeviceBirthMetrics().get(tsKvProto.getKv().getKey())) |
|||
.ifPresent(payload -> parent.sendToDeviceRpcRequest(payload, rpcRequest, sessionInfo)); |
|||
} else { |
|||
parent.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), |
|||
ThingsboardErrorCode.BAD_REQUEST_PARAMS, " Failed send To Device Rpc Request: " + |
|||
rpcRequest.getMethodName() + ". This device does not have a metricName: [" + tsKvProto.getKv().getKey() + "]"); |
|||
} |
|||
} catch (ThingsboardException e) { |
|||
parent.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), |
|||
ThingsboardErrorCode.BAD_REQUEST_PARAMS, " Failed send To Device Rpc Request: " + |
|||
rpcRequest.getMethodName() + ". " + e.getMessage()); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,340 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.session; |
|||
|
|||
import com.google.common.util.concurrent.FutureCallback; |
|||
import com.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import com.google.common.util.concurrent.MoreExecutors; |
|||
import com.google.gson.JsonParser; |
|||
import com.google.gson.JsonSyntaxException; |
|||
import com.google.protobuf.Descriptors; |
|||
import io.netty.handler.codec.mqtt.MqttMessage; |
|||
import io.netty.handler.codec.mqtt.MqttPublishMessage; |
|||
import io.netty.handler.codec.mqtt.MqttQoS; |
|||
import io.netty.handler.codec.mqtt.MqttTopicSubscription; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.eclipse.leshan.core.ResponseCode; |
|||
import org.springframework.util.CollectionUtils; |
|||
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.transport.adaptor.AdaptorException; |
|||
import org.thingsboard.server.common.transport.adaptor.JsonConverter; |
|||
import org.thingsboard.server.common.transport.adaptor.ProtoConverter; |
|||
import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; |
|||
import org.thingsboard.server.gen.transport.TransportApiProtos; |
|||
import org.thingsboard.server.gen.transport.TransportProtos; |
|||
import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; |
|||
import org.thingsboard.server.transport.mqtt.MqttTransportHandler; |
|||
import org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType; |
|||
import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic; |
|||
|
|||
import javax.annotation.Nullable; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Optional; |
|||
import java.util.Set; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.concurrent.ExecutionException; |
|||
import java.util.stream.Collectors; |
|||
|
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DBIRTH; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NBIRTH; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.ONLINE; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.createMetric; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.fromSparkplugBMetricToKeyValueProto; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.validatedValueByTypeMetric; |
|||
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.parseTopicSubscribe; |
|||
|
|||
/** |
|||
* Created by nickAS21 on 12.12.22 |
|||
*/ |
|||
@Slf4j |
|||
public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<SparkplugDeviceSessionContext> { |
|||
|
|||
private final SparkplugTopic sparkplugTopicNode; |
|||
private final Map<String, SparkplugBProto.Payload.Metric> nodeBirthMetrics; |
|||
private final MqttTransportHandler parent; |
|||
|
|||
public SparkplugNodeSessionHandler(MqttTransportHandler parent, DeviceSessionCtx deviceSessionCtx, UUID sessionId, |
|||
SparkplugTopic sparkplugTopicNode) { |
|||
super(deviceSessionCtx, sessionId); |
|||
this.parent = parent; |
|||
this.sparkplugTopicNode = sparkplugTopicNode; |
|||
this.nodeBirthMetrics = new ConcurrentHashMap<>(); |
|||
} |
|||
|
|||
public void setNodeBirthMetrics(java.util.List<org.thingsboard.server.gen.transport.mqtt.SparkplugBProto.Payload.Metric> metrics) { |
|||
this.nodeBirthMetrics.putAll(metrics.stream() |
|||
.collect(Collectors.toMap(SparkplugBProto.Payload.Metric::getName, metric -> metric))); |
|||
} |
|||
|
|||
public Map<String, SparkplugBProto.Payload.Metric> getNodeBirthMetrics() { |
|||
return this.nodeBirthMetrics; |
|||
} |
|||
|
|||
public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { |
|||
DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; |
|||
byte[] bytes = getBytes(inbound.payload()); |
|||
Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor()); |
|||
try { |
|||
return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor))); |
|||
} catch (Exception e) { |
|||
log.debug("Failed to decode post telemetry request", e); |
|||
throw new AdaptorException(e); |
|||
} |
|||
} |
|||
|
|||
public void onAttributesTelemetryProto(int msgId, SparkplugBProto.Payload sparkplugBProto, String deviceName, SparkplugTopic topic) throws AdaptorException, ThingsboardException { |
|||
checkDeviceName(deviceName); |
|||
|
|||
ListenableFuture<MqttDeviceAwareSessionContext> contextListenableFuture; |
|||
if (topic.isNode()) { |
|||
if (topic.isType(NBIRTH)) { |
|||
sendSparkplugStateOnTelemetry(this.deviceSessionCtx.getSessionInfo(), deviceName, ONLINE, |
|||
sparkplugBProto.getTimestamp()); |
|||
setNodeBirthMetrics(sparkplugBProto.getMetricsList()); |
|||
} |
|||
contextListenableFuture = Futures.immediateFuture(this.deviceSessionCtx); |
|||
} else { |
|||
ListenableFuture<SparkplugDeviceSessionContext> deviceCtx = onDeviceConnectProto(deviceName); |
|||
contextListenableFuture = Futures.transform(deviceCtx, ctx -> { |
|||
if (topic.isType(DBIRTH)) { |
|||
sendSparkplugStateOnTelemetry(ctx.getSessionInfo(), deviceName, ONLINE, |
|||
sparkplugBProto.getTimestamp()); |
|||
ctx.setDeviceBirthMetrics(sparkplugBProto.getMetricsList()); |
|||
} |
|||
return ctx; |
|||
}, MoreExecutors.directExecutor()); |
|||
} |
|||
Set<String> attributesMetricNames = ((MqttDeviceProfileTransportConfiguration) deviceSessionCtx |
|||
.getDeviceProfile().getProfileData().getTransportConfiguration()).getSparkplugAttributesMetricNames(); |
|||
if (attributesMetricNames != null) { |
|||
List<TransportApiProtos.AttributesMsg> attributesMsgList = convertToPostAttributes(sparkplugBProto, attributesMetricNames, deviceName); |
|||
onDeviceAttributesProto(contextListenableFuture, msgId, attributesMsgList, deviceName); |
|||
} |
|||
List<TransportProtos.PostTelemetryMsg> postTelemetryMsgList = convertToPostTelemetry(sparkplugBProto, attributesMetricNames, topic.getType().name()); |
|||
onDeviceTelemetryProto(contextListenableFuture, msgId, postTelemetryMsgList, deviceName); |
|||
} |
|||
|
|||
public void onDeviceTelemetryProto(ListenableFuture<MqttDeviceAwareSessionContext> contextListenableFuture, |
|||
int msgId, List<TransportProtos.PostTelemetryMsg> postTelemetryMsgList, String deviceName) throws AdaptorException { |
|||
try { |
|||
int finalMsgId = msgId; |
|||
postTelemetryMsgList.forEach(telemetryMsg -> { |
|||
Futures.addCallback(contextListenableFuture, |
|||
new FutureCallback<>() { |
|||
@Override |
|||
public void onSuccess(@Nullable MqttDeviceAwareSessionContext deviceCtx) { |
|||
try { |
|||
processPostTelemetryMsg(deviceCtx, telemetryMsg, deviceName, finalMsgId); |
|||
} catch (Throwable e) { |
|||
log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, telemetryMsg, e); |
|||
channel.close(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(Throwable t) { |
|||
log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); |
|||
} |
|||
}, context.getExecutor()); |
|||
}); |
|||
} catch (RuntimeException e) { |
|||
throw new AdaptorException(e); |
|||
} |
|||
} |
|||
|
|||
private void onDeviceAttributesProto(ListenableFuture<MqttDeviceAwareSessionContext> contextListenableFuture, int msgId, |
|||
List<TransportApiProtos.AttributesMsg> attributesMsgList, String deviceName) throws AdaptorException { |
|||
try { |
|||
if (!CollectionUtils.isEmpty(attributesMsgList)) { |
|||
attributesMsgList.forEach(attributesMsg -> { |
|||
Futures.addCallback(contextListenableFuture, |
|||
new FutureCallback<>() { |
|||
@Override |
|||
public void onSuccess(@Nullable MqttDeviceAwareSessionContext deviceCtx) { |
|||
TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg(); |
|||
try { |
|||
TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto.toByteArray()); |
|||
processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); |
|||
} catch (Throwable e) { |
|||
log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(Throwable t) { |
|||
log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); |
|||
} |
|||
}, context.getExecutor()); |
|||
}); |
|||
} else { |
|||
log.debug("[{}] Devices attributes keys list is empty for: [{}]", sessionId, gateway.getDeviceId()); |
|||
} |
|||
} catch (RuntimeException e) { |
|||
throw new AdaptorException(e); |
|||
} |
|||
} |
|||
|
|||
public void handleSparkplugSubscribeMsg(List<Integer> grantedQoSList, MqttTopicSubscription subscription, |
|||
MqttQoS reqQoS) throws ThingsboardException, AdaptorException, |
|||
ExecutionException, InterruptedException { |
|||
SparkplugTopic sparkplugTopic = parseTopicSubscribe(subscription.topicName()); |
|||
if (sparkplugTopic.getGroupId() == null) { |
|||
// TODO SUBSCRIBE NameSpace
|
|||
} else if (sparkplugTopic.getType() == null) { |
|||
// TODO SUBSCRIBE GroupId
|
|||
} else if (sparkplugTopic.isNode()) { |
|||
// SUBSCRIBE Node
|
|||
parent.processAttributesRpcSubscribeSparkplugNode(grantedQoSList, reqQoS); |
|||
} else { |
|||
// SUBSCRIBE Device - DO NOTHING, WE HAVE ALREADY SUBSCRIBED.
|
|||
// TODO: track that node subscribed to # or to particular device.
|
|||
} |
|||
} |
|||
|
|||
public void onDeviceDisconnect(MqttPublishMessage mqttMsg, String deviceName) throws AdaptorException { |
|||
try { |
|||
processOnDisconnect(mqttMsg, deviceName); |
|||
} catch (RuntimeException e) { |
|||
throw new AdaptorException(e); |
|||
} |
|||
} |
|||
|
|||
private ListenableFuture<SparkplugDeviceSessionContext> onDeviceConnectProto(String deviceName) throws ThingsboardException { |
|||
try { |
|||
String deviceType = this.gateway.getDeviceType() + "-node"; |
|||
return onDeviceConnect(deviceName, deviceType); |
|||
} catch (RuntimeException e) { |
|||
log.error("Failed Sparkplug Device connect proto!", e); |
|||
throw new ThingsboardException(e, ThingsboardErrorCode.BAD_REQUEST_PARAMS); |
|||
} |
|||
} |
|||
|
|||
private List<TransportProtos.PostTelemetryMsg> convertToPostTelemetry(SparkplugBProto.Payload sparkplugBProto, Set<String> attributesMetricNames, String topicTypeName) throws AdaptorException { |
|||
try { |
|||
List<TransportProtos.PostTelemetryMsg> msgs = new ArrayList<>(); |
|||
for (SparkplugBProto.Payload.Metric protoMetric : sparkplugBProto.getMetricsList()) { |
|||
if (attributesMetricNames == null || !attributesMetricNames.contains(protoMetric.getName())) { |
|||
long ts = protoMetric.getTimestamp(); |
|||
String key = "bdSeq".equals(protoMetric.getName()) ? |
|||
topicTypeName + " " + protoMetric.getName() : protoMetric.getName(); |
|||
Optional<TransportProtos.KeyValueProto> keyValueProtoOpt = fromSparkplugBMetricToKeyValueProto(key, protoMetric); |
|||
if (keyValueProtoOpt.isPresent()) { |
|||
msgs.add(postTelemetryMsgCreated(keyValueProtoOpt.get(), ts)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (DBIRTH.name().equals(topicTypeName)) { |
|||
TransportProtos.KeyValueProto.Builder keyValueProtoBuilder = TransportProtos.KeyValueProto.newBuilder(); |
|||
keyValueProtoBuilder.setKey(topicTypeName + " " + "seq"); |
|||
keyValueProtoBuilder.setType(TransportProtos.KeyValueType.LONG_V); |
|||
keyValueProtoBuilder.setLongV(sparkplugBProto.getSeq()); |
|||
msgs.add(postTelemetryMsgCreated(keyValueProtoBuilder.build(), sparkplugBProto.getTimestamp())); |
|||
} |
|||
return msgs; |
|||
} catch (IllegalStateException | JsonSyntaxException | ThingsboardException e) { |
|||
log.error("Failed to decode post telemetry request", e); |
|||
throw new AdaptorException(e); |
|||
} |
|||
} |
|||
|
|||
private List<TransportApiProtos.AttributesMsg> convertToPostAttributes(SparkplugBProto.Payload sparkplugBProto, |
|||
Set<String> attributesMetricNames, |
|||
String deviceName) throws AdaptorException { |
|||
try { |
|||
List<TransportApiProtos.AttributesMsg> msgs = new ArrayList<>(); |
|||
for (SparkplugBProto.Payload.Metric protoMetric : sparkplugBProto.getMetricsList()) { |
|||
if (attributesMetricNames.contains(protoMetric.getName())) { |
|||
TransportApiProtos.AttributesMsg.Builder deviceAttributesMsgBuilder = TransportApiProtos.AttributesMsg.newBuilder(); |
|||
Optional<TransportProtos.PostAttributeMsg> msgOpt = getPostAttributeMsg(protoMetric); |
|||
if (msgOpt.isPresent()) { |
|||
deviceAttributesMsgBuilder.setDeviceName(deviceName); |
|||
deviceAttributesMsgBuilder.setMsg(msgOpt.get()); |
|||
msgs.add(deviceAttributesMsgBuilder.build()); |
|||
} |
|||
} |
|||
} |
|||
return msgs; |
|||
} catch (IllegalStateException | JsonSyntaxException | ThingsboardException e) { |
|||
log.error("Failed to decode post telemetry request", e); |
|||
throw new AdaptorException(e); |
|||
} |
|||
} |
|||
|
|||
private Optional<TransportProtos.PostAttributeMsg> getPostAttributeMsg(SparkplugBProto.Payload.Metric protoMetric) throws ThingsboardException { |
|||
Optional<TransportProtos.KeyValueProto> keyValueProtoOpt = fromSparkplugBMetricToKeyValueProto(protoMetric.getName(), protoMetric); |
|||
if (keyValueProtoOpt.isPresent()) { |
|||
TransportProtos.PostAttributeMsg.Builder builder = TransportProtos.PostAttributeMsg.newBuilder(); |
|||
builder.addKv(keyValueProtoOpt.get()); |
|||
return Optional.of(builder.build()); |
|||
} |
|||
return Optional.empty(); |
|||
} |
|||
|
|||
public SparkplugTopic getSparkplugTopicNode() { |
|||
return this.sparkplugTopicNode; |
|||
} |
|||
|
|||
public Optional<MqttPublishMessage> createSparkplugMqttPublishMsg(TransportProtos.TsKvProto tsKvProto, |
|||
String sparkplugTopic, |
|||
SparkplugBProto.Payload.Metric metricBirth) { |
|||
try { |
|||
long ts = tsKvProto.getTs(); |
|||
MetricDataType metricDataType = MetricDataType.fromInteger(metricBirth.getDatatype()); |
|||
Optional value = validatedValueByTypeMetric(tsKvProto.getKv(), metricDataType); |
|||
if (value.isPresent()) { |
|||
SparkplugBProto.Payload.Builder cmdPayload = SparkplugBProto.Payload.newBuilder() |
|||
.setTimestamp(ts); |
|||
cmdPayload.addMetrics(createMetric(value.get(), ts, tsKvProto.getKv().getKey(), metricDataType)); |
|||
byte[] payloadInBytes = cmdPayload.build().toByteArray(); |
|||
return Optional.of(getPayloadAdaptor().createMqttPublishMsg(deviceSessionCtx, sparkplugTopic, payloadInBytes)); |
|||
} else { |
|||
log.trace("DeviceId: [{}] tenantId: [{}] sessionId:[{}] Failed to convert device attributes [{}] response to MQTT sparkplug msg", |
|||
deviceSessionCtx.getDeviceInfo().getDeviceId(), deviceSessionCtx.getDeviceInfo().getTenantId(), sessionId, tsKvProto.getKv()); |
|||
} |
|||
} catch (Exception e) { |
|||
log.trace("DeviceId: [{}] tenantId: [{}] sessionId:[{}] Failed to convert device attributes response to MQTT sparkplug msg", |
|||
deviceSessionCtx.getDeviceInfo().getDeviceId(), deviceSessionCtx.getDeviceInfo().getTenantId(), sessionId, e); |
|||
return Optional.empty(); |
|||
} |
|||
return Optional.empty(); |
|||
} |
|||
|
|||
@Override |
|||
protected SparkplugDeviceSessionContext newDeviceSessionCtx(GetOrCreateDeviceFromGatewayResponse msg) { |
|||
return new SparkplugDeviceSessionContext(this, msg.getDeviceInfo(), msg.getDeviceProfile(), mqttQoSMap, transportService); |
|||
} |
|||
|
|||
protected void sendToDeviceRpcRequest(MqttMessage payload, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, TransportProtos.SessionInfoProto sessionInfo) { |
|||
parent.sendToDeviceRpcRequest(payload, rpcRequest, sessionInfo); |
|||
} |
|||
|
|||
protected void sendErrorRpcResponse(TransportProtos.SessionInfoProto sessionInfo, int requestId, ThingsboardErrorCode result, String errorMsg) { |
|||
parent.sendErrorRpcResponse(sessionInfo, requestId, result, errorMsg); |
|||
} |
|||
|
|||
protected void sendSuccessRpcResponse(TransportProtos.SessionInfoProto sessionInfo, int requestId, ResponseCode result, String successMsg) { |
|||
parent.sendSuccessRpcResponse(sessionInfo, requestId, result, successMsg); |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,156 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.util.sparkplug; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.server.common.transport.adaptor.AdaptorException; |
|||
import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; |
|||
|
|||
import java.math.BigInteger; |
|||
import java.util.Date; |
|||
|
|||
/** |
|||
* Created by nickAS21 on 10.01.23 |
|||
*/ |
|||
|
|||
@Slf4j |
|||
public enum MetricDataType { |
|||
|
|||
// Basic Types
|
|||
Int8(1, Byte.class), |
|||
Int16(2, Short.class), |
|||
Int32(3, Integer.class), |
|||
Int64(4, Long.class), |
|||
UInt8(5, Short.class), |
|||
UInt16(6, Integer.class), |
|||
UInt32(7, Long.class), |
|||
UInt64(8, BigInteger.class), |
|||
Float(9, Float.class), |
|||
Double(10, Double.class), |
|||
Boolean(11, Boolean.class), |
|||
String(12, String.class), |
|||
DateTime(13, Date.class), |
|||
Text(14, String.class), |
|||
|
|||
// Custom Types for Metrics
|
|||
UUID(15, String.class), |
|||
DataSet(16, SparkplugBProto.Payload.DataSet.class), |
|||
Bytes(17, byte[].class), |
|||
File(18, SparkplugMetricUtil.File.class), |
|||
Template(19, SparkplugBProto.Payload.Template.class), |
|||
|
|||
// PropertyValue Types (20 and 21) are NOT metric datatypes
|
|||
|
|||
// Unknown
|
|||
Unknown(0, Object.class); |
|||
|
|||
private Class<?> clazz = null; |
|||
private int intValue = 0; |
|||
|
|||
/** |
|||
* Constructor |
|||
* |
|||
* @param intValue the integer value of this {@link MetricDataType} |
|||
* @param clazz the {@link Class} type associated with this {@link MetricDataType} |
|||
*/ |
|||
private MetricDataType(int intValue, Class<?> clazz) { |
|||
this.intValue = intValue; |
|||
this.clazz = clazz; |
|||
} |
|||
|
|||
/** |
|||
* Checks the type of a specified value against the specified {@link MetricDataType} |
|||
* |
|||
* @param value the {@link Object} value to check against the {@link MetricDataType} |
|||
* @throws AdaptorException if the value is not a valid type for the given {@link MetricDataType} |
|||
*/ |
|||
public void checkType(Object value) throws AdaptorException { |
|||
if (value != null && !clazz.isAssignableFrom(value.getClass())) { |
|||
String msgError = "Failed type check - " + clazz + " != " + ((value != null) ? value.getClass().toString() : "null"); |
|||
log.debug(msgError); |
|||
throw new AdaptorException(msgError); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Returns an integer representation of the data type. |
|||
* |
|||
* @return an integer representation of the data type. |
|||
*/ |
|||
public int toIntValue() { |
|||
return this.intValue; |
|||
} |
|||
|
|||
/** |
|||
* Converts the integer representation of the data type into a {@link MetricDataType} instance. |
|||
* |
|||
* @param i the integer representation of the data type. |
|||
* @return a {@link MetricDataType} instance. |
|||
*/ |
|||
public static MetricDataType fromInteger(int i) { |
|||
switch (i) { |
|||
case 1: |
|||
return Int8; |
|||
case 2: |
|||
return Int16; |
|||
case 3: |
|||
return Int32; |
|||
case 4: |
|||
return Int64; |
|||
case 5: |
|||
return UInt8; |
|||
case 6: |
|||
return UInt16; |
|||
case 7: |
|||
return UInt32; |
|||
case 8: |
|||
return UInt64; |
|||
case 9: |
|||
return Float; |
|||
case 10: |
|||
return Double; |
|||
case 11: |
|||
return Boolean; |
|||
case 12: |
|||
return String; |
|||
case 13: |
|||
return DateTime; |
|||
case 14: |
|||
return Text; |
|||
case 15: |
|||
return UUID; |
|||
case 16: |
|||
return DataSet; |
|||
case 17: |
|||
return Bytes; |
|||
case 18: |
|||
return File; |
|||
case 19: |
|||
return Template; |
|||
default: |
|||
return Unknown; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @return the class type for this DataType |
|||
*/ |
|||
public Class<?> getClazz() { |
|||
return clazz; |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.util.sparkplug; |
|||
|
|||
public enum SparkplugConnectionState { |
|||
/** |
|||
* The EoN node should examine the payload of this |
|||
* message to ensure that it is a value of “ONLINE” |
|||
*/ |
|||
OFFLINE, |
|||
/** |
|||
* If the value is “OFFLINE”, this indicates the Primary Application |
|||
* has lost its MQTT Session to this particular MQTT Server. |
|||
*/ |
|||
ONLINE |
|||
|
|||
} |
|||
@ -0,0 +1,113 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.util.sparkplug; |
|||
|
|||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
|
|||
/** |
|||
* An enumeration of Sparkplug MQTT message types. The type provides an indication as to what the MQTT Payload of |
|||
* message will contain. |
|||
*/ |
|||
public enum SparkplugMessageType { |
|||
|
|||
/** |
|||
* Birth certificate for MQTT Edge of Network (EoN) Nodes. |
|||
*/ |
|||
NBIRTH, |
|||
|
|||
/** |
|||
* Death certificate for MQTT Edge of Network (EoN) Nodes. |
|||
*/ |
|||
NDEATH, |
|||
|
|||
/** |
|||
* Birth certificate for MQTT Devices. |
|||
*/ |
|||
DBIRTH, |
|||
|
|||
/** |
|||
* Death certificate for MQTT Devices. |
|||
*/ |
|||
DDEATH, |
|||
|
|||
/** |
|||
* Edge of Network (EoN) Node data message. |
|||
*/ |
|||
NDATA, |
|||
|
|||
/** |
|||
* Device data message. |
|||
*/ |
|||
DDATA, |
|||
|
|||
/** |
|||
* Edge of Network (EoN) Node command message. |
|||
*/ |
|||
NCMD, |
|||
|
|||
/** |
|||
* Device command message. |
|||
*/ |
|||
DCMD, |
|||
|
|||
/** |
|||
* Critical application state message. |
|||
*/ |
|||
STATE, |
|||
|
|||
/** |
|||
* Device record message. |
|||
*/ |
|||
DRECORD, |
|||
|
|||
/** |
|||
* Edge of Network (EoN) Node record message. |
|||
*/ |
|||
NRECORD; |
|||
|
|||
public static SparkplugMessageType parseMessageType(String type) throws ThingsboardException { |
|||
for (SparkplugMessageType messageType : SparkplugMessageType.values()) { |
|||
if (messageType.name().equals(type)) { |
|||
return messageType; |
|||
} |
|||
} |
|||
throw new ThingsboardException("Invalid message type: " + type, ThingsboardErrorCode.INVALID_ARGUMENTS); |
|||
} |
|||
public static String messageName(SparkplugMessageType type) { |
|||
return STATE.equals(type) ? "sparkplugConnectionState" : type.name(); |
|||
} |
|||
|
|||
public boolean isDeath() { |
|||
return this.equals(DDEATH) || this.equals(NDEATH); |
|||
} |
|||
|
|||
public boolean isCommand() { |
|||
return this.equals(DCMD) || this.equals(NCMD); |
|||
} |
|||
|
|||
public boolean isData() { |
|||
return this.equals(DDATA) || this.equals(NDATA); |
|||
} |
|||
|
|||
public boolean isBirth() { |
|||
return this.equals(DBIRTH) || this.equals(NBIRTH); |
|||
} |
|||
|
|||
public boolean isRecord() { |
|||
return this.equals(DRECORD) || this.equals(NRECORD); |
|||
} |
|||
} |
|||
@ -0,0 +1,452 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.util.sparkplug; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
|||
import com.fasterxml.jackson.core.type.TypeReference; |
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import com.fasterxml.jackson.databind.annotation.JsonSerialize; |
|||
import com.fasterxml.jackson.databind.node.ArrayNode; |
|||
import com.fasterxml.jackson.databind.ser.std.FileSerializer; |
|||
import com.google.protobuf.ByteString; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.gen.transport.TransportProtos; |
|||
import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; |
|||
|
|||
import java.math.BigDecimal; |
|||
import java.nio.ByteBuffer; |
|||
import java.text.NumberFormat; |
|||
import java.util.Arrays; |
|||
import java.util.List; |
|||
import java.util.Optional; |
|||
|
|||
import static org.thingsboard.common.util.JacksonUtil.newArrayNode; |
|||
|
|||
/** |
|||
* Provides utility methods for SparkplugB MQTT Payload Metric. |
|||
*/ |
|||
@Slf4j |
|||
public class SparkplugMetricUtil { |
|||
|
|||
public static Optional<TransportProtos.KeyValueProto> fromSparkplugBMetricToKeyValueProto(String key, SparkplugBProto.Payload.Metric protoMetric) throws ThingsboardException { |
|||
// Check if the null flag has been set indicating that the value is null
|
|||
if (protoMetric.getIsNull()) { |
|||
return Optional.empty(); |
|||
} |
|||
// Otherwise convert the value based on the type
|
|||
int metricType = protoMetric.getDatatype(); |
|||
TransportProtos.KeyValueProto.Builder builderProto = TransportProtos.KeyValueProto.newBuilder(); |
|||
ArrayNode nodeArray = newArrayNode(); |
|||
MetricDataType metricDataType = MetricDataType.fromInteger(metricType); |
|||
try { |
|||
switch (metricDataType) { |
|||
case Boolean: |
|||
return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.BOOLEAN_V) |
|||
.setBoolV(protoMetric.getBooleanValue()).build()); |
|||
case DateTime: |
|||
case Int64: |
|||
return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.LONG_V) |
|||
.setLongV(protoMetric.getLongValue()).build()); |
|||
case Float: |
|||
var f = new BigDecimal(String.valueOf(protoMetric.getFloatValue())); |
|||
return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.DOUBLE_V) |
|||
.setDoubleV(f.doubleValue()).build()); |
|||
case Double: |
|||
return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.LONG_V) |
|||
.setLongV(Double.valueOf(protoMetric.getDoubleValue()).longValue()).build()); |
|||
case Int8: |
|||
case UInt8: |
|||
case Int16: |
|||
case Int32: |
|||
case UInt16: |
|||
return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.LONG_V) |
|||
.setLongV(protoMetric.getIntValue()).build()); |
|||
case UInt32: |
|||
case UInt64: |
|||
if (protoMetric.hasIntValue()) { |
|||
return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.LONG_V) |
|||
.setLongV(protoMetric.getIntValue()).build()); |
|||
} else if (protoMetric.hasLongValue()) { |
|||
return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.LONG_V) |
|||
.setLongV(protoMetric.getLongValue()).build()); |
|||
} else { |
|||
log.error("Invalid value for UInt32 datatype"); |
|||
throw new ThingsboardException("Invalid value for " + MetricDataType.fromInteger(metricType).name() + " datatype " + metricType, ThingsboardErrorCode.INVALID_ARGUMENTS); |
|||
} |
|||
case String: |
|||
case Text: |
|||
case UUID: |
|||
return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.STRING_V) |
|||
.setStringV(protoMetric.getStringValue()).build()); |
|||
// byte[]
|
|||
case Bytes: |
|||
ByteBuffer byteBuffer = ByteBuffer.wrap(protoMetric.getBytesValue().toByteArray()); |
|||
while (byteBuffer.hasRemaining()) { |
|||
nodeArray.add(byteBuffer.get()); |
|||
} |
|||
return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.JSON_V) |
|||
.setJsonV(nodeArray.toString()).build()); |
|||
case DataSet: |
|||
case Template: |
|||
case File: |
|||
//TODO
|
|||
// Build the and create the DataSet
|
|||
/** |
|||
SparkplugBProto.Payload.DataSet protoDataSet = protoMetric.getDatasetValue(); |
|||
return new SparkplugBProto.Payload.DataSet.Builder(protoDataSet.getNumOfColumns()).addColumnNames(protoDataSet.getColumnsList()) |
|||
.addTypes(convertDataSetDataTypes(protoDataSet.getTypesList())) |
|||
.addRows(convertDataSetRows(protoDataSet.getRowsList(), protoDataSet.getTypesList())) |
|||
.createDataSet(); |
|||
return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.STRING_V) |
|||
.setStringV(protoDataSet.toString()).build()); |
|||
**/ |
|||
//TODO
|
|||
// Build the and create the Template
|
|||
/** |
|||
SparkplugBProto.Payload.Template protoTemplate = protoMetric.getTemplateValue(); |
|||
return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.STRING_V) |
|||
.setStringV( protoTemplate.toString()).build()); |
|||
**/ |
|||
//TODO
|
|||
// Build the and create the File
|
|||
/** |
|||
String filename = protoMetric.getMetadata().getFileName(); |
|||
return Optional.of(builderPrbyteValueoto.setKey(key + "_" + filename).setType(TransportProtos.KeyValueType.STRING_V) |
|||
.setStringV(Hex.encodeHexString((protoMetric.getBytesValue().toByteArray()))).build()); |
|||
**/ |
|||
return Optional.empty(); |
|||
case Unknown: |
|||
default: |
|||
throw new ThingsboardException("Failed to decode: Unknown MetricDataType " + metricType, ThingsboardErrorCode.INVALID_ARGUMENTS); |
|||
} |
|||
} catch (Exception e) { |
|||
log.error("", e); |
|||
return Optional.empty(); |
|||
} |
|||
} |
|||
|
|||
public static SparkplugBProto.Payload.Metric createMetric(Object value, long ts, String key, MetricDataType metricDataType) throws ThingsboardException { |
|||
SparkplugBProto.Payload.Metric metric = SparkplugBProto.Payload.Metric.newBuilder() |
|||
.setTimestamp(ts) |
|||
.setName(key) |
|||
.setDatatype(metricDataType.toIntValue()) |
|||
.build(); |
|||
switch (metricDataType) { |
|||
case Int8: // (byte)
|
|||
return metric.toBuilder().setIntValue(((Byte) value).intValue()).build(); |
|||
case Int16: // (short)
|
|||
case UInt8: |
|||
return metric.toBuilder().setIntValue(((Short) value).intValue()).build(); |
|||
case UInt16: // (int)
|
|||
case Int32: |
|||
return metric.toBuilder().setIntValue(((Integer) value).intValue()).build(); |
|||
case UInt32: // (long)
|
|||
case Int64: |
|||
case UInt64: |
|||
case DateTime: |
|||
return metric.toBuilder().setLongValue(((Long) value).longValue()).build(); |
|||
case Float: // (float)
|
|||
return metric.toBuilder().setFloatValue(((Float) value).floatValue()).build(); |
|||
case Double: // (double)
|
|||
return metric.toBuilder().setDoubleValue(((Double) value).doubleValue()).build(); |
|||
case Boolean: // (boolean)
|
|||
return metric.toBuilder().setBooleanValue(((Boolean) value).booleanValue()).build(); |
|||
case String: // String)
|
|||
case Text: |
|||
case UUID: |
|||
return metric.toBuilder().setStringValue((String) value).build(); |
|||
case Bytes: |
|||
ByteString byteString = ByteString.copyFrom((byte[]) value); |
|||
return metric.toBuilder().setBytesValue(byteString).build(); |
|||
case DataSet: |
|||
return metric.toBuilder().setDatasetValue((SparkplugBProto.Payload.DataSet) value).build(); |
|||
case File: |
|||
SparkplugMetricUtil.File file = (SparkplugMetricUtil.File) value; |
|||
ByteString byteFileString = ByteString.copyFrom(file.getBytes()); |
|||
return metric.toBuilder().setBytesValue(byteFileString).build(); |
|||
case Template: |
|||
return metric.toBuilder().setTemplateValue((SparkplugBProto.Payload.Template) value).build(); |
|||
case Unknown: |
|||
throw new ThingsboardException("Invalid value for MetricDataType " + metricDataType.name(), ThingsboardErrorCode.INVALID_ARGUMENTS); |
|||
} |
|||
return metric; |
|||
} |
|||
|
|||
public static TransportProtos.TsKvProto getTsKvProto(String key, Object value, long ts) throws ThingsboardException { |
|||
try { |
|||
TransportProtos.TsKvProto.Builder tsKvProtoBuilder = TransportProtos.TsKvProto.newBuilder(); |
|||
TransportProtos.KeyValueProto.Builder keyValueProtoBuilder = TransportProtos.KeyValueProto.newBuilder(); |
|||
keyValueProtoBuilder.setKey(key); |
|||
if (value instanceof String) { |
|||
keyValueProtoBuilder.setType(TransportProtos.KeyValueType.STRING_V); |
|||
keyValueProtoBuilder.setStringV((String) value); |
|||
} else if (value instanceof Integer) { |
|||
keyValueProtoBuilder.setType(TransportProtos.KeyValueType.LONG_V); |
|||
keyValueProtoBuilder.setLongV((Integer) value); |
|||
} else if (value instanceof Long) { |
|||
keyValueProtoBuilder.setType(TransportProtos.KeyValueType.LONG_V); |
|||
keyValueProtoBuilder.setLongV((Long) value); |
|||
} else if (value instanceof Boolean) { |
|||
keyValueProtoBuilder.setType(TransportProtos.KeyValueType.BOOLEAN_V); |
|||
keyValueProtoBuilder.setBoolV((Boolean) value); |
|||
} else if (value instanceof Double) { |
|||
keyValueProtoBuilder.setType(TransportProtos.KeyValueType.DOUBLE_V); |
|||
keyValueProtoBuilder.setDoubleV((Double) value); |
|||
} else if (value instanceof List) { |
|||
keyValueProtoBuilder.setType(TransportProtos.KeyValueType.JSON_V); |
|||
ArrayNode arrayNodeBytes = JacksonUtil.convertValue(value, ArrayNode.class); |
|||
keyValueProtoBuilder.setJsonV(arrayNodeBytes.toString()); |
|||
} else { |
|||
throw new ThingsboardException("Failed to convert device/node RPC command to TsKvProto for Sparkplug MQT msg: value [" + value + "]", ThingsboardErrorCode.INVALID_ARGUMENTS); |
|||
} |
|||
tsKvProtoBuilder.setKv(keyValueProtoBuilder.build()); |
|||
tsKvProtoBuilder.setTs(ts); |
|||
return tsKvProtoBuilder.build(); |
|||
} catch (Exception e) { |
|||
throw new ThingsboardException("Failed to convert device/node RPC command to TsKvProto for Sparkplug MQT msg: value [" + value + "]", ThingsboardErrorCode.INVALID_ARGUMENTS); |
|||
} |
|||
} |
|||
|
|||
public static Optional<Object> validatedValueByTypeMetric(TransportProtos.KeyValueProto kv, MetricDataType metricDataType) throws ThingsboardException { |
|||
if (kv.getTypeValue() <= 3) { |
|||
return validatedValuePrimitiveByTypeMetric(kv, metricDataType); |
|||
} else if (kv.getTypeValue() == 4) { |
|||
JsonNode arrayNode = JacksonUtil.fromString(kv.getJsonV(), JsonNode.class); |
|||
if (arrayNode.isArray()) { |
|||
return validatedValueJsonByTypeMetric(kv.getJsonV(), metricDataType); |
|||
} |
|||
} else { |
|||
throw new ThingsboardException("Invalid type KeyValueProto " + kv.toString() + " for MetricDataType " + metricDataType.name(), ThingsboardErrorCode.INVALID_ARGUMENTS); |
|||
} |
|||
return Optional.empty(); |
|||
} |
|||
|
|||
public static Optional<Object> validatedValuePrimitiveByTypeMetric(TransportProtos.KeyValueProto kv, MetricDataType metricDataType) throws ThingsboardException { |
|||
Optional<String> valueOpt = getValueKvProtoPrimitive(kv); |
|||
if (valueOpt.isPresent()) { |
|||
try { |
|||
switch (metricDataType) { |
|||
// int
|
|||
case Int8: |
|||
case Int16: |
|||
case UInt8: |
|||
case UInt16: |
|||
case Int32: |
|||
Optional<Integer> boolInt8 = booleanStringToInt(valueOpt.get()); |
|||
if (boolInt8.isPresent()) { |
|||
return Optional.of(boolInt8.get()); |
|||
} |
|||
try { |
|||
return Optional.of(Integer.valueOf(valueOpt.get())); |
|||
} catch (NumberFormatException eInt) { |
|||
var i = new BigDecimal(valueOpt.get()); |
|||
if (i.longValue() <= Integer.MAX_VALUE) { |
|||
return Optional.of(i.intValue()); |
|||
} |
|||
throw new ThingsboardException("Invalid type value " + kv.toString() + " for MetricDataType " |
|||
+ metricDataType.name(), eInt, ThingsboardErrorCode.INVALID_ARGUMENTS); |
|||
} |
|||
// long
|
|||
case UInt32: |
|||
case Int64: |
|||
case UInt64: |
|||
case DateTime: |
|||
Optional<Integer> boolInt64 = booleanStringToInt(valueOpt.get()); |
|||
if (boolInt64.isPresent()) { |
|||
return Optional.of(Long.valueOf(boolInt64.get())); |
|||
} |
|||
var l = new BigDecimal(valueOpt.get()); |
|||
return Optional.of(l.longValue()); |
|||
// float
|
|||
case Float: |
|||
Optional<Integer> boolFloat = booleanStringToInt(valueOpt.get()); |
|||
if (boolFloat.isPresent()) { |
|||
var fb = new BigDecimal(boolFloat.get()); |
|||
return Optional.of(fb.floatValue()); |
|||
} |
|||
var f = new BigDecimal(valueOpt.get()); |
|||
return Optional.of(f.floatValue()); |
|||
// double
|
|||
case Double: |
|||
Optional<Integer> boolDouble = booleanStringToInt(valueOpt.get()); |
|||
if (boolDouble.isPresent()) { |
|||
return Optional.of(Double.valueOf(boolDouble.get())); |
|||
} |
|||
var dd = new BigDecimal(valueOpt.get()); |
|||
return Optional.of(dd.doubleValue()); |
|||
case Boolean: |
|||
if ("true".equals(valueOpt.get())) { |
|||
return Optional.of(true); |
|||
} else if ("false".equals(valueOpt.get())) { |
|||
return Optional.of(false); |
|||
} else { |
|||
Number number = NumberFormat.getInstance().parse(valueOpt.get()); |
|||
if (StringUtils.isBlank(number.toString()) || "0".equals(number.toString())) { // ok 0
|
|||
return Optional.of(false); |
|||
} else { |
|||
return Optional.of(true); |
|||
} |
|||
} |
|||
case String: |
|||
case Text: |
|||
case UUID: |
|||
return Optional.of(valueOpt.get()); |
|||
} |
|||
} catch (Exception e) { |
|||
log.trace("Invalid type value [{}] for MetricDataType [{}] [{}]", kv, metricDataType.name(), e.getMessage()); |
|||
throw new ThingsboardException("Invalid type value " + kv.toString() + " for MetricDataType " + metricDataType.name(), e, ThingsboardErrorCode.INVALID_ARGUMENTS); |
|||
} |
|||
} |
|||
return Optional.empty(); |
|||
} |
|||
|
|||
public static Optional<Object> validatedValueJsonByTypeMetric(String arrayNodeStr, MetricDataType metricDataType) { |
|||
try { |
|||
Optional<Object> valueOpt; |
|||
switch (metricDataType) { |
|||
// byte[]
|
|||
case Bytes: |
|||
List<Byte> listBytes = JacksonUtil.fromString(arrayNodeStr, new TypeReference<>() { |
|||
}); |
|||
byte[] bytes = new byte[listBytes.size()]; |
|||
for (int i = 0; i < listBytes.size(); i++) { |
|||
bytes[i] = listBytes.get(i).byteValue(); |
|||
} |
|||
return Optional.of(bytes); |
|||
case DataSet: |
|||
case File: |
|||
case Template: |
|||
log.error("Invalid type value [{}] for MetricDataType [{}]", arrayNodeStr, metricDataType.name()); |
|||
return Optional.empty(); |
|||
case Unknown: |
|||
default: |
|||
log.error("Invalid MetricDataType [{}] type, value [{}]", arrayNodeStr, metricDataType.name()); |
|||
return Optional.empty(); |
|||
} |
|||
} catch (Exception e) { |
|||
log.error("Invalid type value [{}] for MetricDataType [{}] [{}]", arrayNodeStr, metricDataType.name(), e.getMessage()); |
|||
return Optional.empty(); |
|||
} |
|||
} |
|||
|
|||
private static Optional<String> getValueKvProtoPrimitive(TransportProtos.KeyValueProto kv) { |
|||
if (kv.getTypeValue() == 0) { // boolean
|
|||
return Optional.of(String.valueOf(kv.getBoolV())); |
|||
} else if (kv.getTypeValue() == 1) { // kvLong
|
|||
return Optional.of(String.valueOf(kv.getLongV())); |
|||
} else if (kv.getTypeValue() == 2) { // kvDouble/float
|
|||
return Optional.of(String.valueOf(kv.getDoubleV())); |
|||
} else if (kv.getTypeValue() == 3) { // kvString
|
|||
return Optional.of(kv.getStringV()); |
|||
} else { |
|||
return Optional.empty(); |
|||
} |
|||
} |
|||
|
|||
private static Optional<Integer> booleanStringToInt(String booleanStr) { |
|||
if ("true".equals(booleanStr)) { |
|||
return Optional.of(1); |
|||
} else if ("false".equals(booleanStr)) { |
|||
return Optional.of(0); |
|||
} else { |
|||
return Optional.empty(); |
|||
} |
|||
} |
|||
|
|||
@JsonIgnoreProperties( |
|||
value = {"fileName"}) |
|||
@JsonSerialize( |
|||
using = FileSerializer.class) |
|||
public class File { |
|||
|
|||
private String fileName; |
|||
private byte[] bytes; |
|||
|
|||
/** |
|||
* Default Constructor |
|||
*/ |
|||
public File() { |
|||
super(); |
|||
} |
|||
|
|||
/** |
|||
* Constructor |
|||
* |
|||
* @param fileName the full file name path |
|||
* @param bytes the array of bytes that represent the contents of the file |
|||
*/ |
|||
public File(String fileName, byte[] bytes) { |
|||
super(); |
|||
this.fileName = fileName == null |
|||
? null |
|||
: fileName.replace("/", System.getProperty("file.separator")).replace("\\", |
|||
System.getProperty("file.separator")); |
|||
this.bytes = Arrays.copyOf(bytes, bytes.length); |
|||
} |
|||
|
|||
/** |
|||
* Gets the full filename path |
|||
* |
|||
* @return the full filename path |
|||
*/ |
|||
public String getFileName() { |
|||
return fileName; |
|||
} |
|||
|
|||
/** |
|||
* Sets the full filename path |
|||
* |
|||
* @param fileName the full filename path |
|||
*/ |
|||
public void setFileName(String fileName) { |
|||
this.fileName = fileName; |
|||
} |
|||
|
|||
/** |
|||
* Gets the bytes that represent the contents of the file |
|||
* |
|||
* @return the bytes that represent the contents of the file |
|||
*/ |
|||
public byte[] getBytes() { |
|||
return bytes; |
|||
} |
|||
|
|||
/** |
|||
* Sets the bytes that represent the contents of the file |
|||
* |
|||
* @param bytes the bytes that represent the contents of the file |
|||
*/ |
|||
public void setBytes(byte[] bytes) { |
|||
this.bytes = bytes; |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
StringBuilder builder = new StringBuilder(); |
|||
builder.append("File [fileName="); |
|||
builder.append(fileName); |
|||
builder.append(", bytes="); |
|||
builder.append(Arrays.toString(bytes)); |
|||
builder.append("]"); |
|||
return builder.toString(); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.util.sparkplug; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
@JsonIgnoreProperties(ignoreUnknown = true) |
|||
public class SparkplugRpcRequestHeader { |
|||
|
|||
private String messageType; |
|||
private String metricName; |
|||
private Object value; |
|||
|
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.util.sparkplug; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonInclude; |
|||
import lombok.Builder; |
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
@Builder |
|||
@JsonInclude(JsonInclude.Include.NON_NULL) |
|||
public class SparkplugRpcResponseBody { |
|||
|
|||
private String result; |
|||
private String value; |
|||
private String error; |
|||
|
|||
} |
|||
@ -0,0 +1,164 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.util.sparkplug; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonInclude; |
|||
|
|||
/** |
|||
* Created by nickAS21 on 12.12.22 |
|||
*/ |
|||
@JsonInclude(JsonInclude.Include.NON_NULL) |
|||
public class SparkplugTopic { |
|||
|
|||
/** |
|||
* The Sparkplug namespace version. |
|||
* For the Sparkplug™ B version of the specification, the UTF-8 string constant for the namespace element will be: “spBv1.0” |
|||
*/ |
|||
private String namespace; |
|||
|
|||
/** |
|||
* The ID of the logical grouping of Edge of Network (EoN) Nodes and devices. |
|||
*/ |
|||
private String groupId; |
|||
|
|||
/** |
|||
* The ID of the Edge of Network (EoN) Node. |
|||
*/ |
|||
private String edgeNodeId; |
|||
|
|||
/** |
|||
* The ID of the device. |
|||
*/ |
|||
private String deviceId; |
|||
|
|||
/** |
|||
* The message type. |
|||
*/ |
|||
private SparkplugMessageType type; |
|||
|
|||
/** |
|||
* Constructor (device). |
|||
* |
|||
* @param namespace the namespace. |
|||
* @param groupId the group ID. |
|||
* @param edgeNodeId the edge node ID. |
|||
* @param deviceId the device ID. |
|||
* @param type the message type. |
|||
*/ |
|||
public SparkplugTopic(String namespace, String groupId, String edgeNodeId, String deviceId, SparkplugMessageType type) { |
|||
super(); |
|||
this.namespace = namespace; |
|||
this.groupId = groupId; |
|||
this.edgeNodeId = edgeNodeId; |
|||
this.deviceId = deviceId; |
|||
this.type = type; |
|||
} |
|||
|
|||
/** |
|||
* Constructor (node). |
|||
* |
|||
* @param namespace the namespace. |
|||
* @param groupId the group ID. |
|||
* @param edgeNodeId the edge node ID. |
|||
* @param type the message type. |
|||
*/ |
|||
public SparkplugTopic(String namespace, String groupId, String edgeNodeId, SparkplugMessageType type) { |
|||
super(); |
|||
this.namespace = namespace; |
|||
this.groupId = groupId; |
|||
this.edgeNodeId = edgeNodeId; |
|||
this.deviceId = null; |
|||
this.type = type; |
|||
} |
|||
|
|||
public SparkplugTopic(SparkplugTopic sparkplugTopic, SparkplugMessageType type) { |
|||
super(); |
|||
this.namespace = sparkplugTopic.namespace; |
|||
this.groupId = sparkplugTopic.groupId; |
|||
this.edgeNodeId = sparkplugTopic.edgeNodeId; |
|||
this.deviceId = null; |
|||
this.type = type; |
|||
} |
|||
public SparkplugTopic(SparkplugTopic sparkplugTopic, SparkplugMessageType type, String deviceId) { |
|||
super(); |
|||
this.namespace = sparkplugTopic.namespace; |
|||
this.groupId = sparkplugTopic.groupId; |
|||
this.edgeNodeId = sparkplugTopic.edgeNodeId; |
|||
this.deviceId = deviceId; |
|||
this.type = type; |
|||
} |
|||
|
|||
/** |
|||
* @return the Sparkplug namespace version |
|||
*/ |
|||
public String getNamespace() { |
|||
return namespace; |
|||
} |
|||
|
|||
/** |
|||
* Returns the ID of the logical grouping of Edge of Network (EoN) Nodes and devices. |
|||
* |
|||
* @return the group ID |
|||
*/ |
|||
public String getGroupId() { |
|||
return groupId; |
|||
} |
|||
|
|||
/** |
|||
* @return the ID of the Edge of Network (EoN) Node |
|||
*/ |
|||
public String getEdgeNodeId() { |
|||
return edgeNodeId; |
|||
} |
|||
|
|||
/** |
|||
* @return the device ID |
|||
*/ |
|||
public String getDeviceId() { |
|||
return deviceId; |
|||
} |
|||
|
|||
/** |
|||
* @return the message type |
|||
*/ |
|||
public SparkplugMessageType getType() { |
|||
return type; |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
StringBuilder sb = new StringBuilder(getNamespace()).append("/") |
|||
.append(getGroupId()).append("/") |
|||
.append(getType()).append("/") |
|||
.append(getEdgeNodeId()); |
|||
if (getDeviceId() != null) { |
|||
sb.append("/").append(getDeviceId()); |
|||
} |
|||
return sb.toString(); |
|||
} |
|||
|
|||
/** |
|||
* @param type the type to check |
|||
* @return true if this topic's type matches the passes in type, false otherwise |
|||
*/ |
|||
public boolean isType(SparkplugMessageType type) { |
|||
return this.type != null && this.type.equals(type); |
|||
} |
|||
|
|||
public boolean isNode() { |
|||
return this.deviceId == null; |
|||
} |
|||
} |
|||
@ -0,0 +1,116 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.transport.mqtt.util.sparkplug; |
|||
|
|||
import com.fasterxml.jackson.core.JsonProcessingException; |
|||
import com.fasterxml.jackson.databind.ObjectMapper; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
|
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* Provides utility methods for handling Sparkplug MQTT message topics. |
|||
*/ |
|||
public class SparkplugTopicUtil { |
|||
|
|||
private static final Map<String, String[]> SPLIT_TOPIC_CACHE = new HashMap<String, String[]>(); |
|||
private static final String TOPIC_INVALID_NUMBER = "Invalid number of topic elements: "; |
|||
public static final String NAMESPACE = "spBv1.0"; |
|||
|
|||
public static String[] getSplitTopic(String topic) { |
|||
String[] splitTopic = SPLIT_TOPIC_CACHE.get(topic); |
|||
if (splitTopic == null) { |
|||
splitTopic = topic.split("/"); |
|||
SPLIT_TOPIC_CACHE.put(topic, splitTopic); |
|||
} |
|||
|
|||
return splitTopic; |
|||
} |
|||
|
|||
/** |
|||
* Serializes a {@link SparkplugTopic} instance in to a JSON string. |
|||
* |
|||
* @param topic a {@link SparkplugTopic} instance |
|||
* @return a JSON string |
|||
* @throws JsonProcessingException |
|||
*/ |
|||
public static String sparkplugTopicToString(SparkplugTopic topic) throws JsonProcessingException { |
|||
ObjectMapper mapper = new ObjectMapper(); |
|||
return mapper.writeValueAsString(topic); |
|||
} |
|||
|
|||
/** |
|||
* Parses a Sparkplug MQTT message topic string and returns a {@link SparkplugTopic} instance. |
|||
* |
|||
* @param topic a topic string |
|||
* @return a {@link SparkplugTopic} instance |
|||
* @throws ThingsboardException if an error occurs while parsing |
|||
*/ |
|||
public static SparkplugTopic parseTopicSubscribe(String topic) throws ThingsboardException { |
|||
// TODO "+", "$"
|
|||
topic = topic.indexOf("#") > 0 ? topic.substring(0, topic.indexOf("#")) : topic; |
|||
return parseTopic(SparkplugTopicUtil.getSplitTopic(topic)); |
|||
} |
|||
|
|||
public static SparkplugTopic parseTopicPublish(String topic) throws ThingsboardException { |
|||
if (topic.contains("#") || topic.contains("$") || topic.contains("+")) { |
|||
throw new ThingsboardException("Invalid of topic elements for Publish", ThingsboardErrorCode.INVALID_ARGUMENTS); |
|||
} else { |
|||
String[] splitTopic = SparkplugTopicUtil.getSplitTopic(topic); |
|||
if (splitTopic.length < 4 || splitTopic.length > 5) { |
|||
throw new ThingsboardException(TOPIC_INVALID_NUMBER + splitTopic.length, ThingsboardErrorCode.INVALID_ARGUMENTS); |
|||
} |
|||
return parseTopic(splitTopic); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Parses a Sparkplug MQTT message topic string and returns a {@link SparkplugTopic} instance. |
|||
* |
|||
* @param splitTopic a topic split into tokens |
|||
* @return a {@link SparkplugTopic} instance |
|||
* @throws Exception if an error occurs while parsing |
|||
*/ |
|||
@SuppressWarnings("incomplete-switch") |
|||
public static SparkplugTopic parseTopic(String[] splitTopic) throws ThingsboardException { |
|||
int length = splitTopic.length; |
|||
if (length == 0) { |
|||
throw new ThingsboardException(TOPIC_INVALID_NUMBER + length, ThingsboardErrorCode.INVALID_ARGUMENTS); |
|||
} else { |
|||
SparkplugMessageType type; |
|||
String namespace, edgeNodeId, groupId, deviceId; |
|||
namespace = validateNameSpace(splitTopic[0]); |
|||
groupId = length > 1 ? splitTopic[1] : null; |
|||
type = length > 2 ? SparkplugMessageType.parseMessageType(splitTopic[2]) : null; |
|||
edgeNodeId = length > 3 ? splitTopic[3] : null; |
|||
deviceId = length > 4 ? splitTopic[4] : null; |
|||
return new SparkplugTopic(namespace, groupId, edgeNodeId, deviceId, type); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* For the Sparkplug™ B version of the specification, the UTF-8 string constant for the namespace element will be: "spBv1.0" |
|||
* @param nameSpace |
|||
* @return |
|||
*/ |
|||
private static String validateNameSpace(String nameSpace) throws ThingsboardException { |
|||
if (NAMESPACE.equals(nameSpace)) return nameSpace; |
|||
throw new ThingsboardException("The namespace [" + nameSpace + "] is not valid and must be [" + NAMESPACE + "] for the Sparkplug™ B version.", ThingsboardErrorCode.INVALID_ARGUMENTS); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,204 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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. |
|||
*/ |
|||
syntax = "proto3"; |
|||
|
|||
import "google/protobuf/any.proto"; |
|||
|
|||
|
|||
option java_package = "org.thingsboard.server.gen.transport.mqtt"; |
|||
option java_outer_classname = "SparkplugBProto"; |
|||
|
|||
message Payload { |
|||
/* |
|||
// Indexes of Data Types |
|||
// Unknown placeholder for future expansion. |
|||
Unknown = 0; |
|||
// Basic Types |
|||
Int8 = 1; |
|||
Int16 = 2; |
|||
Int32 = 3; |
|||
Int64 = 4; |
|||
UInt8 = 5; |
|||
UInt16 = 6; |
|||
UInt32 = 7; |
|||
UInt64 = 8; |
|||
Float = 9; |
|||
Double = 10; |
|||
Boolean = 11; |
|||
String = 12; |
|||
DateTime = 13; |
|||
Text = 14; |
|||
// Additional Metric Types |
|||
UUID = 15; |
|||
DataSet = 16; |
|||
Bytes = 17; |
|||
File = 18; |
|||
Template = 19; |
|||
|
|||
// Additional PropertyValue Types |
|||
PropertySet = 20; |
|||
PropertySetList = 21; |
|||
*/ |
|||
|
|||
message Template { |
|||
|
|||
message Parameter { |
|||
optional string name = 1; |
|||
optional uint32 type = 2; |
|||
|
|||
oneof value { |
|||
uint32 int_value = 3; |
|||
uint64 long_value = 4; |
|||
float float_value = 5; |
|||
double double_value = 6; |
|||
bool boolean_value = 7; |
|||
string string_value = 8; |
|||
ParameterValueExtension extension_value = 9; |
|||
} |
|||
|
|||
message ParameterValueExtension { |
|||
google.protobuf.Any extensions = 1; |
|||
} |
|||
} |
|||
|
|||
optional string version = 1; // The version of the Template to prevent mismatches |
|||
repeated Metric metrics = 2; // Each metric is the name of the metric and the datatype of the member but does not contain a value |
|||
repeated Parameter parameters = 3; |
|||
optional string template_ref = 4; // Reference to a template if this is extending a Template or an instance - must exist if an instance |
|||
optional bool is_definition = 5; |
|||
google.protobuf.Any extensions = 6; |
|||
} |
|||
|
|||
message DataSet { |
|||
|
|||
message DataSetValue { |
|||
|
|||
oneof value { |
|||
uint32 int_value = 1; |
|||
uint64 long_value = 2; |
|||
float float_value = 3; |
|||
double double_value = 4; |
|||
bool boolean_value = 5; |
|||
string string_value = 6; |
|||
DataSetValueExtension extension_value = 7; |
|||
} |
|||
|
|||
message DataSetValueExtension { |
|||
google.protobuf.Any extensions = 1; |
|||
} |
|||
} |
|||
|
|||
message Row { |
|||
repeated DataSetValue elements = 1; |
|||
google.protobuf.Any extensions = 2; // For third party extensions |
|||
} |
|||
|
|||
optional uint64 num_of_columns = 1; |
|||
repeated string columns = 2; |
|||
repeated uint32 types = 3; |
|||
repeated Row rows = 4; |
|||
google.protobuf.Any extensions = 5; // For third party extensions |
|||
} |
|||
|
|||
message PropertyValue { |
|||
|
|||
optional uint32 type = 1; |
|||
optional bool is_null = 2; |
|||
|
|||
oneof value { |
|||
uint32 int_value = 3; |
|||
uint64 long_value = 4; |
|||
float float_value = 5; |
|||
double double_value = 6; |
|||
bool boolean_value = 7; |
|||
string string_value = 8; |
|||
PropertySet propertyset_value = 9; |
|||
PropertySetList propertysets_value = 10; // List of Property Values |
|||
PropertyValueExtension extension_value = 11; |
|||
} |
|||
|
|||
message PropertyValueExtension { |
|||
google.protobuf.Any extensions = 1; |
|||
} |
|||
} |
|||
|
|||
message PropertySet { |
|||
repeated string keys = 1; // Names of the properties |
|||
repeated PropertyValue values = 2; |
|||
google.protobuf.Any extensions = 3; |
|||
} |
|||
|
|||
message PropertySetList { |
|||
repeated PropertySet propertyset = 1; |
|||
google.protobuf.Any extensions = 2; |
|||
} |
|||
|
|||
message MetaData { |
|||
// Bytes specific metadata |
|||
optional bool is_multi_part = 1; |
|||
|
|||
// General metadata |
|||
optional string content_type = 2; // Content/Media type |
|||
optional uint64 size = 3; // File size, String size, Multi-part size, etc |
|||
optional uint64 seq = 4; // Sequence number for multi-part messages |
|||
|
|||
// File metadata |
|||
optional string file_name = 5; // File name |
|||
optional string file_type = 6; // File type (i.e. xml, json, txt, cpp, etc) |
|||
optional string md5 = 7; // md5 of data |
|||
|
|||
// Catchalls and future expansion |
|||
optional string description = 8; // Could be anything such as json or xml of custom properties |
|||
google.protobuf.Any extensions = 9; |
|||
} |
|||
|
|||
message Metric { |
|||
|
|||
optional string name = 1; // Metric name - should only be included on birth |
|||
optional uint64 alias = 2; // Metric alias - tied to name on birth and included in all later DATA messages |
|||
optional uint64 timestamp = 3; // Timestamp associated with data acquisition time |
|||
optional uint32 datatype = 4; // DataType of the metric/tag value |
|||
optional bool is_historical = 5; // If this is historical data and should not update real time tag |
|||
optional bool is_transient = 6; // Tells consuming clients such as MQTT Engine to not store this as a tag |
|||
optional bool is_null = 7; // If this is null - explicitly say so rather than using -1, false, etc for some datatypes. |
|||
optional MetaData metadata = 8; // Metadata for the payload |
|||
optional PropertySet properties = 9; |
|||
|
|||
oneof value { |
|||
uint32 int_value = 10; |
|||
uint64 long_value = 11; |
|||
float float_value = 12; |
|||
double double_value = 13; |
|||
bool boolean_value = 14; |
|||
string string_value = 15; |
|||
bytes bytes_value = 16; // Bytes, File |
|||
DataSet dataset_value = 17; |
|||
Template template_value = 18; |
|||
MetricValueExtension extension_value = 19; |
|||
} |
|||
|
|||
message MetricValueExtension { |
|||
google.protobuf.Any extensions = 1; |
|||
} |
|||
} |
|||
|
|||
optional uint64 timestamp = 1; // Timestamp at message sending time |
|||
repeated Metric metrics = 2; // Repeated forever - no limit in Google Protobufs |
|||
optional uint64 seq = 3; // Sequence number |
|||
optional string uuid = 4; // UUID to track message type in terms of schema definitions |
|||
optional bytes body = 5; // To optionally bypass the whole definition above |
|||
google.protobuf.Any extensions = 6; |
|||
} |
|||
Loading…
Reference in new issue