propagationEntityIds = proto.getPropagationEntityIdsList().stream().map(ProtoUtils::fromProto).toList();
+ state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(propagationEntityIds));
}
case ALARM -> {
AlarmCalculatedFieldState alarmState = (AlarmCalculatedFieldState) state;
diff --git a/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java
index 172a3e8e2d..c72620bb8f 100644
--- a/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java
+++ b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java
@@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.lwm2m.LwM2mInstance;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
import org.thingsboard.server.common.data.lwm2m.LwM2mResourceObserve;
import org.thingsboard.server.common.data.util.TbDDFFileParser;
-import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.exception.DataValidationException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 66669a8280..787111c28e 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -516,14 +516,14 @@ actors:
response_timeout_ms: "${ACTORS_RPC_RESPONSE_TIMEOUT_MS:30000}"
# Close transport session if RPC delivery timed out. If enabled, RPC will be reverted to the queued state.
# Note:
- # - For MQTT transport:
- # - QoS level 0: This feature does not apply, as no acknowledgment is expected, and therefore no timeout is triggered.
- # - QoS level 1: This feature applies, as an acknowledgment is expected.
- # - QoS level 2: Unsupported.
- # - For CoAP transport:
- # - Confirmable requests: This feature applies, as delivery confirmation is expected.
- # - Non-confirmable requests: This feature does not apply, as no delivery acknowledgment is expected.
- # - For HTTP and SNPM transports: RPC is considered delivered immediately, and there is no logic to await acknowledgment.
+ # - For MQTT transport:
+ #
- QoS level 0: This feature does not apply, as no acknowledgment is expected, and therefore no timeout is triggered.
+ # - QoS level 1: This feature applies, as an acknowledgment is expected.
+ # - QoS level 2: Unsupported.
+ # - For CoAP transport:
+ #
- Confirmable requests: This feature applies, as delivery confirmation is expected.
+ # - Non-confirmable requests: This feature does not apply, as no delivery acknowledgment is expected.
+ # - For HTTP and SNPM transports: RPC is considered delivered immediately, and there is no logic to await acknowledgment.
close_session_on_rpc_delivery_timeout: "${ACTORS_RPC_CLOSE_SESSION_ON_RPC_DELIVERY_TIMEOUT:false}"
statistics:
# Enable/disable actor statistics
@@ -733,13 +733,13 @@ redis:
# if set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
sentinel:
- # name of the master node
+ # Name of the master node
master: "${REDIS_MASTER:}"
- # comma-separated list of "host:port" pairs of sentinels
+ # Comma-separated list of "host:port" pairs of sentinels
sentinels: "${REDIS_SENTINELS:}"
- # password to authenticate with sentinel
+ # Password to authenticate with sentinel
password: "${REDIS_SENTINEL_PASSWORD:}"
- # if set false will be used pool config build from values of the pool config section
+ # If set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
# db index
db: "${REDIS_DB:0}"
@@ -1039,10 +1039,10 @@ transport:
activity:
# This property specifies the strategy for reporting activity events within each reporting period.
# The accepted values are 'FIRST', 'LAST', 'FIRST_AND_LAST' and 'ALL'.
- # - 'FIRST': Only the first activity event in each reporting period is reported.
- # - 'LAST': Only the last activity event in the reporting period is reported.
- # - 'FIRST_AND_LAST': Both the first and last activity events in the reporting period are reported.
- # - 'ALL': All activity events in the reporting period are reported.
+ # - 'FIRST': Only the first activity event in each reporting period is reported.
+ # - 'LAST': Only the last activity event in the reporting period is reported.
+ # - 'FIRST_AND_LAST': Both the first and last activity events in the reporting period are reported.
+ # - 'ALL': All activity events in the reporting period are reported.
reporting_strategy: "${TB_TRANSPORT_ACTIVITY_REPORTING_STRATEGY:LAST}"
json:
# Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON
@@ -1160,15 +1160,15 @@ transport:
dtls:
# RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000
retransmission_timeout: "${LWM2M_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}"
- # CoAP DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2
- # Default: off
+ # LWM2M DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2
+ # Default: off.
# Control usage of DTLS connection ID length (CID).
- # - 'off' to deactivate it.
- # - 'on' to activate Connection ID support (same as CID 0 or more 0).
- # - A positive value defines generated CID size in bytes.
- # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
- # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
- # - A value that are > 4: MultiNodeConnectionIdGenerator is used
+ # - 'off' to deactivate it.
+ # - 'on' to activate Connection ID support (same as CID 0 or more 0).
+ # - A positive value defines generated CID size in bytes.
+ # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
+ # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
+ # - A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:8}"
server:
# LwM2M Server ID
@@ -1351,14 +1351,14 @@ coap:
# CoAP DTLS bind port
bind_port: "${COAP_DTLS_BIND_PORT:5684}"
# CoAP DTLS connection ID length. RFC 9146, Connection Identifier for DTLS 1.2
- # Default: off
+ # Default: off.
# Control usage of DTLS connection ID length (CID).
- # - 'off' to deactivate it.
- # - 'on' to activate Connection ID support (same as CID 0 or more 0).
- # - A positive value defines generated CID size in bytes.
- # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
- # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
- # - A value that are > 4: MultiNodeConnectionIdGenerator is used
+ # - 'off' to deactivate it.
+ # - 'on' to activate Connection ID support (same as CID 0 or more 0).
+ # - A positive value defines generated CID size in bytes.
+ # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
+ # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
+ # - A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:8}"
# Specify the MTU (Maximum Transmission Unit).
# Should be used if LAN MTU is not used, e.g. if IP tunnels are used or if the client uses a smaller value than the LAN MTU.
@@ -1375,13 +1375,13 @@ coap:
# In order to negotiate smaller maximum fragment lengths,
# clients MAY include an extension of type "max_fragment_length" in the (extended) client hello.
# The "extension_data" field of this extension SHALL contain:
- # enum {
+ # enum {
# 2^9(1) == 512,
# 2^10(2) == 1024,
# 2^11(3) == 2048,
# 2^12(4) == 4096,
# (255)
- # } MaxFragmentLength;
+ # } MaxFragmentLength;
# TLS already requires clients and servers to support fragmentation of handshake messages.
max_fragment_length: "${COAP_DTLS_MAX_FRAGMENT_LENGTH:1024}"
# Server DTLS credentials
@@ -1745,6 +1745,8 @@ queue:
print-interval-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_MIN_PRINT_INTERVAL_MS:60000}"
# Time to wait for the stats-loading requests to Kafka to finish
kafka-response-timeout-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_RESPONSE_TIMEOUT_MS:1000}"
+ # Topics cache TTL in milliseconds. 5 minutes by default
+ topics_cache_ttl_ms: "${TB_QUEUE_KAFKA_TOPICS_CACHE_TTL_MS:300000}"
partitions:
hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" # murmur3_32, murmur3_128 or sha256
transport_api:
diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
index 6f1ae6f481..fa30dc9025 100644
--- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
@@ -799,23 +799,22 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
doPost("/api/calculatedField", cf, CalculatedField.class);
- // --- Assert initial evaluation (ENTERED / OUTSIDE) ---
+ // --- Assert initial evaluation ---
await().alias("initial geofencing evaluation")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ArrayNode attrs = getServerAttributes(device.getId(),
"allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus", "restrictedZonesEvent");
- // --- no restrictedZonesEvent as no transition happened yet
- assertThat(attrs).isNotNull().isNotEmpty().hasSize(3);
+ // --- no transition events as no transitions happened yet
+ assertThat(attrs).isNotNull().isNotEmpty().hasSize(2);
Map m = kv(attrs);
- assertThat(m).containsEntry("allowedZonesEvent", "ENTERED")
- .containsEntry("allowedZonesStatus", "INSIDE")
+ assertThat(m).containsEntry("allowedZonesStatus", "INSIDE")
.containsEntry("restrictedZonesStatus", "OUTSIDE");
});
// --- delete attributes reported in previous evaluation
- doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/SERVER_SCOPE?keys=allowedZonesEvent,allowedZonesStatus,restrictedZonesStatus", String.class);
+ doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/SERVER_SCOPE?keys=allowedZonesStatus,restrictedZonesStatus", String.class);
// --- Update restrictedZone by 'restrictedZone' attribute update
doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE,
@@ -909,17 +908,16 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
doPost("/api/calculatedField", cf, CalculatedField.class);
- // --- Assert initial evaluation (ENTERED / OUTSIDE) ---
+ // --- Assert initial evaluation ---
await().alias("initial geofencing evaluation")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ArrayNode attrs = getServerAttributes(device.getId(),
- "allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus");
- assertThat(attrs).isNotNull().isNotEmpty().hasSize(3);
+ "allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus", "restrictedZonesEvent");
+ assertThat(attrs).isNotNull().isNotEmpty().hasSize(2);
Map m = kv(attrs);
- assertThat(m).containsEntry("allowedZonesEvent", "ENTERED")
- .containsEntry("allowedZonesStatus", "INSIDE")
+ assertThat(m).containsEntry("allowedZonesStatus", "INSIDE")
.containsEntry("restrictedZonesStatus", "OUTSIDE");
});
@@ -1014,16 +1012,15 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
var geofencingConfiguration = (GeofencingCalculatedFieldConfiguration) configuration;
assertThat(geofencingConfiguration.isScheduledUpdateEnabled()).isTrue();
- // --- Assert initial evaluation (ENTERED) ---
+ // --- Assert initial evaluation ---
await().alias("initial geofencing evaluation")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus");
- assertThat(attrs).isNotNull().isNotEmpty().hasSize(2);
+ assertThat(attrs).isNotNull().isNotEmpty().hasSize(1);
Map m = kv(attrs);
- assertThat(m).containsEntry("allowedZonesEvent", "ENTERED")
- .containsEntry("allowedZonesStatus", "INSIDE");
+ assertThat(m).containsEntry("allowedZonesStatus", "INSIDE");
});
// --- Move device OUTSIDE Zone A (expect LEFT) ---
@@ -1057,11 +1054,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(device.getId(), savedCalculatedField.getId(), minAllowedScheduledUpdateIntervalInSecForCF);
- // --- Same coordinates as before, but now we expect ENTERED since a new zone is registered ---
+ // --- Same coordinates as before, but now we expect INSIDE group status since a new zone is registered ---
doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope",
JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}")).andExpect(status().isOk());
- // --- Assert dynamic refresh picks up new relation and flips event back to ENTERED on the next telemetry update ---
+ // --- Assert dynamic refresh picks up a new relation and flips status back to INSIDE on the next telemetry update ---
await().alias("dynamic refresh rebinds allowedZones")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(1, TimeUnit.SECONDS)
@@ -1069,7 +1066,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus");
assertThat(attrs).isNotNull().isNotEmpty().hasSize(2);
Map m = kv(attrs);
- assertThat(m).containsEntry("allowedZonesEvent", "ENTERED")
+ assertThat(m).containsEntry("allowedZonesEvent", "LEFT") // attribute from previous eval with outdated ts.
.containsEntry("allowedZonesStatus", "INSIDE");
});
}
@@ -1089,7 +1086,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
// Telemetry on device
doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope",
- JacksonUtil.toJsonNode("{\"temperature\":12.5}")).andExpect(status().isOk());
+ JacksonUtil.toJsonNode("{\"temperature\":12.5, \"humidity\":85}")).andExpect(status().isOk());
// --- Build CF: PROPAGATION with expression ---
CalculatedField cf = new CalculatedField();
@@ -1102,11 +1099,14 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE));
cfg.setApplyExpressionToResolvedArguments(true);
- Argument arg = new Argument();
- arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
- cfg.setArguments(Map.of("t", arg));
+ Argument arg1 = new Argument();
+ arg1.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
- cfg.setExpression("{\"testResult\": t * 2}");
+ Argument arg2 = new Argument();
+ arg2.setRefEntityKey(new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null));
+
+ cfg.setArguments(Map.of("t", arg1, "h", arg2));
+ cfg.setExpression("return { testResult: (t + h) / 2};");
AttributesOutput output = new AttributesOutput();
output.setScope(AttributeScope.SERVER_SCOPE);
@@ -1125,8 +1125,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
ArrayNode attrs2 = getServerAttributes(asset2.getId(), "testResult");
assertThat(attrs1).isNotNull();
assertThat(attrs2).isNotNull();
- assertThat(attrs1.get(0).get("value").asDouble()).isEqualTo(25.0);
- assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(25.0);
+ assertThat(attrs1.get(0).get("value").asDouble()).isEqualTo(48.75);
+ assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(48.75);
});
String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s",
@@ -1148,7 +1148,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
ArrayNode attrs2 = getServerAttributes(asset2.getId(), "testResult");
assertThat(attrs1).isNullOrEmpty();
assertThat(attrs2).isNotNull();
- assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(50);
+ assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(55);
});
}
@@ -1167,7 +1167,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
// Telemetry on device
long ts = System.currentTimeMillis() - 300000L;
- postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":12.5}}", ts));
+ postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":12.5, \"humidity\":85}}", ts));
// --- Build CF: PROPAGATION without expression ---
CalculatedField cf = new CalculatedField();
@@ -1180,9 +1180,12 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE));
cfg.setApplyExpressionToResolvedArguments(false); // arguments-only mode
- Argument arg = new Argument();
- arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
- cfg.setArguments(Map.of("temperatureComputed", arg));
+ Argument arg1 = new Argument();
+ arg1.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
+ Argument arg2 = new Argument();
+ arg2.setRefEntityKey(new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null));
+
+ cfg.setArguments(Map.of("temperatureComputed", arg1, "humidityComputed", arg2));
TimeSeriesOutput output = new TimeSeriesOutput();
output.setStrategy(new TimeSeriesImmediateOutputStrategy(0, true, true, true, true));
@@ -1197,14 +1200,18 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
- ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed");
- ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed");
+ ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed,humidityComputed");
+ ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed,humidityComputed");
assertThat(telemetry1).isNotNull();
assertThat(telemetry2).isNotNull();
assertThat(telemetry1.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts));
assertThat(telemetry1.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(12.5);
+ assertThat(telemetry1.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts));
+ assertThat(telemetry1.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85);
assertThat(telemetry2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts));
assertThat(telemetry2.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(12.5);
+ assertThat(telemetry2.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts));
+ assertThat(telemetry2.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85);
});
String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s",
@@ -1212,10 +1219,10 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
EntityRelation.CONTAINS_TYPE, device.getId().getId(), EntityType.DEVICE
);
doDelete(deleteUrl).andExpect(status().isOk());
- doDelete("/api/plugins/telemetry/ASSET/" + asset1.getId() + "/timeseries/delete?keys=temperatureComputed&deleteAllDataForKeys=true").andExpect(status().isOk());
+ doDelete("/api/plugins/telemetry/ASSET/" + asset1.getId() + "/timeseries/delete?keys=temperatureComputed,humidityComputed&deleteAllDataForKeys=true").andExpect(status().isOk());
- // Update telemetry on device
- long newTs = System.currentTimeMillis() - 300000L;
+ // Update telemetry on the device
+ long newTs = ts + 300000L;
postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":25}}", newTs));
// --- Assert propagated calculation (arguments-only mode after update) ---
@@ -1223,14 +1230,37 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
- ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed");
- ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed");
+ ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed,humidityComputed");
+ ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed,humidityComputed");
assertThat(telemetry1).isNotNull();
assertThat(telemetry2).isNotNull();
assertThat(telemetry1.get("temperatureComputed").get(0).get("value")).isEqualTo(NullNode.instance);
+ assertThat(telemetry1.get("humidityComputed").get(0).get("value")).isEqualTo(NullNode.instance);
+
assertThat(telemetry2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs));
assertThat(telemetry2.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(25);
+ // TS for humidity is not updated -> expected
+ assertThat(telemetry2.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts));
+ assertThat(telemetry2.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85);
});
+
+ Asset asset3 = createAsset("Propagated Asset 3", null);
+ EntityRelation rel3 = new EntityRelation(asset3.getId(), device.getId(), EntityRelation.CONTAINS_TYPE);
+ doPost("/api/relation", rel3).andExpect(status().isOk());
+
+ // --- Assert propagated calculation (arguments-only mode after update) ---
+ await().alias("propagation args-only to new entity after relation creation")
+ .atMost(TIMEOUT, TimeUnit.SECONDS)
+ .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
+ .untilAsserted(() -> {
+ ObjectNode telemetry = getLatestTelemetry(asset3.getId(), "temperatureComputed,humidityComputed");
+ assertThat(telemetry).isNotNull();
+ assertThat(telemetry.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs));
+ assertThat(telemetry.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(25);
+ assertThat(telemetry.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs));
+ assertThat(telemetry.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85);
+ });
+
}
@Test
@@ -1282,6 +1312,83 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
});
}
+ @Test
+ public void testCalculatedFieldWhenBatchOfTelemetrySent() throws Exception {
+ Device testDevice = createDevice("Test device", "1234567890");
+ long now = System.currentTimeMillis();
+ doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":5, \"b\":10}}", now - TimeUnit.MINUTES.toMillis(3))));
+ doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"b\":20}}", now - TimeUnit.MINUTES.toMillis(1))));
+
+ CalculatedField calculatedField = new CalculatedField();
+ calculatedField.setEntityId(testDevice.getId());
+ calculatedField.setType(CalculatedFieldType.SCRIPT);
+ calculatedField.setName("Script CF");
+ calculatedField.setDebugSettings(DebugSettings.all());
+
+ ScriptCalculatedFieldConfiguration config = new ScriptCalculatedFieldConfiguration();
+
+ ReferencedEntityKey refEntityKeyA = new ReferencedEntityKey("a", ArgumentType.TS_LATEST, null);
+ Argument argumentA = new Argument();
+ argumentA.setRefEntityKey(refEntityKeyA);
+ Argument argumentB = new Argument();
+ ReferencedEntityKey refEntityKeyB = new ReferencedEntityKey("b", ArgumentType.TS_ROLLING, null);
+ argumentB.setTimeWindow(TimeUnit.MINUTES.toMillis(10));
+ argumentB.setLimit(1000);
+ argumentB.setRefEntityKey(refEntityKeyB);
+ config.setArguments(Map.of("a", argumentA, "b", argumentB));
+ config.setExpression("""
+ return {
+ "latestA": a,
+ "avgB": b.avg
+ };
+ """);
+
+ config.setOutput(new TimeSeriesOutput());
+
+ calculatedField.setConfiguration(config);
+
+ doPost("/api/calculatedField", calculatedField, CalculatedField.class);
+
+ await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS)
+ .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
+ .untilAsserted(() -> {
+ ObjectNode result = getLatestTelemetry(testDevice.getId(), "latestA", "avgB");
+ assertThat(result).isNotNull();
+ assertThat(result.get("latestA").get(0).get("value").asText()).isEqualTo("5");
+ assertThat(result.get("avgB").get(0).get("value").asText()).isEqualTo("15.0");
+ });
+
+ doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("""
+ [{
+ "ts": %s,
+ "values": {
+ "a": 6,
+ "b": 100
+ }
+ }, {
+ "ts": %s,
+ "values": {
+ "a": 7,
+ "b": 200
+ }
+ }, {
+ "ts": %s,
+ "values": {
+ "a": 8,
+ "b": 300
+ }
+ }]""", now - TimeUnit.MINUTES.toMillis(2), now, now - TimeUnit.MINUTES.toMillis(5))));
+
+ await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS)
+ .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
+ .untilAsserted(() -> {
+ ObjectNode result = getLatestTelemetry(testDevice.getId(), "latestA", "avgB");
+ assertThat(result).isNotNull();
+ assertThat(result.get("latestA").get(0).get("value").asText()).isEqualTo("7");
+ assertThat(result.get("avgB").get(0).get("value").asText()).isEqualTo("126.0");
+ });
+ }
+
@Test
public void testSimpleCalculatedFieldWhenSkipRuleEngineOutputProcessing() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
index 80e79280df..b9c6e17ded 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
@@ -1209,7 +1209,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
Map statesMap = (Map) ReflectionTestUtils.getField(processor, "states");
Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> {
CalculatedFieldState calculatedFieldState = statesMap.get(cfId);
- boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs() <
+ boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastScheduledRefreshTs() <
System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval);
log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady);
return isReady;
@@ -1441,7 +1441,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
EntityType entityType,
List entities,
List names) throws Exception {
- return doGetTypedWithPageLink("/api/calculatedFields?type=" + type + "&" +
+ return doGetTypedWithPageLink("/api/calculatedFields?" +
+ (type != null ? "types=" + type + "&" : "") +
(entityType != null ? "entityType=" + entityType + "&" : "") +
(entities != null ? "entities=" + String.join(",",
entities.stream().map(UUID::toString).toList()) + "&" : "") +
diff --git a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java
index e160fff59e..d039d7a26d 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java
@@ -50,7 +50,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.asset.AssetDao;
-import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.service.DaoSqlTest;
diff --git a/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java
index 2691352d01..a1efe16381 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java
@@ -42,13 +42,17 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.asset.AssetProfileDao;
-import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Function;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.containsString;
@@ -195,6 +199,38 @@ public class AssetProfileControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedAssetProfile.getName(), foundAssetProfileInfo.getName());
}
+ @Test
+ public void testFindAssetProfileInfoByIds() throws Exception {
+ List assetProfiles = new ArrayList<>();
+ for (int i = 0; i < 20; i++) {
+ AssetProfile assetProfile = this.createAssetProfile("Asset Profile" + i);
+ assetProfile.setTenantId(savedTenant.getId());
+ assetProfiles.add(doPost("/api/assetProfile", assetProfile, AssetProfileInfo.class));
+ }
+
+ List expected = assetProfiles.subList(5, 15);
+
+ String idsParam = expected.stream()
+ .map(ap -> ap.getId().getId().toString())
+ .collect(Collectors.joining(","));
+ AssetProfileInfo[] foundAssetProfileInfos = doGet("/api/assetProfileInfos?assetProfileIds=" + idsParam, AssetProfileInfo[].class);
+
+ Assert.assertNotNull(foundAssetProfileInfos);
+ Assert.assertEquals(expected.size(), foundAssetProfileInfos.length);
+
+ Map infoById = Arrays.stream(foundAssetProfileInfos)
+ .collect(Collectors.toMap(info -> info.getId().getId(), Function.identity()));
+
+ for (AssetProfileInfo assetProfileInfo : expected) {
+ UUID id = assetProfileInfo.getId().getId();
+ AssetProfileInfo info = infoById.get(id);
+ Assert.assertNotNull("AssetProfileInfo not found for id " + id, info);
+
+ Assert.assertEquals(assetProfileInfo.getId(), info.getId());
+ Assert.assertEquals(assetProfileInfo.getName(), info.getName());
+ }
+ }
+
@Test
public void whenGetAssetProfileInfoById_thenPermissionsAreChecked() throws Exception {
AssetProfile assetProfile = createAssetProfile("Asset profile 1");
diff --git a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
index 33f6bfa250..cc9098d492 100644
--- a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
@@ -259,6 +259,8 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
List allCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE,
null, null, null);
assertThat(allCalculatedFields).contains(deviceCalculatedField, profileCalculatedField);
+ allCalculatedFields = getCalculatedFields(null, null, null, null);
+ assertThat(allCalculatedFields).contains(deviceCalculatedField, profileCalculatedField);
List profileLevelCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE,
EntityType.DEVICE_PROFILE, null, null);
diff --git a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
index f4e91f993e..be99343459 100644
--- a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
@@ -44,12 +44,17 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.customer.CustomerDao;
-import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.Map;
+import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
@@ -261,6 +266,40 @@ public class CustomerControllerTest extends AbstractControllerTest {
.andExpect(status().isOk());
}
+ @Test
+ public void testFindCustomersByIds() throws Exception {
+ List savedCustomers = new ArrayList<>();
+ for (int i = 0; i < 5; i++) {
+ Customer customer = new Customer();
+ customer.setTitle("My customer " + i);
+ savedCustomers.add(doPost("/api/customer", customer, Customer.class));
+ }
+
+ String idsParam = savedCustomers.stream()
+ .map(c -> c.getId().getId().toString())
+ .collect(Collectors.joining(","));
+
+ Customer[] foundCustomers = doGet("/api/customers?customerIds=" + idsParam, Customer[].class);
+
+ Assert.assertNotNull(foundCustomers);
+ Assert.assertEquals(savedCustomers.size(), foundCustomers.length);
+
+ Map foundById = Arrays.stream(foundCustomers)
+ .collect(Collectors.toMap(c -> c.getId().getId(), Function.identity()));
+
+ for (Customer savedCustomer : savedCustomers) {
+ UUID id = savedCustomer.getId().getId();
+ Customer foundCustomer = foundById.get(id);
+ Assert.assertNotNull("Customer not found for id " + id, foundCustomer);
+ Assert.assertEquals(savedCustomer, foundCustomer);
+ }
+
+ for (Customer savedCustomer : savedCustomers) {
+ doDelete("/api/customer/" + savedCustomer.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+ }
+
@Test
public void testDeleteCustomer() throws Exception {
Customer customer = new Customer();
diff --git a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java
index a73bc3befb..712b9f1472 100644
--- a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java
@@ -51,13 +51,16 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.dashboard.DashboardDao;
-import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Map;
+import java.util.UUID;
+import java.util.function.Function;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
@@ -155,6 +158,42 @@ public class DashboardControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedDashboard, foundDashboard);
}
+ @Test
+ public void testFindDashboardInfosByIds() throws Exception {
+ List dashboards = new ArrayList<>();
+ for (int i = 0; i < 20; i++) {
+ Dashboard dashboard = new Dashboard();
+ dashboard.setTitle("My dashboard " + i);
+ dashboards.add(doPost("/api/dashboard", dashboard, Dashboard.class));
+ }
+
+ List expected = dashboards.subList(5, 15);
+
+ String idsParam = expected.stream()
+ .map(d -> d.getId().getId().toString())
+ .collect(Collectors.joining(","));
+
+ DashboardInfo[] result = doGet(
+ "/api/dashboards?dashboardIds=" + idsParam,
+ DashboardInfo[].class
+ );
+
+ Assert.assertNotNull(result);
+ Assert.assertEquals(expected.size(), result.length);
+
+ Map infoById = Arrays.stream(result)
+ .collect(Collectors.toMap(info -> info.getId().getId(), Function.identity()));
+
+ for (Dashboard dashboard : expected) {
+ UUID id = dashboard.getId().getId();
+ DashboardInfo info = infoById.get(id);
+ Assert.assertNotNull("DashboardInfo not found for id " + id, info);
+
+ Assert.assertEquals(dashboard.getId(), info.getId());
+ Assert.assertEquals(dashboard.getTitle(), info.getTitle());
+ }
+ }
+
@Test
public void testDeleteDashboard() throws Exception {
Dashboard dashboard = new Dashboard();
diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
index 78679948ab..efbf18d5f5 100644
--- a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
@@ -72,7 +72,7 @@ import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumn
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult;
import org.thingsboard.server.dao.device.DeviceDao;
-import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.service.DaoSqlTest;
diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java
index 320e3815f6..970461480d 100644
--- a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java
@@ -52,13 +52,17 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.device.DeviceProfileDao;
-import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Function;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
@@ -214,6 +218,42 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedDeviceProfile.getType(), foundDeviceProfileInfo.getType());
}
+ @Test
+ public void testFindDeviceProfileInfosByIds() throws Exception {
+ List deviceProfiles = new ArrayList<>();
+ for (int i = 0; i < 20; i++) {
+ DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile " + i);
+ deviceProfiles.add(saveDeviceProfile(deviceProfile));
+ }
+
+ List expected = deviceProfiles.subList(5, 15);
+
+ String idsParam = expected.stream()
+ .map(dp -> dp.getId().getId().toString())
+ .collect(Collectors.joining(","));
+
+ DeviceProfileInfo[] result = doGet(
+ "/api/deviceProfileInfos?deviceProfileIds=" + idsParam,
+ DeviceProfileInfo[].class
+ );
+
+ Assert.assertNotNull(result);
+ Assert.assertEquals(expected.size(), result.length);
+
+ Map infoById = Arrays.stream(result)
+ .collect(Collectors.toMap(info -> info.getId().getId(), Function.identity()));
+
+ for (DeviceProfile dp : expected) {
+ UUID id = dp.getId().getId();
+ DeviceProfileInfo info = infoById.get(id);
+ Assert.assertNotNull("DeviceProfileInfo not found for id " + id, info);
+
+ Assert.assertEquals(dp.getId(), info.getId());
+ Assert.assertEquals(dp.getName(), info.getName());
+ Assert.assertEquals(dp.getType(), info.getType());
+ }
+ }
+
@Test
public void whenGetDeviceProfileInfoById_thenPermissionsAreChecked() throws Exception {
DeviceProfile deviceProfile = createDeviceProfile("Device profile 1", null);
diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java
index f1d3ef1d52..217bba43bb 100644
--- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java
@@ -72,7 +72,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.dao.edge.EdgeDao;
-import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.edge.imitator.EdgeImitator;
diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java
index 167048a969..fdff81799f 100644
--- a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java
@@ -63,16 +63,19 @@ import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.entityview.EntityViewDao;
-import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
import java.util.stream.Collectors;
import static java.util.concurrent.TimeUnit.HOURS;
@@ -148,6 +151,36 @@ public class EntityViewControllerTest extends AbstractControllerTest {
assertEquals(savedView, foundView);
}
+ @Test
+ public void testFindEntityViewByIds() throws Exception {
+ List assetProfiles = new ArrayList<>();
+ for (int i = 0; i < 20; i++) {
+ assetProfiles.add(getNewSavedEntityView("Test entity view " + i));
+ }
+
+ List expected = assetProfiles.subList(5, 15);
+
+ String idsParam = expected.stream()
+ .map(ap -> ap.getId().getId().toString())
+ .collect(Collectors.joining(","));
+ EntityView[] foundEntityViews = doGet("/api/entityViews?entityViewIds=" + idsParam, EntityView[].class);
+
+ Assert.assertNotNull(foundEntityViews);
+ Assert.assertEquals(expected.size(), foundEntityViews.length);
+
+ Map infoById = Arrays.stream(foundEntityViews)
+ .collect(Collectors.toMap(info -> info.getId().getId(), Function.identity()));
+
+ for (EntityView entityView : expected) {
+ UUID id = entityView.getId().getId();
+ EntityView view = infoById.get(id);
+ Assert.assertNotNull("Entity view not found for id " + id, view);
+
+ Assert.assertEquals(entityView.getId(), view.getId());
+ Assert.assertEquals(entityView.getName(), view.getName());
+ }
+ }
+
@Test
public void testSaveEntityView() throws Exception {
String name = "Test entity view";
diff --git a/application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java
index 3fc839996c..7df2b84c43 100644
--- a/application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java
@@ -37,7 +37,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
-import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.nio.ByteBuffer;
diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java
index a6a022347f..a02332812b 100644
--- a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java
@@ -47,12 +47,17 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.security.Authority;
-import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.rule.RuleChainDao;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
@@ -231,6 +236,44 @@ public class RuleChainControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedRuleChain, foundRuleChain);
}
+ @Test
+ public void testFindRuleChainsByIds() throws Exception {
+ List ruleChains = new ArrayList<>();
+ for (int i = 0; i < 20; i++) {
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setName("RuleChain " + i);
+ ruleChains.add(doPost("/api/ruleChain", ruleChain, RuleChain.class));
+ }
+
+ List expected = ruleChains.subList(5, 15);
+
+ String idsParam = expected.stream()
+ .map(rc -> rc.getId().getId().toString())
+ .collect(Collectors.joining(","));
+
+ RuleChain[] result = doGet(
+ "/api/ruleChains?ruleChainIds=" + idsParam,
+ RuleChain[].class
+ );
+
+ Assert.assertNotNull(result);
+ Assert.assertEquals(expected.size(), result.length);
+
+ Map rcById = Arrays.stream(result)
+ .collect(Collectors.toMap(rc -> rc.getId().getId(), Function.identity()));
+
+ for (RuleChain rc : expected) {
+ UUID id = rc.getId().getId();
+ RuleChain found = rcById.get(id);
+ Assert.assertNotNull("RuleChain not found for id " + id, found);
+
+ Assert.assertEquals(rc.getId(), found.getId());
+ Assert.assertEquals(rc.getName(), found.getName());
+ Assert.assertEquals(rc.getTenantId(), found.getTenantId());
+ }
+ }
+
+
@Test
public void testDeleteRuleChain() throws Exception {
RuleChain ruleChain = new RuleChain();
diff --git a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
index 0875551112..5e880fb957 100644
--- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
@@ -48,7 +48,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
-import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
index 6f68b97619..4b439ab4f9 100644
--- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
@@ -68,6 +68,7 @@ import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.QueueKey;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
@@ -77,6 +78,7 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
+import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -187,6 +189,42 @@ public class TenantControllerTest extends AbstractControllerTest {
deleteTenant(savedTenant.getId());
}
+ @Test
+ public void testFindTenantsByIds() throws Exception {
+ loginSysAdmin();
+
+ List savedTenants = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant " + i);
+ savedTenants.add(saveTenant(tenant));
+ }
+
+ String idsParam = savedTenants.stream()
+ .map(t -> t.getId().getId().toString())
+ .collect(Collectors.joining(","));
+
+ Tenant[] foundTenants = doGet("/api/tenants?tenantIds=" + idsParam, Tenant[].class);
+
+ Assert.assertNotNull(foundTenants);
+ Assert.assertEquals(savedTenants.size(), foundTenants.length);
+
+ Map foundById = Arrays.stream(foundTenants)
+ .collect(Collectors.toMap(t -> t.getId().getId(), Function.identity()));
+
+ for (Tenant savedTenant : savedTenants) {
+ UUID id = savedTenant.getId().getId();
+ Tenant foundTenant = foundById.get(id);
+ Assert.assertNotNull("Tenant not found for id " + id, foundTenant);
+ Assert.assertEquals(savedTenant, foundTenant);
+ }
+
+ for (Tenant savedTenant : savedTenants) {
+ deleteTenant(savedTenant.getId());
+ }
+ }
+
+
@Test
public void testFindTenantInfoById() throws Exception {
loginSysAdmin();
diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java
index 8f83f2c186..4862a00125 100644
--- a/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java
@@ -42,8 +42,12 @@ import org.thingsboard.server.queue.TbQueueCallback;
import java.lang.reflect.Field;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Function;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.containsString;
@@ -120,6 +124,43 @@ public class TenantProfileControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedTenantProfile.getName(), foundTenantProfileInfo.getName());
}
+ @Test
+ public void testFindTenantProfilesByIds() throws Exception {
+ loginSysAdmin();
+ List tenantProfiles = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile " + i);
+ tenantProfiles.add(doPost("/api/tenantProfile", tenantProfile, TenantProfile.class));
+ }
+
+ List expected = tenantProfiles.subList(2, 7);
+
+ String idsParam = expected.stream()
+ .map(tp -> tp.getId().getId().toString())
+ .collect(Collectors.joining(","));
+
+ TenantProfile[] result = doGet(
+ "/api/tenantProfiles?ids=" + idsParam,
+ TenantProfile[].class
+ );
+
+ Assert.assertNotNull(result);
+ Assert.assertEquals(expected.size(), result.length);
+
+ Map tpById = Arrays.stream(result)
+ .collect(Collectors.toMap(tp -> tp.getId().getId(), Function.identity()));
+
+ for (TenantProfile tp : expected) {
+ UUID id = tp.getId().getId();
+ TenantProfile found = tpById.get(id);
+ Assert.assertNotNull("TenantProfile not found for id " + id, found);
+
+ Assert.assertEquals(tp.getId(), found.getId());
+ Assert.assertEquals(tp.getName(), found.getName());
+ }
+ }
+
+
@Test
public void testFindDefaultTenantProfileInfo() throws Exception {
loginSysAdmin();
diff --git a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
index 28d43699fd..8fef7f3e1d 100644
--- a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
@@ -50,13 +50,17 @@ import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.settings.StarredDashboardInfo;
import org.thingsboard.server.common.data.settings.UserDashboardsInfo;
import org.thingsboard.server.dao.device.DeviceService;
-import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.user.UserDao;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Function;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
@@ -263,6 +267,37 @@ public class UserControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedUser, foundUser);
}
+ @Test
+ public void testFindUsersByIds() throws Exception {
+ loginTenantAdmin();
+ List savedUsers = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ User user = createTenantAdminUser();
+ savedUsers.add(doPost("/api/user", user, User.class));
+ }
+
+ String idsParam = savedUsers.stream()
+ .map(u -> u.getId().getId().toString())
+ .collect(Collectors.joining(","));
+
+ User[] foundUsers = doGet("/api/users?userIds=" + idsParam, User[].class);
+
+ Assert.assertNotNull(foundUsers);
+ Assert.assertEquals(savedUsers.size(), foundUsers.length);
+
+ Map foundById = Arrays.stream(foundUsers)
+ .collect(Collectors.toMap(u -> u.getId().getId(), Function.identity()));
+
+ for (User savedUser : savedUsers) {
+ User foundUser = foundById.get(savedUser.getId().getId());
+ Assert.assertNotNull("User not found for id " + savedUser.getId().getId(), foundUser);
+
+ foundUser.setAdditionalInfo(savedUser.getAdditionalInfo());
+ Assert.assertEquals(savedUser, foundUser);
+ }
+ }
+
+
@Test
public void testSaveUserWithSameEmail() throws Exception {
loginSysAdmin();
diff --git a/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java
index 3440f26ea8..f84e5eb6e7 100644
--- a/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java
@@ -29,12 +29,17 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
-import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.stream.Collectors;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -150,6 +155,37 @@ public class WidgetsBundleControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedWidgetsBundle, foundWidgetsBundle);
}
+ @Test
+ public void testFindWidgetsBundlesByIds() throws Exception {
+ List savedWidgetsBundles = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ WidgetsBundle widgetsBundle = new WidgetsBundle();
+ widgetsBundle.setTitle("My widgets bundle " + i);
+ savedWidgetsBundles.add(doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class));
+ }
+
+ String idsParam = savedWidgetsBundles.stream()
+ .map(wb -> wb.getId().getId().toString())
+ .collect(Collectors.joining(","));
+
+ WidgetsBundle[] foundWidgetsBundles =
+ doGet("/api/widgetsBundles?widgetsBundleIds=" + idsParam, WidgetsBundle[].class);
+
+ Assert.assertNotNull(foundWidgetsBundles);
+ Assert.assertEquals(savedWidgetsBundles.size(), foundWidgetsBundles.length);
+
+ Map foundById = Arrays.stream(foundWidgetsBundles)
+ .collect(Collectors.toMap(wb -> wb.getId().getId(), Function.identity()));
+
+ for (WidgetsBundle savedWidgetsBundle : savedWidgetsBundles) {
+ UUID id = savedWidgetsBundle.getId().getId();
+ WidgetsBundle foundWidgetsBundle = foundById.get(id);
+ Assert.assertNotNull("WidgetsBundle not found for id " + id, foundWidgetsBundle);
+ Assert.assertEquals(savedWidgetsBundle, foundWidgetsBundle);
+ }
+ }
+
+
@Test
public void testDeleteWidgetsBundle() throws Exception {
WidgetsBundle widgetsBundle = new WidgetsBundle();
diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
index 84d879c993..ef11a4dcf8 100644
--- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
+++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
@@ -22,6 +22,7 @@ import com.google.protobuf.AbstractMessage;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.MessageLite;
import lombok.extern.slf4j.Slf4j;
+import org.awaitility.Awaitility;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@@ -102,6 +103,7 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.UUID;
@@ -738,4 +740,27 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
return rpc;
}
+ protected void verifyEdgeDisconnected() {
+ verifyEdgeActiveFlag(false);
+ }
+
+ protected void verifyEdgeConnected() {
+ verifyEdgeActiveFlag(true);
+ }
+
+ private void verifyEdgeActiveFlag(boolean value) {
+ Awaitility.await()
+ .atMost(TIMEOUT, TimeUnit.SECONDS)
+ .until(() -> {
+ List