Browse Source

Merge branch 'master' into fix/3656-unneccesssary-params-mqtt

pull/11012/head
Max Petrov 2 years ago
committed by GitHub
parent
commit
aa17630935
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      application/pom.xml
  2. 14
      application/src/main/data/json/edge/instructions/install/centos/instructions.md
  3. 14
      application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md
  4. 5
      application/src/main/data/json/system/widget_bundles/cards.json
  5. 25
      application/src/main/data/json/system/widget_types/label___value_card.json
  6. 25
      application/src/main/data/json/system/widget_types/label_card.json
  7. 6
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  8. 65
      application/src/main/java/org/thingsboard/server/service/edge/instructions/BaseEdgeInstallUpgradeInstructionsService.java
  9. 71
      application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java
  10. 55
      application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java
  11. 29
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  12. 2
      common/actor/pom.xml
  13. 2
      common/cache/pom.xml
  14. 2
      common/cluster-api/pom.xml
  15. 2
      common/coap-server/pom.xml
  16. 2
      common/dao-api/pom.xml
  17. 2
      common/data/pom.xml
  18. 6
      common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java
  19. 29
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  20. 3
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java
  21. 2
      common/edge-api/pom.xml
  22. 2
      common/message/pom.xml
  23. 2
      common/pom.xml
  24. 2
      common/proto/pom.xml
  25. 2
      common/queue/pom.xml
  26. 2
      common/script/pom.xml
  27. 2
      common/script/remote-js-client/pom.xml
  28. 2
      common/script/script-api/pom.xml
  29. 2
      common/stats/pom.xml
  30. 2
      common/transport/coap/pom.xml
  31. 2
      common/transport/http/pom.xml
  32. 2
      common/transport/lwm2m/pom.xml
  33. 2
      common/transport/mqtt/pom.xml
  34. 2
      common/transport/pom.xml
  35. 2
      common/transport/snmp/pom.xml
  36. 2
      common/transport/transport-api/pom.xml
  37. 2
      common/util/pom.xml
  38. 2
      common/version-control/pom.xml
  39. 2
      dao/pom.xml
  40. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java
  41. 7
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  42. 52
      dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java
  43. 16
      dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java
  44. 2
      monitoring/pom.xml
  45. 91
      monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java
  46. 2
      msa/black-box-tests/pom.xml
  47. 2
      msa/js-executor/package.json
  48. 2
      msa/js-executor/pom.xml
  49. 2
      msa/monitoring/pom.xml
  50. 2
      msa/pom.xml
  51. 2
      msa/tb-node/pom.xml
  52. 4
      msa/tb/pom.xml
  53. 2
      msa/transport/coap/pom.xml
  54. 2
      msa/transport/http/pom.xml
  55. 2
      msa/transport/lwm2m/pom.xml
  56. 2
      msa/transport/mqtt/pom.xml
  57. 2
      msa/transport/pom.xml
  58. 2
      msa/transport/snmp/pom.xml
  59. 2
      msa/vc-executor-docker/pom.xml
  60. 2
      msa/vc-executor/pom.xml
  61. 2
      msa/web-ui/package.json
  62. 2
      msa/web-ui/pom.xml
  63. 4
      netty-mqtt/pom.xml
  64. 2
      pom.xml
  65. 2
      rest-client/pom.xml
  66. 2
      rule-engine/pom.xml
  67. 2
      rule-engine/rule-engine-api/pom.xml
  68. 2
      rule-engine/rule-engine-components/pom.xml
  69. 16
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java
  70. 12
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
  71. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java
  72. 273
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java
  73. 155
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgCountNodeTest.java
  74. 176
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java
  75. 263
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java
  76. 64
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java
  77. 87
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java
  78. 421
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java
  79. 249
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java
  80. 2
      tools/pom.xml
  81. 2
      transport/coap/pom.xml
  82. 2
      transport/http/pom.xml
  83. 2
      transport/lwm2m/pom.xml
  84. 2
      transport/mqtt/pom.xml
  85. 2
      transport/pom.xml
  86. 2
      transport/snmp/pom.xml
  87. 2
      ui-ngx/package.json
  88. 2
      ui-ngx/pom.xml
  89. 18
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  90. 88
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-card-basic-config.component.html
  91. 130
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-card-basic-config.component.ts
  92. 114
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.html
  93. 191
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.ts
  94. 30
      ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.html
  95. 70
      ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.scss
  96. 170
      ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.ts
  97. 60
      ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.models.ts
  98. 34
      ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.html
  99. 80
      ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.scss
  100. 218
      ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.ts

2
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>application</artifactId>

14
application/src/main/data/json/edge/instructions/install/centos/instructions.md

@ -8,15 +8,15 @@ sudo yum install -y nano wget
sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
```
#### Install Java 11 (OpenJDK)
ThingsBoard service is running on Java 11. Follow these instructions to install OpenJDK 11:
#### Install Java 17 (OpenJDK)
ThingsBoard service is running on Java 17. Follow these instructions to install OpenJDK 17:
```bash
sudo yum install java-11-openjdk
sudo yum install java-17-openjdk
{:copy-code}
```
Please don't forget to configure your operating system to use OpenJDK 11 by default.
Please don't forget to configure your operating system to use OpenJDK 17 by default.
You can configure which version is the default using the following command:
```bash
@ -34,7 +34,7 @@ java -version
Expected command output is:
```text
openjdk version "11.0.xx"
openjdk version "17.x.xx"
OpenJDK Runtime Environment (...)
OpenJDK 64-Bit Server VM (build ...)
```
@ -144,14 +144,14 @@ CREATE DATABASE tb_edge;
Download installation package:
```bash
wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_VERSION}/tb-edge-${TB_EDGE_VERSION}.rpm
wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.rpm
{:copy-code}
```
Go to the download repository and install ThingsBoard Edge service:
```bash
sudo rpm -Uvh tb-edge-${TB_EDGE_VERSION}.rpm
sudo rpm -Uvh tb-edge-${TB_EDGE_TAG}.rpm
{:copy-code}
```

14
application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md

@ -1,15 +1,15 @@
Here is the list of commands, that can be used to quickly install ThingsBoard Edge on Ubuntu Server and connect to the server.
#### Install Java 11 (OpenJDK)
ThingsBoard service is running on Java 11. Follow these instructions to install OpenJDK 11:
#### Install Java 17 (OpenJDK)
ThingsBoard service is running on Java 17. Follow these instructions to install OpenJDK 17:
```bash
sudo apt update
sudo apt install openjdk-11-jdk
sudo apt install openjdk-17-jdk
{:copy-code}
```
Please don't forget to configure your operating system to use OpenJDK 11 by default.
Please don't forget to configure your operating system to use OpenJDK 17 by default.
You can configure which version is the default using the following command:
```bash
@ -27,7 +27,7 @@ java -version
Expected command output is:
```text
openjdk version "11.0.xx"
openjdk version "17.x.xx"
OpenJDK Runtime Environment (...)
OpenJDK 64-Bit Server VM (build ...)
```
@ -76,14 +76,14 @@ CREATE DATABASE tb_edge;
Download installation package:
```bash
wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_VERSION}/tb-edge-${TB_EDGE_VERSION}.deb
wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.deb
{:copy-code}
```
Go to the download repository and install ThingsBoard Edge service:
```bash
sudo dpkg -i tb-edge-${TB_EDGE_VERSION}.deb
sudo dpkg -i tb-edge-${TB_EDGE_TAG}.deb
{:copy-code}
```

5
application/src/main/data/json/system/widget_bundles/cards.json

File diff suppressed because one or more lines are too long

25
application/src/main/data/json/system/widget_types/label___value_card.json

File diff suppressed because one or more lines are too long

25
application/src/main/data/json/system/widget_types/label_card.json

File diff suppressed because one or more lines are too long

6
application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java

@ -118,8 +118,8 @@ public class TenantProfileController extends BaseController {
"Let's review the example of tenant profile data below: " +
"\n\n" + MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"name\": \"Default\",\n" +
" \"description\": \"Default tenant profile\",\n" +
" \"name\": \"Your name\",\n" +
" \"description\": \"Your description\",\n" +
" \"isolatedTbRuleEngine\": false,\n" +
" \"profileData\": {\n" +
" \"configuration\": {\n" +
@ -162,7 +162,7 @@ public class TenantProfileController extends BaseController {
" \"warnThreshold\": 0\n" +
" }\n" +
" },\n" +
" \"default\": true\n" +
" \"default\": false\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"Remove 'id', from the request body example (below) to create new Tenant Profile entity." +

65
application/src/main/java/org/thingsboard/server/service/edge/instructions/BaseEdgeInstallUpgradeInstructionsService.java

@ -0,0 +1,65 @@
/**
* Copyright © 2016-2024 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.service.edge.instructions;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.server.service.install.InstallScripts;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Slf4j
@RequiredArgsConstructor
public abstract class BaseEdgeInstallUpgradeInstructionsService {
private static final String EDGE_DIR = "edge";
private static final String INSTRUCTIONS_DIR = "instructions";
private final InstallScripts installScripts;
@Value("${app.version:unknown}")
@Setter
protected String appVersion;
protected String readFile(Path file) {
try {
return Files.readString(file);
} catch (IOException e) {
log.warn("Failed to read file: {}", file, e);
throw new RuntimeException(e);
}
}
protected String getTagVersion(String version) {
return version.endsWith(".0") ? version.substring(0, version.length() - 2) : version;
}
protected Path resolveFile(String subDir, String... subDirs) {
return getEdgeInstructionsDir().resolve(Paths.get(subDir, subDirs));
}
protected Path getEdgeInstructionsDir() {
return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, INSTRUCTIONS_DIR, getBaseDirName());
}
protected abstract String getBaseDirName();
}

71
application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java

@ -15,8 +15,7 @@
*/
package org.thingsboard.server.service.edge.instructions;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -27,47 +26,32 @@ import org.thingsboard.server.dao.util.DeviceConnectivityUtil;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.install.InstallScripts;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Service
@Slf4j
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true")
@TbCoreComponent
public class DefaultEdgeInstallInstructionsService implements EdgeInstallInstructionsService {
public class DefaultEdgeInstallInstructionsService extends BaseEdgeInstallUpgradeInstructionsService implements EdgeInstallInstructionsService {
private static final String EDGE_DIR = "edge";
private static final String INSTRUCTIONS_DIR = "instructions";
private static final String INSTALL_DIR = "install";
private final InstallScripts installScripts;
@Value("${edges.rpc.port}")
private int rpcPort;
@Value("${edges.rpc.ssl.enabled}")
private boolean sslEnabled;
@Value("${app.version:unknown}")
@Setter
private String appVersion;
public DefaultEdgeInstallInstructionsService(InstallScripts installScripts) {
super(installScripts);
}
@Override
public EdgeInstructions getInstallInstructions(Edge edge, String installationMethod, HttpServletRequest request) {
switch (installationMethod.toLowerCase()) {
case "docker":
return getDockerInstallInstructions(edge, request);
case "ubuntu":
return getUbuntuInstallInstructions(edge, request);
case "centos":
return getCentosInstallInstructions(edge, request);
default:
throw new IllegalArgumentException("Unsupported installation method for Edge: " + installationMethod);
}
return switch (installationMethod.toLowerCase()) {
case "docker" -> getDockerInstallInstructions(edge, request);
case "ubuntu", "centos" -> getLinuxInstallInstructions(edge, request, installationMethod.toLowerCase());
default ->
throw new IllegalArgumentException("Unsupported installation method for Edge: " + installationMethod);
};
}
private EdgeInstructions getDockerInstallInstructions(Edge edge, HttpServletRequest request) {
@ -88,25 +72,16 @@ public class DefaultEdgeInstallInstructionsService implements EdgeInstallInstruc
return new EdgeInstructions(dockerInstallInstructions);
}
private EdgeInstructions getUbuntuInstallInstructions(Edge edge, HttpServletRequest request) {
String ubuntuInstallInstructions = readFile(resolveFile("ubuntu", "instructions.md"));
private EdgeInstructions getLinuxInstallInstructions(Edge edge, HttpServletRequest request, String os) {
String ubuntuInstallInstructions = readFile(resolveFile(os, "instructions.md"));
ubuntuInstallInstructions = replacePlaceholders(ubuntuInstallInstructions, edge);
ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${BASE_URL}", request.getServerName());
String edgeVersion = appVersion.replace("-SNAPSHOT", "");
ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion);
ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_TAG}", getTagVersion(edgeVersion));
return new EdgeInstructions(ubuntuInstallInstructions);
}
private EdgeInstructions getCentosInstallInstructions(Edge edge, HttpServletRequest request) {
String centosInstallInstructions = readFile(resolveFile("centos", "instructions.md"));
centosInstallInstructions = replacePlaceholders(centosInstallInstructions, edge);
centosInstallInstructions = centosInstallInstructions.replace("${BASE_URL}", request.getServerName());
String edgeVersion = appVersion.replace("-SNAPSHOT", "");
centosInstallInstructions = centosInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion);
return new EdgeInstructions(centosInstallInstructions);
}
private String replacePlaceholders(String instructions, Edge edge) {
instructions = instructions.replace("${CLOUD_ROUTING_KEY}", edge.getRoutingKey());
instructions = instructions.replace("${CLOUD_ROUTING_SECRET}", edge.getSecret());
@ -115,20 +90,8 @@ public class DefaultEdgeInstallInstructionsService implements EdgeInstallInstruc
return instructions;
}
private String readFile(Path file) {
try {
return Files.readString(file);
} catch (IOException e) {
log.warn("Failed to read file: {}", file, e);
throw new RuntimeException(e);
}
}
private Path resolveFile(String subDir, String... subDirs) {
return getEdgeInstallInstructionsDir().resolve(Paths.get(subDir, subDirs));
}
private Path getEdgeInstallInstructionsDir() {
return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, INSTRUCTIONS_DIR, INSTALL_DIR);
@Override
protected String getBaseDirName() {
return INSTALL_DIR;
}
}

55
application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.service.edge.instructions;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@ -32,47 +31,37 @@ import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.install.InstallScripts;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Service
@Slf4j
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true")
@TbCoreComponent
public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstructionsService {
public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgradeInstructionsService implements EdgeUpgradeInstructionsService {
private static final Map<String, EdgeUpgradeInfo> upgradeVersionHashMap = new HashMap<>();
private static final String EDGE_DIR = "edge";
private static final String INSTRUCTIONS_DIR = "instructions";
private static final String UPGRADE_DIR = "upgrade";
private final InstallScripts installScripts;
private final AttributesService attributesService;
@Value("${app.version:unknown}")
@Setter
private String appVersion;
public DefaultEdgeUpgradeInstructionsService(AttributesService attributesService, InstallScripts installScripts) {
super(installScripts);
this.attributesService = attributesService;
}
@Override
public EdgeInstructions getUpgradeInstructions(String edgeVersion, String upgradeMethod) {
String tbVersion = appVersion.replace("-SNAPSHOT", "");
String currentEdgeVersion = convertEdgeVersionToDocsFormat(edgeVersion);
switch (upgradeMethod.toLowerCase()) {
case "docker":
return getDockerUpgradeInstructions(tbVersion, currentEdgeVersion);
case "ubuntu":
case "centos":
return getLinuxUpgradeInstructions(tbVersion, currentEdgeVersion, upgradeMethod.toLowerCase());
default:
throw new IllegalArgumentException("Unsupported upgrade method for Edge: " + upgradeMethod);
}
return switch (upgradeMethod.toLowerCase()) {
case "docker" -> getDockerUpgradeInstructions(tbVersion, currentEdgeVersion);
case "ubuntu", "centos" ->
getLinuxUpgradeInstructions(tbVersion, currentEdgeVersion, upgradeMethod.toLowerCase());
default -> throw new IllegalArgumentException("Unsupported upgrade method for Edge: " + upgradeMethod);
};
}
@Override
@ -167,28 +156,12 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc
return new EdgeInstructions(result.toString());
}
private String getTagVersion(String version) {
return version.endsWith(".0") ? version.substring(0, version.length() - 2) : version;
}
private String convertEdgeVersionToDocsFormat(String edgeVersion) {
return edgeVersion.replace("_", ".").substring(2);
}
private String readFile(Path file) {
try {
return Files.readString(file);
} catch (IOException e) {
log.warn("Failed to read file: {}", file, e);
throw new RuntimeException(e);
}
}
private Path resolveFile(String subDir, String... subDirs) {
return getEdgeInstallInstructionsDir().resolve(Paths.get(subDir, subDirs));
}
private Path getEdgeInstallInstructionsDir() {
return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, INSTRUCTIONS_DIR, UPGRADE_DIR);
@Override
protected String getBaseDirName() {
return UPGRADE_DIR;
}
}

29
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.service.subscription;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
@ -24,9 +26,9 @@ import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.exception.TenantNotFoundException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation;
@ -51,8 +53,6 @@ import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscript
import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@ -143,7 +143,28 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
* Even if we cache locally the list of active subscriptions by entity id, it is still time-consuming operation to get them from cache
* Since number of subscriptions is usually much less than number of devices that are pushing data.
*/
subscriptionsByEntityId.values().forEach(sub -> pushSubEventToManagerService(sub.getTenantId(), sub.getEntityId(), sub.toEvent(ComponentLifecycleEvent.UPDATED)));
Set<UUID> staleSubs = new HashSet<>();
subscriptionsByEntityId.forEach((id, sub) -> {
try {
pushSubEventToManagerService(sub.getTenantId(), sub.getEntityId(), sub.toEvent(ComponentLifecycleEvent.UPDATED));
} catch (TenantNotFoundException e) {
staleSubs.add(id);
log.warn("Cleaning up stale subscription {} for tenant {} due to TenantNotFoundException", id, sub.getTenantId());
} catch (Exception e) {
log.error("Failed to push subscription {} to manager service", sub, e);
}
});
if (!staleSubs.isEmpty()) {
subsLock.lock();
try {
staleSubs.forEach(entityId -> {
subscriptionsByEntityId.remove(entityId);
entityUpdates.remove(entityId);
});
} finally {
subsLock.unlock();
}
}
}
}

2
common/actor/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/cache/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/cluster-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/coap-server/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/dao-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/data/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

6
common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.media.Schema;
@ -51,10 +52,11 @@ public class TenantProfile extends BaseData<TenantProfileId> implements HasName
@NoXss
@Schema(description = "Description of the tenant profile", example = "Any text")
private String description;
@Schema(description = "Default Tenant profile to be used.", example = "true")
@Schema(description = "Default Tenant profile to be used.", example = "false")
@JsonProperty("default")
private boolean isDefault;
@Schema(description = "If enabled, will push all messages related to this tenant and processed by the rule engine into separate queue. " +
"Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "true")
"Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "false")
private boolean isolatedTbRuleEngine;
@Schema(description = "Complex JSON object that contains profile settings: queue configs, max devices, max assets, rate limits, etc.")
private transient TenantProfileData profileData;

29
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data.tenant.profile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -23,6 +24,7 @@ import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.TenantProfileType;
@Schema
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ -41,34 +43,61 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private long maxOtaPackagesInBytes;
private long maxResourceSize;
@Schema(example = "1000:1,20000:60")
private String transportTenantMsgRateLimit;
@Schema(example = "1000:1,20000:60")
private String transportTenantTelemetryMsgRateLimit;
@Schema(example = "1000:1,20000:60")
private String transportTenantTelemetryDataPointsRateLimit;
@Schema(example = "20:1,600:60")
private String transportDeviceMsgRateLimit;
@Schema(example = "20:1,600:60")
private String transportDeviceTelemetryMsgRateLimit;
@Schema(example = "20:1,600:60")
private String transportDeviceTelemetryDataPointsRateLimit;
@Schema(example = "20:1,600:60")
private String transportGatewayMsgRateLimit;
@Schema(example = "20:1,600:60")
private String transportGatewayTelemetryMsgRateLimit;
@Schema(example = "20:1,600:60")
private String transportGatewayTelemetryDataPointsRateLimit;
@Schema(example = "20:1,600:60")
private String transportGatewayDeviceMsgRateLimit;
@Schema(example = "20:1,600:60")
private String transportGatewayDeviceTelemetryMsgRateLimit;
@Schema(example = "20:1,600:60")
private String transportGatewayDeviceTelemetryDataPointsRateLimit;
@Schema(example = "20:1,600:60")
private String tenantEntityExportRateLimit;
@Schema(example = "20:1,600:60")
private String tenantEntityImportRateLimit;
@Schema(example = "20:1,600:60")
private String tenantNotificationRequestsRateLimit;
@Schema(example = "20:1,600:60")
private String tenantNotificationRequestsPerRuleRateLimit;
@Schema(example = "10000000")
private long maxTransportMessages;
@Schema(example = "10000000")
private long maxTransportDataPoints;
@Schema(example = "4000000")
private long maxREExecutions;
@Schema(example = "5000000")
private long maxJSExecutions;
@Schema(example = "5000000")
private long maxTbelExecutions;
@Schema(example = "0")
private long maxDPStorageDays;
@Schema(example = "50")
private int maxRuleNodeExecutionsPerMessage;
@Schema(example = "0")
private long maxEmails;
@Schema(example = "true")
private Boolean smsEnabled;
@Schema(example = "0")
private long maxSms;
@Schema(example = "1000")
private long maxCreatedAlarms;
private String tenantServerRestLimitsConfiguration;

3
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java

@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.swagger.v3.oas.annotations.media.DiscriminatorMapping;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.TenantProfileType;
@ -31,6 +33,7 @@ import java.io.Serializable;
property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = DefaultTenantProfileConfiguration.class, name = "DEFAULT")})
@Schema(discriminatorProperty = "type", discriminatorMapping = {@DiscriminatorMapping(value = "DEFAULT", schema = DefaultTenantProfileConfiguration.class)})
public interface TenantProfileConfiguration extends Serializable {
@JsonIgnore

2
common/edge-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/message/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>common</artifactId>

2
common/proto/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/queue/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/script/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/script/remote-js-client/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>script</artifactId>
</parent>
<groupId>org.thingsboard.common.script</groupId>

2
common/script/script-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>script</artifactId>
</parent>
<groupId>org.thingsboard.common.script</groupId>

2
common/stats/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/lwm2m/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/transport/snmp/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard.common</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>

2
common/transport/transport-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/util/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/version-control/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
dao/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>dao</artifactId>

7
dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.sql.rule;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
@ -51,9 +52,9 @@ public interface RuleNodeRepository extends JpaRepository<RuleNodeEntity, UUID>
Pageable pageable);
@Query("SELECT r.id FROM RuleNodeEntity r WHERE r.type = :ruleType AND r.configurationVersion < :version")
Page<UUID> findAllRuleNodeIdsByTypeAndVersionLessThan(@Param("ruleType") String ruleType,
@Param("version") int version,
Pageable pageable);
Slice<UUID> findAllRuleNodeIdsByTypeAndVersionLessThan(@Param("ruleType") String ruleType,
@Param("version") int version,
Pageable pageable);
List<RuleNodeEntity> findRuleNodesByRuleChainIdAndExternalIdIn(UUID ruleChainId, List<UUID> externalIds);

7
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java

@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntryAggWrapper;
import org.thingsboard.server.common.data.kv.TsKvQuery;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.nosql.TbResultSet;
import org.thingsboard.server.dao.nosql.TbResultSetFuture;
@ -239,10 +240,8 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
long minPartition = toPartitionTs(query.getStartTs());
long maxPartition = toPartitionTs(query.getEndTs());
TbResultSetFuture partitionsFuture = fetchPartitions(tenantId, entityId, query.getKey(), minPartition, maxPartition);
final SimpleListenableFuture<Void> resultFuture = new SimpleListenableFuture<>();
final ListenableFuture<List<Long>> partitionsListFuture = Futures.transformAsync(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor);
final ListenableFuture<List<Long>> partitionsListFuture = getPartitionsFuture(tenantId, query, entityId, minPartition, maxPartition);
Futures.addCallback(partitionsListFuture, new FutureCallback<List<Long>>() {
@Override
@ -416,7 +415,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
readResultsProcessingExecutor);
}
private ListenableFuture<List<Long>> getPartitionsFuture(TenantId tenantId, ReadTsKvQuery query, EntityId entityId, long minPartition, long maxPartition) {
private ListenableFuture<List<Long>> getPartitionsFuture(TenantId tenantId, TsKvQuery query, EntityId entityId, long minPartition, long maxPartition) {
if (isFixedPartitioning()) { //no need to fetch partitions from DB
return Futures.immediateFuture(FIXED_PARTITION);
}

52
dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java

@ -45,6 +45,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.verify;
@ -73,6 +74,9 @@ class DeviceProfileDataValidatorTest {
" \"clientOnlyObserveAfterConnect\": 1\n" +
" }";
private static final String msgErrorLwm2mRange = "LwM2M Server ShortServerId must be in range [1 - 65534]!";
private static final String msgErrorBsRange = "Bootstrap Server ShortServerId must be in range [0 - 65535]!";
private static final String msgErrorNotNull = " Server ShortServerId must not be null!";
private static final String host = "localhost";
private static final String hostBs = "localhost";
@ -116,19 +120,50 @@ class DeviceProfileDataValidatorTest {
validator.validateDataImpl(tenantId, deviceProfile);
verify(validator).validateString("Device profile name", deviceProfile.getName());
}
@Test
void testValidateDeviceProfile_Lwm2mBootstrap_ShortServerId_Ok() {
Integer shortServerId = 123;
Integer shortServerIdBs = 0;
Lwm2mDeviceProfileTransportConfiguration transportConfiguration =
getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(shortServerId, shortServerIdBs));
DeviceProfile deviceProfile = getDeviceProfile(transportConfiguration);
DeviceProfile deviceProfile = getDeviceProfile(shortServerId, shortServerIdBs);
validator.validateDataImpl(tenantId, deviceProfile);
verify(validator).validateString("Device profile name", deviceProfile.getName());
}
private DeviceProfile getDeviceProfile(Lwm2mDeviceProfileTransportConfiguration transportConfiguration) {
@Test
void testValidateDeviceProfile_Lwm2mShortServerId_Ok_BootstrapShortServerId_null_Error() {
verifyValidationError(123, null, "Bootstrap" + msgErrorNotNull);
}
@Test
void testValidateDeviceProfile_Lwm2mShortServerId_Ok_BootstrapShortServerId_More_65535_Error() {
verifyValidationError(123, 65536, msgErrorBsRange);
}
@Test
void testValidateDeviceProfile_Lwm2mShortServerId_Ok_BootstrapShortServerId_Less_0_Error() {
verifyValidationError(123, -1, msgErrorBsRange);
}
@Test
void testValidateDeviceProfile_Lwm2mShortServerId_null_Error_BootstrapShortServerId_Ok() {
verifyValidationError(null, 1, "LwM2M" + msgErrorNotNull);
}
@Test
void testValidateDeviceProfile_Lwm2mShortServerId_More_65534_Error_BootstrapShortServerId_Ok() {
verifyValidationError(65535, 111, msgErrorLwm2mRange);
}
@Test
void testValidateDeviceProfile_Lwm2mShortServerId_Less_1_Error_BootstrapShortServerId_Ok() {
verifyValidationError(0, 111, msgErrorLwm2mRange);
}
private DeviceProfile getDeviceProfile(Integer shortServerId, Integer shortServerIdBs) {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration =
getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(shortServerId, shortServerIdBs));
DeviceProfile deviceProfile = new DeviceProfile();
deviceProfile.setName("default");
deviceProfile.setType(DeviceProfileType.DEFAULT);
@ -151,7 +186,7 @@ class DeviceProfileDataValidatorTest {
return transportConfiguration;
}
private List<LwM2MBootstrapServerCredential> getBootstrapServerCredentialsNoSec(Integer shortServerId, Integer shortServerIdBs){
private List<LwM2MBootstrapServerCredential> getBootstrapServerCredentialsNoSec(Integer shortServerId, Integer shortServerIdBs) {
List<LwM2MBootstrapServerCredential> bootstrap = new ArrayList<>();
bootstrap.add(getBootstrapServerCredentialNoSec(false, shortServerId, shortServerIdBs));
bootstrap.add(getBootstrapServerCredentialNoSec(true, shortServerId, shortServerIdBs));
@ -168,4 +203,11 @@ class DeviceProfileDataValidatorTest {
return bootstrapServerCredential;
}
private void verifyValidationError(Integer shortServerId, Integer shortServerIdBs, String msgError) {
DeviceProfile deviceProfile = getDeviceProfile(shortServerId, shortServerIdBs);
assertThatThrownBy(() -> validator.validateDataImpl(tenantId, deviceProfile))
.hasMessageContaining(msgError);
}
}

16
dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java

@ -22,7 +22,6 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
@ -127,20 +126,15 @@ public class JpaRuleNodeDaoTest extends AbstractJpaDaoTest {
@Test
public void testFindRuleNodeIdsByTypeAndVersionLessThan() {
// test - search text ignored
PageData<RuleNodeId> ruleNodeIds = ruleNodeDao.findAllRuleNodeIdsByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0, PREFIX_FOR_RULE_NODE_NAME));
assertEquals(20, ruleNodeIds.getTotalElements());
assertEquals(2, ruleNodeIds.getTotalPages());
assertEquals(0, ruleNodeIds.getTotalElements()); // due to DaoUtil.pageToPageData impl for Slice
assertEquals(0, ruleNodeIds.getTotalPages()); // due to DaoUtil.pageToPageData impl for Slice
assertEquals(10, ruleNodeIds.getData().size());
ruleNodeIds = ruleNodeDao.findAllRuleNodeIdsByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0));
assertEquals(20, ruleNodeIds.getTotalElements());
assertEquals(2, ruleNodeIds.getTotalPages());
assertEquals(10, ruleNodeIds.getData().size());
// test - search text ignored
ruleNodeIds = ruleNodeDao.findAllRuleNodeIdsByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0, StringUtils.randomAlphabetic(5)));
assertEquals(20, ruleNodeIds.getTotalElements());
assertEquals(2, ruleNodeIds.getTotalPages());
assertEquals(0, ruleNodeIds.getTotalElements()); // due to DaoUtil.pageToPageData impl for Slice
assertEquals(0, ruleNodeIds.getTotalPages()); // due to DaoUtil.pageToPageData impl for Slice
assertEquals(10, ruleNodeIds.getData().size());
}

2
monitoring/pom.xml

@ -21,7 +21,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>

91
monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java

@ -30,10 +30,12 @@ import org.eclipse.leshan.client.californium.LeshanClientBuilder;
import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory;
import org.eclipse.leshan.client.object.Security;
import org.eclipse.leshan.client.object.Server;
import org.eclipse.leshan.client.observer.LwM2mClientObserver;
import org.eclipse.leshan.client.resource.BaseInstanceEnabler;
import org.eclipse.leshan.client.resource.DummyInstanceEnabler;
import org.eclipse.leshan.client.resource.ObjectsInitializer;
import org.eclipse.leshan.client.servers.ServerIdentity;
import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.californium.EndpointFactory;
import org.eclipse.leshan.core.model.InvalidDDFFileException;
import org.eclipse.leshan.core.model.LwM2mModel;
@ -42,6 +44,10 @@ import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.StaticModel;
import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder;
import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder;
import org.eclipse.leshan.core.request.BootstrapRequest;
import org.eclipse.leshan.core.request.DeregisterRequest;
import org.eclipse.leshan.core.request.RegisterRequest;
import org.eclipse.leshan.core.request.UpdateRequest;
import org.eclipse.leshan.core.response.ReadResponse;
import org.thingsboard.monitoring.util.ResourceUtils;
@ -96,7 +102,7 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable {
LwM2mModel model = new StaticModel(models);
ObjectsInitializer initializer = new ObjectsInitializer(model);
initializer.setInstancesForObject(SECURITY, security);
initializer.setInstancesForObject(SERVER, new Server(123, TimeUnit.MINUTES.toSeconds(60)));
initializer.setInstancesForObject(SERVER, new Server(123, TimeUnit.MINUTES.toSeconds(5)));
initializer.setInstancesForObject(DEVICE, this);
initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class);
DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder();
@ -139,6 +145,89 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable {
builder.setEncoder(new DefaultLwM2mEncoder(false));
leshanClient = builder.build();
LwM2mClientObserver observer = new LwM2mClientObserver() {
@Override
public void onBootstrapStarted(ServerIdentity bsserver, BootstrapRequest request) {}
@Override
public void onBootstrapSuccess(ServerIdentity bsserver, BootstrapRequest request) {}
@Override
public void onBootstrapFailure(ServerIdentity bsserver, BootstrapRequest request,
ResponseCode responseCode, String errorMessage, Exception cause) {}
@Override
public void onBootstrapTimeout(ServerIdentity bsserver, BootstrapRequest request) {}
@Override
public void onRegistrationStarted(ServerIdentity server, RegisterRequest request) {
log.debug("onRegistrationStarted [{}]", request.getEndpointName());
}
@Override
public void onRegistrationSuccess(ServerIdentity server, RegisterRequest request, String registrationID) {
log.debug("onRegistrationSuccess [{}] [{}]", request.getEndpointName(), registrationID);
}
@Override
public void onRegistrationFailure(ServerIdentity server, RegisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
log.debug("onRegistrationFailure [{}] [{}] [{}]", request.getEndpointName(), responseCode, errorMessage);
}
@Override
public void onRegistrationTimeout(ServerIdentity server, RegisterRequest request) {
log.debug("onRegistrationTimeout [{}]", request.getEndpointName());
}
@Override
public void onUpdateStarted(ServerIdentity server, UpdateRequest request) {
log.debug("onUpdateStarted [{}]", request.getRegistrationId());
}
@Override
public void onUpdateSuccess(ServerIdentity server, UpdateRequest request) {
log.debug("onUpdateSuccess [{}]", request.getRegistrationId());
}
@Override
public void onUpdateFailure(ServerIdentity server, UpdateRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
log.debug("onUpdateFailure [{}]", request.getRegistrationId());
}
@Override
public void onUpdateTimeout(ServerIdentity server, UpdateRequest request) {
log.debug("onUpdateTimeout [{}]", request.getRegistrationId());
}
@Override
public void onDeregistrationStarted(ServerIdentity server, DeregisterRequest request) {
log.debug("onDeregistrationStarted [{}]", request.getRegistrationId());
}
@Override
public void onDeregistrationSuccess(ServerIdentity server, DeregisterRequest request) {
log.debug("onDeregistrationStarted [{}]", request.getRegistrationId());
}
@Override
public void onDeregistrationFailure(ServerIdentity server, DeregisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
log.debug("onDeregistrationFailure [{}] [{}] [{}]", request.getRegistrationId(), responseCode, errorMessage);
}
@Override
public void onDeregistrationTimeout(ServerIdentity server, DeregisterRequest request) {
log.debug("onDeregistrationTimeout [{}]", request.getRegistrationId());
}
@Override
public void onUnexpectedError(Throwable unexpectedError) {
log.debug("onUnexpectedError [{}]", unexpectedError.toString());
}
};
leshanClient.addObserver(observer);
setLeshanClient(leshanClient);
leshanClient.start();

2
msa/black-box-tests/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/js-executor/package.json

@ -1,7 +1,7 @@
{
"name": "thingsboard-js-executor",
"private": true,
"version": "3.7.0",
"version": "3.7.1",
"description": "ThingsBoard JavaScript Executor Microservice",
"main": "server.ts",
"bin": "server.js",

2
msa/js-executor/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/monitoring/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>

2
msa/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>msa</artifactId>

2
msa/tb-node/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

4
msa/tb/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>
@ -38,7 +38,7 @@
<tb-postgres.docker.name>tb-postgres</tb-postgres.docker.name>
<tb-cassandra.docker.name>tb-cassandra</tb-cassandra.docker.name>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
<pkg.upgradeVersion>3.6.4</pkg.upgradeVersion>
<pkg.upgradeVersion>3.7.0</pkg.upgradeVersion>
</properties>
<dependencies>

2
msa/transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/lwm2m/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/transport/snmp/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard.msa</groupId>
<artifactId>transport</artifactId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/vc-executor-docker/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/vc-executor/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/web-ui/package.json

@ -1,7 +1,7 @@
{
"name": "thingsboard-web-ui",
"private": true,
"version": "3.7.0",
"version": "3.7.1",
"description": "ThingsBoard Web UI Microservice",
"main": "server.ts",
"bin": "server.js",

2
msa/web-ui/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

4
netty-mqtt/pom.xml

@ -19,11 +19,11 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>netty-mqtt</artifactId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Netty MQTT Client</name>

2
pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.thingsboard</groupId>
<artifactId>thingsboard</artifactId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Thingsboard</name>

2
rest-client/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>rest-client</artifactId>

2
rule-engine/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>rule-engine</artifactId>

2
rule-engine/rule-engine-api/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>rule-engine</artifactId>
</parent>
<groupId>org.thingsboard.rule-engine</groupId>

2
rule-engine/rule-engine-components/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>rule-engine</artifactId>
</parent>
<groupId>org.thingsboard.rule-engine</groupId>

16
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java

@ -35,8 +35,6 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.concurrent.ExecutionException;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
@ -81,34 +79,34 @@ public class TbSnsNode extends TbAbstractExternalNode {
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
public void onMsg(TbContext ctx, TbMsg msg) {
var tbMsg = ackIfNeeded(ctx, msg);
withCallback(publishMessageAsync(ctx, tbMsg),
m -> tellSuccess(ctx, m),
t -> tellFailure(ctx, processException(ctx, tbMsg, t), t));
t -> tellFailure(ctx, processException(tbMsg, t), t));
}
private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {
return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(ctx, msg));
return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(msg));
}
private TbMsg publishMessage(TbContext ctx, TbMsg msg) {
private TbMsg publishMessage(TbMsg msg) {
String topicArn = TbNodeUtils.processPattern(this.config.getTopicArnPattern(), msg);
PublishRequest publishRequest = new PublishRequest()
.withTopicArn(topicArn)
.withMessage(msg.getData());
PublishResult result = this.snsClient.publish(publishRequest);
return processPublishResult(ctx, msg, result);
return processPublishResult(msg, result);
}
private TbMsg processPublishResult(TbContext ctx, TbMsg origMsg, PublishResult result) {
private TbMsg processPublishResult(TbMsg origMsg, PublishResult result) {
TbMsgMetaData metaData = origMsg.getMetaData().copy();
metaData.putValue(MESSAGE_ID, result.getMessageId());
metaData.putValue(REQUEST_ID, result.getSdkResponseMetadata().getRequestId());
return TbMsg.transformMsgMetadata(origMsg, metaData);
}
private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) {
private TbMsg processException(TbMsg origMsg, Throwable t) {
TbMsgMetaData metaData = origMsg.getMetaData().copy();
metaData.putValue(ERROR, t.getClass() + ": " + t.getMessage());
return TbMsg.transformMsgMetadata(origMsg, metaData);

12
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java

@ -28,12 +28,14 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.List;
@Slf4j
@RuleNode(
type = ComponentType.EXTERNAL,
name = "rest api call",
configClazz = TbRestApiCallNodeConfiguration.class,
version = 1,
version = 2,
nodeDescription = "Invoke REST API calls to external REST server",
nodeDetails = "Will invoke REST API call <code>GET | POST | PUT | DELETE</code> to external REST server. " +
"Message payload added into Request body. Configured attributes can be added into Headers from Message Metadata." +
@ -58,9 +60,6 @@ public class TbRestApiCallNode extends TbAbstractExternalNode {
super.init(ctx);
TbRestApiCallNodeConfiguration config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class);
httpClient = new TbHttpClient(config, ctx.getSharedEventLoop());
if (config.isUseRedisQueueForMsgPersistence()) {
log.warn("[{}][{}] Usage of Redis Template is deprecated starting 2.5 and will have no affect", ctx.getTenantId(), ctx.getSelfId());
}
}
@Override
@ -88,6 +87,11 @@ public class TbRestApiCallNode extends TbAbstractExternalNode {
((ObjectNode) oldConfiguration).put(PARSE_TO_PLAIN_TEXT, oldConfiguration.get(TRIM_DOUBLE_QUOTES).booleanValue());
((ObjectNode) oldConfiguration).remove(TRIM_DOUBLE_QUOTES);
}
case 1:
if (oldConfiguration.has("useRedisQueueForMsgPersistence")) {
hasChanges = true;
((ObjectNode) oldConfiguration).remove(List.of("useRedisQueueForMsgPersistence", "trimQueue", "maxQueueSize"));
}
break;
default:
break;

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java

@ -36,7 +36,6 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
private boolean useSimpleClientHttpFactory;
private int readTimeoutMs;
private int maxParallelRequestsCount;
private boolean useRedisQueueForMsgPersistence;
private boolean parseToPlainText;
private boolean enableProxy;
private boolean useSystemProxyProperties;
@ -57,7 +56,6 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
configuration.setUseSimpleClientHttpFactory(false);
configuration.setReadTimeoutMs(0);
configuration.setMaxParallelRequestsCount(0);
configuration.setUseRedisQueueForMsgPersistence(false);
configuration.setParseToPlainText(false);
configuration.setEnableProxy(false);
configuration.setCredentials(new AnonymousCredentials());

273
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNodeTest.java

@ -0,0 +1,273 @@
/**
* Copyright © 2016-2024 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.rule.engine.action;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.objects.AttributesEntityView;
import org.thingsboard.server.common.data.objects.TelemetryEntityView;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.entityview.EntityViewService;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT;
import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED;
import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED;
import static org.thingsboard.server.common.data.msg.TbMsgType.INACTIVITY_EVENT;
import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST;
@ExtendWith(MockitoExtension.class)
public class TbCopyAttributesToEntityViewNodeTest {
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("9fdb1f05-dc66-4960-9263-ae195f1b4533"));
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("1d453dc9-9333-476a-a51f-093cf2176e59"));
private final EntityViewId ENTITY_VIEW_ID = new EntityViewId(UUID.fromString("65636806-453d-4bb4-b513-92b833970753"));
private final AttributesEntityView CLIENT_ATTRIBUTES = new AttributesEntityView(List.of("clientAttribute1"), Collections.emptyList(), Collections.emptyList());
private final AttributesEntityView SERVER_ATTRIBUTES = new AttributesEntityView(Collections.emptyList(), List.of("serverAttribute1"), Collections.emptyList());
private final AttributesEntityView SHARED_ATTRIBUTES = new AttributesEntityView(Collections.emptyList(), Collections.emptyList(), List.of("sharedAttribute1"));
private final TelemetryEntityView CLIENT_TELEMETRY_ENTITY_VIEW = new TelemetryEntityView(Collections.emptyList(), CLIENT_ATTRIBUTES);
private final TelemetryEntityView SERVER_TELEMETRY_ENTITY_VIEW = new TelemetryEntityView(Collections.emptyList(), SERVER_ATTRIBUTES);
private final TelemetryEntityView SHARED_TELEMETRY_ENTITY_VIEW = new TelemetryEntityView(Collections.emptyList(), SHARED_ATTRIBUTES);
private final long ENTITY_VIEW_START_TS = Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli();
private final long ENTITY_VIEW_END_TS = Instant.now().plus(1, ChronoUnit.DAYS).toEpochMilli();
private TbCopyAttributesToEntityViewNode node;
@Mock
private TbContext ctxMock;
@Mock
private EntityViewService entityViewServiceMock;
@Mock
private RuleEngineTelemetryService telemetryServiceMock;
@BeforeEach
void setUp() throws TbNodeException {
node = new TbCopyAttributesToEntityViewNode();
var config = new EmptyNodeConfiguration().defaultConfiguration();
var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, configuration);
}
@Test
public void givenExistingClientAttributes_whenOnMsg_thenCopyAttributesToView() {
EntityView entityView = getEntityView(CLIENT_TELEMETRY_ENTITY_VIEW);
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID,
new TbMsgMetaData(Map.of(DataConstants.SCOPE, AttributeScope.SERVER_SCOPE.name())),
"{\"clientAttribute1\": 100, \"clientAttribute2\": \"value2\"}");
mockEntityViewLookup(entityView);
when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock);
doAnswer(invocation -> {
FutureCallback<Void> callback = invocation.getArgument(4);
callback.onSuccess(null);
return null;
}).when(telemetryServiceMock).saveAndNotify(any(), any(), any(AttributeScope.class), anyList(), any(FutureCallback.class));
TbMsg newMsg = TbMsg.newMsg(msg, msg.getQueueName(), msg.getRuleChainId(), msg.getRuleNodeId());
// TODO: use newMsg() with any(TbMsgType.class), replace in other tests as well.
doAnswer(invocation -> newMsg).when(ctxMock).newMsg(any(), any(String.class), any(), any(), any(), any());
node.onMsg(ctxMock, msg);
verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID));
ArgumentCaptor<List<AttributeKvEntry>> filteredAttributesCaptor = ArgumentCaptor.forClass(List.class);
verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), eq(ENTITY_VIEW_ID), eq(AttributeScope.CLIENT_SCOPE),
filteredAttributesCaptor.capture(), any(FutureCallback.class));
List<AttributeKvEntry> filteredAttributesCaptorValue = filteredAttributesCaptor.getValue();
assertThat(filteredAttributesCaptorValue.size()).isEqualTo(1);
assertThat(filteredAttributesCaptorValue.get(0).getKey()).isEqualTo("clientAttribute1");
assertThat(filteredAttributesCaptorValue.get(0).getValue()).isEqualTo(100L);
verify(ctxMock).ack(eq(msg));
verify(ctxMock).enqueueForTellNext(eq(newMsg), eq(TbNodeConnectionType.SUCCESS));
verifyNoMoreInteractions(ctxMock, entityViewServiceMock, telemetryServiceMock);
}
@Test
public void givenExistingServerAttributesAndMsgTypeAttributesDeleted_whenOnMsg_thenDeleteAttributesFromView() {
EntityView entityView = getEntityView(SERVER_TELEMETRY_ENTITY_VIEW);
TbMsg msg = TbMsg.newMsg(
ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of(DataConstants.SCOPE, AttributeScope.SERVER_SCOPE.name())),
"{\"attributes\": [\"serverAttribute1\"]}");
mockEntityViewLookup(entityView);
when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock);
doAnswer(invocation -> {
FutureCallback<Void> callback = invocation.getArgument(4);
callback.onSuccess(null);
return null;
}).when(telemetryServiceMock).deleteAndNotify(any(), any(), any(AttributeScope.class), anyList(), any(FutureCallback.class));
TbMsg newMsg = TbMsg.newMsg(msg, msg.getQueueName(), msg.getRuleChainId(), msg.getRuleNodeId());
doAnswer(invocation -> newMsg).when(ctxMock).newMsg(any(), any(String.class), any(), any(), any(), any());
node.onMsg(ctxMock, msg);
verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID));
ArgumentCaptor<List<String>> filteredAttributesCaptor = ArgumentCaptor.forClass(List.class);
verify(telemetryServiceMock).deleteAndNotify(eq(TENANT_ID), eq(ENTITY_VIEW_ID), eq(AttributeScope.SERVER_SCOPE), filteredAttributesCaptor.capture(), any(FutureCallback.class));
List<String> filteredAttributesCaptorValue = filteredAttributesCaptor.getValue();
assertThat(filteredAttributesCaptorValue.size()).isEqualTo(1);
assertThat(filteredAttributesCaptorValue.get(0)).isEqualTo("serverAttribute1");
verify(ctxMock).ack(eq(msg));
verify(ctxMock).enqueueForTellNext(eq(newMsg), eq(TbNodeConnectionType.SUCCESS));
verifyNoMoreInteractions(ctxMock, entityViewServiceMock, telemetryServiceMock);
}
@Test
public void givenNonMatchedSharedAttributesAndMsgTypeIsAttributesDeleted_whenOnMsg_thenNoAttributesDeleteFromView() {
EntityView entityView = getEntityView(SHARED_TELEMETRY_ENTITY_VIEW);
TbMsg msg = TbMsg.newMsg(
TbMsgType.ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of(DataConstants.SCOPE, AttributeScope.SHARED_SCOPE.name())),
"{\"attributes\": [\"anotherAttribute\"]}");
mockEntityViewLookup(entityView);
node.onMsg(ctxMock, msg);
verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID));
verify(ctxMock).ack(eq(msg));
verifyNoMoreInteractions(ctxMock, entityViewServiceMock);
}
@Test
public void givenNonMatchedAttributesAndMsgTypeIsPostAttributesRequest_whenOnMsg_thenCopyNoAttributesToView() {
EntityView entityView = getEntityView(CLIENT_TELEMETRY_ENTITY_VIEW);
TbMsg msg = TbMsg.newMsg(
TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, new TbMsgMetaData(Map.of(DataConstants.SCOPE, AttributeScope.SERVER_SCOPE.name())),
"{\"clientAttribute2\": \"value2\"}");
mockEntityViewLookup(entityView);
when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock);
doAnswer(invocation -> {
FutureCallback<Void> callback = invocation.getArgument(4);
callback.onSuccess(null);
return null;
}).when(telemetryServiceMock).saveAndNotify(any(), any(), any(AttributeScope.class), anyList(), any(FutureCallback.class));
TbMsg newMsg = TbMsg.newMsg(msg, msg.getQueueName(), msg.getRuleChainId(), msg.getRuleNodeId());
doAnswer(invocation -> newMsg).when(ctxMock).newMsg(any(), any(String.class), any(), any(), any(), any());
node.onMsg(ctxMock, msg);
verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID));
verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), eq(ENTITY_VIEW_ID), eq(AttributeScope.CLIENT_SCOPE), eq(Collections.emptyList()), any(FutureCallback.class));
verify(ctxMock).ack(eq(msg));
verify(ctxMock).enqueueForTellNext(eq(newMsg), eq(TbNodeConnectionType.SUCCESS));
verifyNoMoreInteractions(ctxMock, entityViewServiceMock, telemetryServiceMock);
}
@Test
public void givenAttributesValidityPeriodOutOfStartDateAndEndDate_whenOnMsg_thenDoNothing() {
EntityView entityView = getEntityView(
SERVER_TELEMETRY_ENTITY_VIEW,
Instant.now().minus(2, ChronoUnit.DAYS).toEpochMilli(),
Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli()
);
mockEntityViewLookup(entityView);
TbMsg msg = TbMsg.newMsg(
ATTRIBUTES_DELETED, DEVICE_ID, new TbMsgMetaData(Map.of(DataConstants.SCOPE, AttributeScope.SERVER_SCOPE.name())),
"{\"attributes\": [\"serverAttribute1\"]}");
node.onMsg(ctxMock, msg);
verify(entityViewServiceMock).findEntityViewsByTenantIdAndEntityIdAsync(eq(TENANT_ID), eq(DEVICE_ID));
verify(ctxMock).ack(eq(msg));
verifyNoMoreInteractions(ctxMock, entityViewServiceMock);
}
@ParameterizedTest
@EnumSource(TbMsgType.class)
public void givenMsgTypeAndEmptyMetadata_whenOnMsg_thenVerifyFailureMsg(TbMsgType msgType) {
TbMsg msg = TbMsg.newMsg(msgType, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
node.onMsg(ctxMock, msg);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture());
if (msg.isTypeOneOf(ATTRIBUTES_UPDATED, ATTRIBUTES_DELETED,
ACTIVITY_EVENT, INACTIVITY_EVENT, POST_ATTRIBUTES_REQUEST)) {
assertThat(throwableCaptor.getValue()).isInstanceOf(IllegalArgumentException.class)
.hasMessage("Message metadata is empty");
return;
}
assertThat(throwableCaptor.getValue()).isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported msg type [" + msgType + "]");
verifyNoMoreInteractions(ctxMock);
}
private EntityView getEntityView(TelemetryEntityView attributesEntityView, long startTimeMs, long endTimeMs) {
EntityView entityView = new EntityView(ENTITY_VIEW_ID);
entityView.setStartTimeMs(startTimeMs);
entityView.setEndTimeMs(endTimeMs);
entityView.setKeys(attributesEntityView);
return entityView;
}
private EntityView getEntityView(TelemetryEntityView attributesEntityView) {
return getEntityView(attributesEntityView, ENTITY_VIEW_START_TS, ENTITY_VIEW_END_TS);
}
private void mockEntityViewLookup(EntityView entityView) {
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock);
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(entityViewServiceMock.findEntityViewsByTenantIdAndEntityIdAsync(any(), any()))
.thenReturn(Futures.immediateFuture(List.of(entityView)));
}
}

155
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgCountNodeTest.java

@ -0,0 +1,155 @@
/**
* Copyright © 2016-2024 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.rule.engine.action;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.stubbing.Answer;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.ArrayList;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.times;
@Slf4j
@ExtendWith(MockitoExtension.class)
public class TbMsgCountNodeTest {
private final RuleNodeId RULE_NODE_ID = new RuleNodeId(UUID.fromString("ee682a85-7f5a-4182-91bc-46e555138fe2"));
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("1b21c7cc-0c9e-4ab1-b867-99451599e146"));
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("04dfbd38-10e5-47b7-925f-11e795db89e1"));
private final ThingsBoardThreadFactory factory = ThingsBoardThreadFactory.forName("msg-count-node-test");
private final TbMsg tickMsg = TbMsg.newMsg(TbMsgType.MSG_COUNT_SELF_MSG, RULE_NODE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING);
private ScheduledExecutorService executorService;
private TbMsgCountNode node;
private TbMsgCountNodeConfiguration config;
@Mock
private TbContext ctxMock;
@BeforeEach
public void setUp() {
node = new TbMsgCountNode();
config = new TbMsgCountNodeConfiguration().defaultConfiguration();
executorService = Executors.newSingleThreadScheduledExecutor(factory);
}
@AfterEach
public void tearDown() {
if (executorService != null) {
executorService.shutdownNow();
}
node.destroy();
}
@Test
public void verifyDefaultConfig() {
assertThat(config.getInterval()).isEqualTo(1);
assertThat(config.getTelemetryPrefix()).isEqualTo("messageCount");
}
@Test
public void givenIncomingMsgs_whenOnMsg_thenSendsMsgWithMsgCount() throws TbNodeException, InterruptedException {
// GIVEN
int msgCount = 100;
var awaitTellSelfLatch = new CountDownLatch(1);
var currentMsgNumber = new AtomicInteger(0);
var msgWithCounterSent = new AtomicBoolean(false);
willAnswer((Answer<Void>) invocationOnMock -> {
executorService.schedule(() -> {
TbMsg tickMsg = invocationOnMock.getArgument(0);
msgWithCounterSent.set(true);
node.onMsg(ctxMock, tickMsg);
awaitTellSelfLatch.countDown();
}, config.getInterval(), TimeUnit.SECONDS);
return null;
}).given(ctxMock).tellSelf(any(TbMsg.class), anyLong());
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(ctxMock.getServiceId()).willReturn("tb-rule-engine");
given(ctxMock.getSelfId()).willReturn(RULE_NODE_ID);
given(ctxMock.newMsg(null, TbMsgType.MSG_COUNT_SELF_MSG, RULE_NODE_ID, null, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING)).willReturn(tickMsg);
// WHEN
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
var expectedProcessedMsgs = new ArrayList<TbMsg>();
for (int i = 0; i < msgCount; i++) {
var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING);
if (msgWithCounterSent.get()) {
break;
}
node.onMsg(ctxMock, msg);
expectedProcessedMsgs.add(msg);
currentMsgNumber.getAndIncrement();
}
awaitTellSelfLatch.await();
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
then(ctxMock).should(times(currentMsgNumber.get())).ack(msgCaptor.capture());
var actualProcessedMsgs = msgCaptor.getAllValues();
assertThat(actualProcessedMsgs).hasSize(expectedProcessedMsgs.size());
assertThat(actualProcessedMsgs).isNotEmpty();
assertThat(actualProcessedMsgs).containsExactlyInAnyOrderElementsOf(expectedProcessedMsgs);
ArgumentCaptor<TbMsg> msgWithCounterCaptor = ArgumentCaptor.forClass(TbMsg.class);
then(ctxMock).should().enqueueForTellNext(msgWithCounterCaptor.capture(), eq(TbNodeConnectionType.SUCCESS));
TbMsg resultedMsg = msgWithCounterCaptor.getValue();
String expectedData = "{\"messageCount_tb-rule-engine\":" + currentMsgNumber + "}";
TbMsg expectedMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TENANT_ID, TbMsgMetaData.EMPTY, expectedData);
assertThat(resultedMsg).usingRecursiveComparison()
.ignoringFields("id", "ts", "ctx", "metaData")
.isEqualTo(expectedMsg);
Map<String, String> actualMetadata = resultedMsg.getMetaData().getData();
assertThat(actualMetadata).hasFieldOrProperty("delta");
}
}

176
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sns/TbSnsNodeTest.java

@ -0,0 +1,176 @@
/**
* Copyright © 2016-2024 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.rule.engine.aws.sns;
import com.amazonaws.ResponseMetadata;
import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.model.PublishRequest;
import com.amazonaws.services.sns.model.PublishResult;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.never;
import static org.mockito.BDDMockito.verifyNoMoreInteractions;
@ExtendWith(MockitoExtension.class)
class TbSnsNodeTest {
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("fccfdf2e-6a88-4a94-81dd-5cbb557019cf"));
private final ListeningExecutor executor = new TestDbCallbackExecutor();
private TbSnsNode node;
private TbSnsNodeConfiguration config;
@Mock
private TbContext ctxMock;
@Mock
private AmazonSNS snsClientMock;
@Mock
private PublishResult publishResultMock;
@Mock
private ResponseMetadata responseMetadataMock;
@BeforeEach
void setUp() {
node = new TbSnsNode();
config = new TbSnsNodeConfiguration().defaultConfiguration();
ReflectionTestUtils.setField(node, "snsClient", snsClientMock);
ReflectionTestUtils.setField(node, "config", config);
}
@Test
void verifyDefaultConfig() {
assertThat(config.getTopicArnPattern()).isEqualTo("arn:aws:sns:us-east-1:123456789012:MyNewTopic");
assertThat(config.getAccessKeyId()).isNull();
assertThat(config.getSecretAccessKey()).isNull();
assertThat(config.getRegion()).isEqualTo("us-east-1");
}
@ParameterizedTest
@MethodSource
void givenForceAckIsTrueAndTopicNamePattern_whenOnMsg_thenEnqueueForTellNext(String topicName, TbMsgMetaData metaData, String data) {
ReflectionTestUtils.setField(node, "forceAck", true);
config.setAccessKeyId("accessKeyId");
config.setSecretAccessKey("secretAccessKey");
config.setTopicArnPattern(topicName);
String messageId = "msgId-1d186a16-80c7-44b3-a245-a1fc835f20c7";
String requestId = "reqId-bef0799b-dde9-4aa0-855b-86bbafaeaf31";
given(ctxMock.getExternalCallExecutor()).willReturn(executor);
given(snsClientMock.publish(any(PublishRequest.class))).willReturn(publishResultMock);
given(publishResultMock.getMessageId()).willReturn(messageId);
given(publishResultMock.getSdkResponseMetadata()).willReturn(responseMetadataMock);
given(responseMetadataMock.getRequestId()).willReturn(requestId);
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, data);
node.onMsg(ctxMock, msg);
then(ctxMock).should().ack(msg);
PublishRequest publishRequest = new PublishRequest()
.withTopicArn(TbNodeUtils.processPattern(topicName, msg))
.withMessage(data);
then(snsClientMock).should().publish(publishRequest);
ArgumentCaptor<TbMsg> actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
then(ctxMock).should().enqueueForTellNext(actualMsgCaptor.capture(), eq(TbNodeConnectionType.SUCCESS));
TbMsg actualMsg = actualMsgCaptor.getValue();
assertThat(actualMsg)
.usingRecursiveComparison()
.ignoringFields("metaData", "ctx")
.isEqualTo(msg);
assertThat(actualMsg.getMetaData().getData())
.hasFieldOrPropertyWithValue("messageId", messageId)
.hasFieldOrPropertyWithValue("requestId", requestId);
verifyNoMoreInteractions(ctxMock, snsClientMock, publishResultMock, responseMetadataMock);
}
private static Stream<Arguments> givenForceAckIsTrueAndTopicNamePattern_whenOnMsg_thenEnqueueForTellNext() {
return Stream.of(
Arguments.of("arn:aws:sns:us-east-1:123456789012:NewTopic", TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT),
Arguments.of("arn:aws:sns:us-east-1:123456789012:$[msgTopicName]", TbMsgMetaData.EMPTY, "{\"msgTopicName\":\"msg-topic-name\"}"),
Arguments.of("arn:aws:sns:us-east-1:123456789012:${mdTopicName}", new TbMsgMetaData(Map.of("mdTopicName", "md-topic-name")), TbMsg.EMPTY_JSON_OBJECT)
);
}
@Test
void givenForceAckIsFalseAndErrorOccursDuringProcessingRequest_whenOnMsg_thenTellFailure() {
ReflectionTestUtils.setField(node, "forceAck", false);
ListeningExecutor listeningExecutor = mock(ListeningExecutor.class);
given(ctxMock.getExternalCallExecutor()).willReturn(listeningExecutor);
String errorMsg = "Something went wrong";
ListenableFuture<TbMsg> failedFuture = Futures.immediateFailedFuture(new RuntimeException(errorMsg));
given(listeningExecutor.executeAsync(any(Callable.class))).willReturn(failedFuture);
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
node.onMsg(ctxMock, msg);
then(ctxMock).should(never()).enqueueForTellNext(any(), any(String.class));
ArgumentCaptor<TbMsg> actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
then(ctxMock).should().tellFailure(actualMsgCaptor.capture(), throwableCaptor.capture());
TbMsg actualMsg = actualMsgCaptor.getValue();
assertThat(actualMsg)
.usingRecursiveComparison()
.ignoringFields("metaData", "ctx")
.isEqualTo(msg);
assertThat(actualMsg.getMetaData().getData())
.hasFieldOrPropertyWithValue("error", RuntimeException.class + ": " + errorMsg);
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage(errorMsg);
verifyNoMoreInteractions(ctxMock, snsClientMock);
}
@Test
void givenSnsClientIsNotNull_whenDestroy_thenShutdown() {
node.destroy();
then(snsClientMock).should().shutdown();
}
@Test
void givenSnsClientIsNull_whenDestroy_thenVerifyNoInteractions() {
ReflectionTestUtils.setField(node, "snsClient", null);
node.destroy();
then(snsClientMock).shouldHaveNoInteractions();
}
}

263
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNodeTest.java

@ -0,0 +1,263 @@
/**
* Copyright © 2016-2024 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.rule.engine.aws.sqs;
import com.amazonaws.ResponseMetadata;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.model.MessageAttributeValue;
import com.amazonaws.services.sqs.model.SendMessageRequest;
import com.amazonaws.services.sqs.model.SendMessageResult;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.aws.sqs.TbSqsNodeConfiguration.QueueType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.never;
import static org.mockito.BDDMockito.verifyNoMoreInteractions;
@ExtendWith(MockitoExtension.class)
class TbSqsNodeTest {
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("764de824-929f-4114-95ea-0ea0401ffa3d"));
private final ListeningExecutor executor = new TestDbCallbackExecutor();
private final String messageId = "msgId-1d186a16-80c7-44b3-a245-a1fc835f20c7";
private final String requestId = "reqId-bef0799b-dde9-4aa0-855b-86bbafaeaf31";
private TbSqsNode node;
private TbSqsNodeConfiguration config;
@Mock
private TbContext ctxMock;
@Mock
private AmazonSQS sqsClientMock;
@Mock
private SendMessageResult sendMessageResultMock;
@Mock
private ResponseMetadata responseMetadataMock;
@BeforeEach
void setUp() {
node = new TbSqsNode();
config = new TbSqsNodeConfiguration().defaultConfiguration();
ReflectionTestUtils.setField(node, "sqsClient", sqsClientMock);
ReflectionTestUtils.setField(node, "config", config);
}
@Test
void verifyDefaultConfig() {
assertThat(config.getQueueType()).isEqualTo(QueueType.STANDARD);
assertThat(config.getQueueUrlPattern()).isEqualTo("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue-name");
assertThat(config.getDelaySeconds()).isEqualTo(0);
assertThat(config.getMessageAttributes()).isEqualTo(Collections.emptyMap());
assertThat(config.getAccessKeyId()).isNull();
assertThat(config.getSecretAccessKey()).isNull();
assertThat(config.getRegion()).isEqualTo("us-east-1");
}
@ParameterizedTest
@MethodSource
void givenQueueUrlPatternsAndQueueTypeIsFifo_whenOnMsg_thenVerifyRequest(String queueUrl, TbMsgMetaData metaData, String data) {
config.setQueueType(QueueType.FIFO);
config.setQueueUrlPattern(queueUrl);
mockSendingMsgRequest();
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, data);
node.onMsg(ctxMock, msg);
SendMessageRequest sendMsgRequest = new SendMessageRequest()
.withQueueUrl(TbNodeUtils.processPattern(queueUrl, msg))
.withMessageBody(data)
.withMessageDeduplicationId(msg.getId().toString())
.withMessageGroupId(DEVICE_ID.toString());
then(sqsClientMock).should().sendMessage(sendMsgRequest);
}
private static Stream<Arguments> givenQueueUrlPatternsAndQueueTypeIsFifo_whenOnMsg_thenVerifyRequest() {
return Stream.of(
Arguments.of(
"https://sqs.us-east-1.amazonaws.com/123456789012/new-queue-name",
TbMsgMetaData.EMPTY,
TbMsg.EMPTY_JSON_OBJECT),
Arguments.of(
"https://sqs.us-east-1.amazonaws.com/123456789012/$[msgQueueName]",
TbMsgMetaData.EMPTY,
"{\"msgQueueName\":\"msg-queue-name\"}"),
Arguments.of(
"https://sqs.us-east-1.amazonaws.com/123456789012/${mdQueueName}",
new TbMsgMetaData(Map.of("mdQueueName", "md-queue-name")),
TbMsg.EMPTY_JSON_OBJECT)
);
}
@ParameterizedTest
@MethodSource
void givenMsgAttributesPatternsAndQueueTypeIsStandard_whenOnMsg_thenVerifyRequest(TbMsgMetaData metaData, String data,
Map<String, String> attributes) {
config.setMessageAttributes(attributes);
mockSendingMsgRequest();
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, data);
node.onMsg(ctxMock, msg);
Map<String, MessageAttributeValue> messageAttributes = new HashMap<>();
this.config.getMessageAttributes().forEach((k, v) -> {
String name = TbNodeUtils.processPattern(k, msg);
String val = TbNodeUtils.processPattern(v, msg);
messageAttributes.put(name, new MessageAttributeValue().withDataType("String").withStringValue(val));
});
SendMessageRequest sendMsgRequest = new SendMessageRequest()
.withQueueUrl(config.getQueueUrlPattern())
.withMessageBody(data)
.withMessageAttributes(messageAttributes)
.withDelaySeconds(config.getDelaySeconds());
then(sqsClientMock).should().sendMessage(sendMsgRequest);
}
private static Stream<Arguments> givenMsgAttributesPatternsAndQueueTypeIsStandard_whenOnMsg_thenVerifyRequest() {
return Stream.of(
Arguments.of(TbMsgMetaData.EMPTY,
TbMsg.EMPTY_JSON_OBJECT,
Map.of("attributeName", "attributeValue")),
Arguments.of(TbMsgMetaData.EMPTY,
"{\"msgAttrNamePattern\":\"msgAttrName\",\"msgAttrValuePattern\":\"msgAttrValue\"}",
Map.of("$[msgAttrNamePattern]", "$[msgAttrValuePattern]")),
Arguments.of(new TbMsgMetaData(Map.of("mdAttrNamePattern", "mdAttrName", "mdAttrValuePattern", "mdAttrValue")),
TbMsg.EMPTY_JSON_OBJECT,
Map.of("${mdAttrNamePattern}", "${mdAttrValuePattern}"))
);
}
@Test
void givenForceAckIsTrueAndMsgResultContainsBodyAndAttributesAndNumber_whenOnMsg_thenEnqueueForTellNext() {
ReflectionTestUtils.setField(node, "forceAck", true);
String messageBodyMd5 = "msgBodyMd5-55fb8ba2-2b71-4673-a82a-969756764761";
String messageAttributesMd5 = "msgAttrMd5-e3ba3eef-52ae-436a-bec1-0c2c2252d1f1";
String sequenceNumber = "seqNum-bb5ddce0-cf4e-4295-b015-524bdb6a332f";
mockSendingMsgRequest();
given(sendMessageResultMock.getMD5OfMessageBody()).willReturn(messageBodyMd5);
given(sendMessageResultMock.getMD5OfMessageAttributes()).willReturn(messageAttributesMd5);
given(sendMessageResultMock.getSequenceNumber()).willReturn(sequenceNumber);
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
node.onMsg(ctxMock, msg);
then(ctxMock).should().ack(msg);
SendMessageRequest sendMsgRequest = new SendMessageRequest()
.withQueueUrl(TbNodeUtils.processPattern(config.getQueueUrlPattern(), msg))
.withMessageBody(msg.getData())
.withDelaySeconds(config.getDelaySeconds());
then(sqsClientMock).should().sendMessage(sendMsgRequest);
ArgumentCaptor<TbMsg> actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
then(ctxMock).should().enqueueForTellNext(actualMsgCaptor.capture(), eq(TbNodeConnectionType.SUCCESS));
TbMsg actualMsg = actualMsgCaptor.getValue();
assertThat(actualMsg)
.usingRecursiveComparison()
.ignoringFields("metaData", "ctx")
.isEqualTo(msg);
assertThat(actualMsg.getMetaData().getData())
.hasFieldOrPropertyWithValue("messageId", messageId)
.hasFieldOrPropertyWithValue("requestId", requestId)
.hasFieldOrPropertyWithValue("messageBodyMd5", messageBodyMd5)
.hasFieldOrPropertyWithValue("messageAttributesMd5", messageAttributesMd5)
.hasFieldOrPropertyWithValue("sequenceNumber", sequenceNumber);
verifyNoMoreInteractions(ctxMock, sqsClientMock, sendMessageResultMock, responseMetadataMock);
}
@Test
void givenForceAckIsFalseAndErrorOccursDuringProcessingRequest_whenOnMsg_thenTellFailure() {
ReflectionTestUtils.setField(node, "forceAck", false);
ListeningExecutor listeningExecutor = mock(ListeningExecutor.class);
given(ctxMock.getExternalCallExecutor()).willReturn(listeningExecutor);
String errorMsg = "Something went wrong";
ListenableFuture<TbMsg> failedFuture = Futures.immediateFailedFuture(new RuntimeException(errorMsg));
given(listeningExecutor.executeAsync(any(Callable.class))).willReturn(failedFuture);
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
node.onMsg(ctxMock, msg);
then(ctxMock).should(never()).enqueueForTellNext(any(), any(String.class));
ArgumentCaptor<TbMsg> actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
then(ctxMock).should().tellFailure(actualMsgCaptor.capture(), throwableCaptor.capture());
TbMsg actualMsg = actualMsgCaptor.getValue();
assertThat(actualMsg)
.usingRecursiveComparison()
.ignoringFields("metaData", "ctx")
.isEqualTo(msg);
assertThat(actualMsg.getMetaData().getData())
.hasFieldOrPropertyWithValue("error", RuntimeException.class + ": " + errorMsg);
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage(errorMsg);
verifyNoMoreInteractions(ctxMock, sqsClientMock);
}
@Test
void givenSqsClientIsNotNull_whenDestroy_thenShutdown() {
node.destroy();
then(sqsClientMock).should().shutdown();
}
@Test
void givenSqsClientIsNull_whenDestroy_thenVerifyNoInteractions() {
ReflectionTestUtils.setField(node, "sqsClient", null);
node.destroy();
then(sqsClientMock).shouldHaveNoInteractions();
}
private void mockSendingMsgRequest() {
given(ctxMock.getExternalCallExecutor()).willReturn(executor);
given(sqsClientMock.sendMessage(any(SendMessageRequest.class))).willReturn(sendMessageResultMock);
given(sendMessageResultMock.getMessageId()).willReturn(messageId);
given(sendMessageResultMock.getSdkResponseMetadata()).willReturn(responseMetadataMock);
given(responseMetadataMock.getRequestId()).willReturn(requestId);
}
}

64
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java

@ -16,7 +16,6 @@
package org.thingsboard.rule.engine.rest;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
@ -27,13 +26,16 @@ import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestHandler;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.provider.Arguments;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.id.DeviceId;
@ -41,7 +43,6 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -50,6 +51,7 @@ import java.io.IOException;
import java.util.Collections;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
@ -57,8 +59,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
public class TbRestApiCallNodeTest {
public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest {
@Spy
private TbRestApiCallNode restNode;
@Mock
@ -214,20 +217,45 @@ public class TbRestApiCallNodeTest {
assertEquals(TbMsg.EMPTY_JSON_OBJECT, dataCaptor.getValue());
}
@Test
public void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception {
var defaultConfig = new TbRestApiCallNodeConfiguration().defaultConfiguration();
var node = new TbRestApiCallNode();
String oldConfig = "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\":\"POST\"," +
"\"useSimpleClientHttpFactory\":false,\"ignoreRequestBody\":false,\"enableProxy\":false," +
"\"useSystemProxyProperties\":false,\"proxyScheme\":null,\"proxyHost\":null,\"proxyPort\":0," +
"\"proxyUser\":null,\"proxyPassword\":null,\"readTimeoutMs\":0,\"maxParallelRequestsCount\":0," +
"\"headers\":{\"Content-Type\":\"application/json\"},\"useRedisQueueForMsgPersistence\":false," +
"\"trimQueue\":null,\"maxQueueSize\":null,\"credentials\":{\"type\":\"anonymous\"},\"trimDoubleQuotes\":true}";
JsonNode configJson = JacksonUtil.toJsonNode(oldConfig);
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, configJson);
Assertions.assertTrue(upgrade.getFirst());
Assertions.assertTrue(JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass()).isParseToPlainText());
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// config for version 2 with upgrade from version 0
Arguments.of(0,
"{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\":\"POST\"," +
"\"useSimpleClientHttpFactory\":false,\"ignoreRequestBody\":false,\"enableProxy\":false," +
"\"useSystemProxyProperties\":false,\"proxyScheme\":null,\"proxyHost\":null,\"proxyPort\":0," +
"\"proxyUser\":null,\"proxyPassword\":null,\"readTimeoutMs\":0,\"maxParallelRequestsCount\":0," +
"\"headers\":{\"Content-Type\":\"application/json\"},\"useRedisQueueForMsgPersistence\":false," +
"\"trimQueue\":null,\"maxQueueSize\":null,\"credentials\":{\"type\":\"anonymous\"},\"trimDoubleQuotes\":false}",
true,
"{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," +
"\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," +
"\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," +
"\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," +
"\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," +
"\"credentials\": {\"type\": \"anonymous\"}}"),
// config for version 2 with upgrade from version 1
Arguments.of(1,
"{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," +
"\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," +
"\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," +
"\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," +
"\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," +
"\"useRedisQueueForMsgPersistence\": false,\"trimQueue\": null,\"maxQueueSize\": null," +
"\"credentials\": {\"type\": \"anonymous\"}}",
true,
"{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," +
"\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," +
"\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," +
"\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," +
"\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," +
"\"credentials\": {\"type\": \"anonymous\"}}")
);
}
@Override
protected TbNode getTestNode() {
return restNode;
}
}

87
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java

@ -19,8 +19,12 @@ import com.google.common.util.concurrent.SettableFuture;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
@ -29,7 +33,10 @@ import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsg;
@ -37,24 +44,30 @@ import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.edge.EdgeEventService;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TbSendRPCReplyNodeTest {
private static final String DUMMY_SERVICE_ID = "testServiceId";
private static final int DUMMY_REQUEST_ID = 0;
private static final UUID DUMMY_SESSION_ID = UUID.randomUUID();
private static final String DUMMY_DATA = "{\"key\":\"value\"}";
private static final UUID DUMMY_SESSION_ID = UUID.fromString("4f1d94aa-f6ee-4078-8499-b8e68443f8ad");
private final String DUMMY_DATA = "{\"key\":\"value\"}";
TbSendRPCReplyNode node;
private TbSendRPCReplyNode node;
private TbSendRpcReplyNodeConfiguration config;
private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
private final DeviceId deviceId = new DeviceId(UUID.randomUUID());
private final TenantId tenantId = TenantId.fromUUID(UUID.fromString("4e2e2336-3376-4238-ba0a-c669b412ca66"));
private final DeviceId deviceId = new DeviceId(UUID.fromString("af64d1b9-8635-47e1-8738-6389df7fe57e"));
@Mock
private TbContext ctx;
@ -71,14 +84,13 @@ public class TbSendRPCReplyNodeTest {
@BeforeEach
public void setUp() throws TbNodeException {
node = new TbSendRPCReplyNode();
TbSendRpcReplyNodeConfiguration config = new TbSendRpcReplyNodeConfiguration().defaultConfiguration();
config = new TbSendRpcReplyNodeConfiguration().defaultConfiguration();
node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
}
@Test
public void sendReplyToTransport() {
Mockito.when(ctx.getRpcService()).thenReturn(rpcService);
when(ctx.getRpcService()).thenReturn(rpcService);
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, getDefaultMetadata(),
TbMsgDataType.JSON, DUMMY_DATA, null, null);
@ -91,10 +103,10 @@ public class TbSendRPCReplyNodeTest {
@Test
public void sendReplyToEdgeQueue() {
Mockito.when(ctx.getTenantId()).thenReturn(tenantId);
Mockito.when(ctx.getEdgeEventService()).thenReturn(edgeEventService);
Mockito.when(edgeEventService.saveAsync(any())).thenReturn(SettableFuture.create());
Mockito.when(ctx.getDbCallbackExecutor()).thenReturn(listeningExecutor);
when(ctx.getTenantId()).thenReturn(tenantId);
when(ctx.getEdgeEventService()).thenReturn(edgeEventService);
when(edgeEventService.saveAsync(any())).thenReturn(SettableFuture.create());
when(ctx.getDbCallbackExecutor()).thenReturn(listeningExecutor);
TbMsgMetaData defaultMetadata = getDefaultMetadata();
defaultMetadata.putValue(DataConstants.EDGE_ID, UUID.randomUUID().toString());
@ -108,6 +120,55 @@ public class TbSendRPCReplyNodeTest {
verify(rpcService, never()).sendRpcReplyToDevice(DUMMY_SERVICE_ID, DUMMY_SESSION_ID, DUMMY_REQUEST_ID, DUMMY_DATA);
}
@ParameterizedTest
@EnumSource(EntityType.class)
public void testOriginatorEntityTypes(EntityType entityType) {
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, "0f386739-210f-4e23-8739-23f84a172adc");
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
node.onMsg(ctx, msg);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctx).tellFailure(eq(msg), throwableCaptor.capture());
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class)
.hasMessage(EntityType.DEVICE != entityType ? "Message originator is not a device entity!"
: "Request id is not present in the metadata!");
}
@ParameterizedTest
@MethodSource
public void testForAvailabilityOfMetadataAndDataValues(TbMsgMetaData metaData, String errorMsg) {
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, metaData, TbMsg.EMPTY_STRING);
node.onMsg(ctx, msg);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctx).tellFailure(eq(msg), throwableCaptor.capture());
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage(errorMsg);
}
@Test
public void verifyDefaultConfig() {
assertThat(config.getServiceIdMetaDataAttribute()).isEqualTo("serviceId");
assertThat(config.getSessionIdMetaDataAttribute()).isEqualTo("sessionId");
assertThat(config.getRequestIdMetaDataAttribute()).isEqualTo("requestId");
}
private static Stream<Arguments> testForAvailabilityOfMetadataAndDataValues() {
return Stream.of(
Arguments.of(TbMsgMetaData.EMPTY, "Request id is not present in the metadata!"),
Arguments.of(new TbMsgMetaData(Map.of(
"requestId", Integer.toString(DUMMY_REQUEST_ID))), "Service id is not present in the metadata!"),
Arguments.of(new TbMsgMetaData(Map.of(
"requestId", Integer.toString(DUMMY_REQUEST_ID),
"serviceId", DUMMY_SERVICE_ID)), "Session id is not present in the metadata!"),
Arguments.of(new TbMsgMetaData(Map.of(
"requestId", Integer.toString(DUMMY_REQUEST_ID),
"serviceId", DUMMY_SERVICE_ID, "sessionId",
DUMMY_SESSION_ID.toString())), "Request body is empty!")
);
}
private TbMsgMetaData getDefaultMetadata() {
TbSendRpcReplyNodeConfiguration config = new TbSendRpcReplyNodeConfiguration().defaultConfiguration();
TbMsgMetaData metadata = new TbMsgMetaData();

421
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java

@ -0,0 +1,421 @@
/**
* Copyright © 2016-2024 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.rule.engine.rpc;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest;
import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcResponse;
import org.thingsboard.rule.engine.api.RuleEngineRpcService;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.rpc.RpcError;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
@ExtendWith(MockitoExtension.class)
public class TbSendRPCRequestNodeTest {
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("d3a47f8b-d863-4c1f-b6f0-2c946b43f21c"));
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("b052ae59-b9b4-47e8-ac71-39e7124bbd66"));
private final String MSG_DATA = """
{
"method": "setGpio",
"params": {
"pin": "23",
"value": 1
},
"additionalInfo": "information"
}
""";
private TbSendRPCRequestNode node;
private TbSendRpcRequestNodeConfiguration config;
@Mock
private TbContext ctxMock;
@Mock
private RuleEngineRpcService rpcServiceMock;
@BeforeEach
public void setUp() throws TbNodeException {
node = new TbSendRPCRequestNode();
config = new TbSendRpcRequestNodeConfiguration().defaultConfiguration();
var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, configuration);
}
@Test
public void verifyDefaultConfig() {
assertThat(config.getTimeoutInSeconds()).isEqualTo(60);
}
@ParameterizedTest
@MethodSource
public void givenOneway_whenOnMsg_thenVerifyRequest(String mdKeyValue, boolean expectedResult) {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsgMetaData msgMetadata = new TbMsgMetaData();
msgMetadata.putValue("oneway", mdKeyValue);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, msgMetadata, MSG_DATA);
node.onMsg(ctxMock, msg);
var ruleEngineDeviceRpcRequestCaptor = captureRequest();
assertThat(ruleEngineDeviceRpcRequestCaptor.getValue().isOneway()).isEqualTo(expectedResult);
}
private static Stream<Arguments> givenOneway_whenOnMsg_thenVerifyRequest() {
return Stream.of(
Arguments.of("true", true),
Arguments.of("false", false),
Arguments.of(null, false),
Arguments.of("", false)
);
}
@Test
public void givenMsgBody_whenOnMsg_thenVerifyRequest() {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = ArgumentCaptor.forClass(RuleEngineDeviceRpcRequest.class);
then(rpcServiceMock).should().sendRpcRequestToDevice(requestCaptor.capture(), any(Consumer.class));
assertThat(requestCaptor.getValue())
.hasFieldOrPropertyWithValue("method", "setGpio")
.hasFieldOrPropertyWithValue("body", "{\"pin\":\"23\",\"value\":1}")
.hasFieldOrPropertyWithValue("deviceId", DEVICE_ID)
.hasFieldOrPropertyWithValue("tenantId", TENANT_ID)
.hasFieldOrPropertyWithValue("additionalInfo", "information");
}
@Test
public void givenRequestIdIsNotSet_whenOnMsg_thenVerifyRequest() {
Random randomMock = mock(Random.class);
given(randomMock.nextInt()).willReturn(123);
ReflectionTestUtils.setField(node, "random", randomMock);
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsg msg = TbMsg.newMsg(TbMsgType.TO_SERVER_RPC_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getRequestId()).isEqualTo(123);
}
@Test
public void givenRequestId_whenOnMsg_thenVerifyRequest() {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
String data = """
{
"method": "setGpio",
"params": {
"pin": "23",
"value": 1
},
"requestId": 12345
}
""";
TbMsg msg = TbMsg.newMsg(TbMsgType.TO_SERVER_RPC_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, data);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getRequestId()).isEqualTo(12345);
}
@Test
public void givenRequestUUID_whenOnMsg_thenVerifyRequest() {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
String requestUUID = "b795a241-5a30-48fb-92d5-46b864d47130";
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue("requestUUID", requestUUID);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getRequestUUID()).isEqualTo(UUID.fromString(requestUUID));
}
@ParameterizedTest
@NullAndEmptySource
public void givenInvalidRequestUUID_whenOnMsg_thenVerifyRequest(String requestUUID) {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue("requestUUID", requestUUID);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getRequestUUID()).isNotNull();
}
@Test
public void givenOriginServiceId_whenOnMsg_thenVerifyRequest() {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
String originServiceId = "service-id-123";
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue("originServiceId", originServiceId);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getOriginServiceId()).isEqualTo(originServiceId);
}
@ParameterizedTest
@NullAndEmptySource
public void givenInvalidOriginServiceId_whenOnMsg_thenVerifyRequest(String originServiceId) {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue("originServiceId", originServiceId);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getOriginServiceId()).isNull();
}
@Test
public void givenExpirationTime_whenOnMsg_thenVerifyRequest() {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
String expirationTime = "2000000000000";
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue(DataConstants.EXPIRATION_TIME, expirationTime);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getExpirationTime()).isEqualTo(Long.parseLong(expirationTime));
}
@ParameterizedTest
@NullAndEmptySource
public void givenInvalidExpirationTime_whenOnMsg_thenVerifyRequest(String expirationTime) {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue(DataConstants.EXPIRATION_TIME, expirationTime);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getExpirationTime()).isGreaterThan(System.currentTimeMillis());
}
@Test
public void givenRetries_whenOnMsg_thenVerifyRequest() {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
Integer retries = 3;
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue(DataConstants.RETRIES, String.valueOf(retries));
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getRetries()).isEqualTo(retries);
}
@ParameterizedTest
@NullAndEmptySource
public void givenInvalidRetriesValue_whenOnMsg_thenVerifyRequest(String retries) {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue(DataConstants.RETRIES, retries);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getRetries()).isNull();
}
@ParameterizedTest
@EnumSource(TbMsgType.class)
public void givenTbMsgType_whenOnMsg_thenVerifyRequest(TbMsgType msgType) {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsg msg = TbMsg.newMsg(msgType, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
if (msgType == TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE) {
assertThat(requestCaptor.getValue().isRestApiCall()).isTrue();
return;
}
assertThat(requestCaptor.getValue().isRestApiCall()).isFalse();
}
@ParameterizedTest
@MethodSource
public void givenPersistent_whenOnMsg_thenVerifyRequest(String isPersisted, boolean expectedPersistence) {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue(DataConstants.PERSISTENT, isPersisted);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().isPersisted()).isEqualTo(expectedPersistence);
}
private static Stream<Arguments> givenPersistent_whenOnMsg_thenVerifyRequest() {
return Stream.of(
Arguments.of("true", true),
Arguments.of("false", false),
Arguments.of(null, false),
Arguments.of("", false)
);
}
private ArgumentCaptor<RuleEngineDeviceRpcRequest> captureRequest() {
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = ArgumentCaptor.forClass(RuleEngineDeviceRpcRequest.class);
then(rpcServiceMock).should().sendRpcRequestToDevice(requestCaptor.capture(), any(Consumer.class));
return requestCaptor;
}
@Test
public void givenRpcResponseWithoutError_whenOnMsg_thenSendsRpcRequest() {
TbMsg outMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
// TODO: replace deprecated method newMsg()
given(ctxMock.newMsg(any(), any(String.class), any(), any(), any(), any())).willReturn(outMsg);
willAnswer(invocation -> {
Consumer<RuleEngineDeviceRpcResponse> consumer = invocation.getArgument(1);
RuleEngineDeviceRpcResponse rpcResponseMock = mock(RuleEngineDeviceRpcResponse.class);
given(rpcResponseMock.getError()).willReturn(Optional.empty());
given(rpcResponseMock.getResponse()).willReturn(Optional.of(TbMsg.EMPTY_JSON_OBJECT));
consumer.accept(rpcResponseMock);
return null;
}).given(rpcServiceMock).sendRpcRequestToDevice(any(RuleEngineDeviceRpcRequest.class), any(Consumer.class));
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA);
node.onMsg(ctxMock, msg);
then(ctxMock).should().enqueueForTellNext(outMsg, TbNodeConnectionType.SUCCESS);
then(ctxMock).should().ack(msg);
}
@Test
public void givenRpcResponseWithError_whenOnMsg_thenTellFailure() {
TbMsg outMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
// TODO: replace deprecated method newMsg()
given(ctxMock.newMsg(any(), any(String.class), any(), any(), any(), any())).willReturn(outMsg);
willAnswer(invocation -> {
Consumer<RuleEngineDeviceRpcResponse> consumer = invocation.getArgument(1);
RuleEngineDeviceRpcResponse rpcResponseMock = mock(RuleEngineDeviceRpcResponse.class);
given(rpcResponseMock.getError()).willReturn(Optional.of(RpcError.NO_ACTIVE_CONNECTION));
consumer.accept(rpcResponseMock);
return null;
}).given(rpcServiceMock).sendRpcRequestToDevice(any(RuleEngineDeviceRpcRequest.class), any(Consumer.class));
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA);
node.onMsg(ctxMock, msg);
then(ctxMock).should().enqueueForTellFailure(outMsg, RpcError.NO_ACTIVE_CONNECTION.name());
then(ctxMock).should().ack(msg);
}
@ParameterizedTest
@EnumSource(EntityType.class)
public void givenOriginatorIsNotDevice_whenOnMsg_thenThrowsException(EntityType entityType) {
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, "ac21a1bb-eabf-4463-8313-24bea1f498d9");
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
node.onMsg(ctxMock, msg);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
then(ctxMock).should().tellFailure(eq(msg), throwableCaptor.capture());
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class)
.hasMessage(EntityType.DEVICE != entityType ? "Message originator is not a device entity!"
: "Method is not present in the message!");
}
@ParameterizedTest
@ValueSource(strings = {"method", "params"})
public void givenMethodOrParamsAreNotPresent_whenOnMsg_thenThrowsException(String key) {
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, "{\"" + key + "\": \"value\"}");
node.onMsg(ctxMock, msg);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
then(ctxMock).should().tellFailure(eq(msg), throwableCaptor.capture());
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class)
.hasMessage(key.equals("method") ? "Params are not present in the message!" : "Method is not present in the message!");
}
}

249
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java

@ -0,0 +1,249 @@
/**
* Copyright © 2016-2024 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.rule.engine.telemetry;
import com.google.gson.JsonParser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TbMsgTimeseriesNodeTest {
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("c8f34868-603a-4433-876a-7d356e5cf377"));
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("e5095e9a-04f4-44c9-b443-1cf1b97d3384"));
private final TenantProfileId TENANT_PROFILE_ID = new TenantProfileId(UUID.fromString("ab78dd78-83d0-43fa-869f-d42ec9ed1744"));
private TbMsgTimeseriesNode node;
private TbMsgTimeseriesNodeConfiguration config;
private long tenantProfileDefaultStorageTtl;
@Mock
private TbContext ctxMock;
@Mock
private RuleEngineTelemetryService telemetryServiceMock;
@BeforeEach
public void setUp() throws TbNodeException {
node = new TbMsgTimeseriesNode();
config = new TbMsgTimeseriesNodeConfiguration().defaultConfiguration();
}
@Test
public void verifyDefaultConfig() {
assertThat(config.getDefaultTTL()).isEqualTo(0L);
assertThat(config.isSkipLatestPersistence()).isFalse();
assertThat(config.isUseServerTs()).isFalse();
}
@ParameterizedTest
@EnumSource(TbMsgType.class)
public void givenMsgTypeAndEmptyMsgData_whenOnMsg_thenVerifyFailureMsg(TbMsgType msgType) throws TbNodeException {
init();
TbMsg msg = TbMsg.newMsg(msgType, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY);
node.onMsg(ctxMock, msg);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture());
if (TbMsgType.POST_TELEMETRY_REQUEST.equals(msgType)) {
assertThat(throwableCaptor.getValue()).isInstanceOf(IllegalArgumentException.class).hasMessage("Msg body is empty: " + msg.getData());
verifyNoMoreInteractions(ctxMock);
return;
}
assertThat(throwableCaptor.getValue()).isInstanceOf(IllegalArgumentException.class).hasMessage("Unsupported msg type: " + msgType);
verifyNoMoreInteractions(ctxMock);
}
@Test
public void givenTtlFromConfigIsZeroAndUseServiceTsIsTrue_whenOnMsg_thenSaveTimeseriesUsingTenantProfileDefaultTtl() throws TbNodeException {
config.setUseServerTs(true);
init();
String data = """
{
"temp": 45,
"humidity": 77
}
""";
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, data);
when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock);
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
doAnswer(invocation -> {
TelemetryNodeCallback callback = invocation.getArgument(5);
callback.onSuccess(null);
return null;
}).when(telemetryServiceMock).saveAndNotify(any(), any(), any(), anyList(), anyLong(), any());
node.onMsg(ctxMock, msg);
List<TsKvEntry> expectedList = getTsKvEntriesListWithTs(data, System.currentTimeMillis());
ArgumentCaptor<List<TsKvEntry>> entryListCaptor = ArgumentCaptor.forClass(List.class);
verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), isNull(), eq(DEVICE_ID), entryListCaptor.capture(),
eq(tenantProfileDefaultStorageTtl), any(TelemetryNodeCallback.class));
assertThat(entryListCaptor.getValue()).usingRecursiveFieldByFieldElementComparatorIgnoringFields("ts")
.containsExactlyElementsOf(expectedList);
verify(ctxMock).tellSuccess(msg);
verifyNoMoreInteractions(ctxMock, telemetryServiceMock);
}
@Test
public void givenSkipLatestPersistenceIsTrueAndTtlFromConfig_whenOnMsg_thenSaveTimeseriesUsingTtlFromConfig() throws TbNodeException {
long ttlFromConfig = 5L;
config.setDefaultTTL(ttlFromConfig);
config.setSkipLatestPersistence(true);
init();
String data = """
{
"temp": 45,
"humidity": 77
}
""";
long ts = System.currentTimeMillis();
var metadata = Map.of("ts", String.valueOf(ts));
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, new TbMsgMetaData(metadata), data);
when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock);
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
doAnswer(invocation -> {
TelemetryNodeCallback callback = invocation.getArgument(5);
callback.onSuccess(null);
return null;
}).when(telemetryServiceMock).saveWithoutLatestAndNotify(any(), any(), any(), anyList(), anyLong(), any());
node.onMsg(ctxMock, msg);
List<TsKvEntry> expectedList = getTsKvEntriesListWithTs(data, ts);
ArgumentCaptor<List<TsKvEntry>> entryListCaptor = ArgumentCaptor.forClass(List.class);
verify(telemetryServiceMock).saveWithoutLatestAndNotify(
eq(TENANT_ID), isNull(), eq(DEVICE_ID), entryListCaptor.capture(), eq(ttlFromConfig), any(TelemetryNodeCallback.class));
assertThat(entryListCaptor.getValue()).containsExactlyElementsOf(expectedList);
verify(ctxMock).tellSuccess(msg);
verifyNoMoreInteractions(ctxMock, telemetryServiceMock);
}
@ParameterizedTest
@MethodSource
public void givenTtlFromConfigAndTtlFromMd_whenOnMsg_thenVerifyTtl(String ttlFromMd, long ttlFromConfig, long expectedTtl) throws TbNodeException {
config.setDefaultTTL(ttlFromConfig);
init();
when(ctxMock.getTelemetryService()).thenReturn(telemetryServiceMock);
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
String data = """
{
"temp": 45,
"humidity": 77
}
""";
var metadata = new TbMsgMetaData();
metadata.putValue("TTL", ttlFromMd);
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metadata, data);
node.onMsg(ctxMock, msg);
verify(telemetryServiceMock).saveAndNotify(eq(TENANT_ID), isNull(), eq(DEVICE_ID), anyList(), eq(expectedTtl), any(TelemetryNodeCallback.class));
}
private static Stream<Arguments> givenTtlFromConfigAndTtlFromMd_whenOnMsg_thenVerifyTtl() {
return Stream.of(
// when ttl is present in metadata and it is not zero then ttl = ttl from metadata
Arguments.of("1", 2L, 1L),
// when ttl is absent in metadata and present in config and it is not zero then ttl = ttl from config
Arguments.of("", 3L, 3L),
Arguments.of(null, 4L, 4L),
// when ttl is present in metadata or config but it is zero then ttl = default ttl from tenant profile
Arguments.of("0", 0L, TimeUnit.DAYS.toSeconds(5L))
);
}
private void init() throws TbNodeException {
var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
var tenantProfile = getTenantProfile();
when(ctxMock.getTenantProfile()).thenReturn(tenantProfile);
tenantProfile.getProfileConfiguration().ifPresent(profileConfiguration ->
tenantProfileDefaultStorageTtl = TimeUnit.DAYS.toSeconds(profileConfiguration.getDefaultStorageTtlDays()));
node.init(ctxMock, configuration);
verify(ctxMock).addTenantProfileListener(any());
}
private TenantProfile getTenantProfile() {
var tenantProfile = new TenantProfile(TENANT_PROFILE_ID);
var tenantProfileData = new TenantProfileData();
var tenantProfileConfiguration = new DefaultTenantProfileConfiguration();
tenantProfileConfiguration.setDefaultStorageTtlDays(5);
tenantProfileData.setConfiguration(tenantProfileConfiguration);
tenantProfile.setProfileData(tenantProfileData);
return tenantProfile;
}
private static List<TsKvEntry> getTsKvEntriesListWithTs(String data, long ts) {
Map<Long, List<KvEntry>> tsKvMap = JsonConverter.convertToTelemetry(JsonParser.parseString(data), ts);
List<TsKvEntry> expectedList = new ArrayList<>();
for (Map.Entry<Long, List<KvEntry>> tsKvEntry : tsKvMap.entrySet()) {
for (KvEntry kvEntry : tsKvEntry.getValue()) {
expectedList.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry));
}
}
return expectedList;
}
}

2
tools/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>tools</artifactId>

2
transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>

2
transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>

2
transport/lwm2m/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>

2
transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>

2
transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>transport</artifactId>

2
transport/snmp/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>

2
ui-ngx/package.json

@ -1,6 +1,6 @@
{
"name": "thingsboard",
"version": "3.7.0",
"version": "3.7.1",
"scripts": {
"ng": "ng",
"start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open",

2
ui-ngx/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version>
<version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>

18
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -133,6 +133,12 @@ import {
DigitalSimpleGaugeBasicConfigComponent
} from '@home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component';
import { MobileAppQrCodeBasicConfigComponent } from '@home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component';
import {
LabelCardBasicConfigComponent
} from '@home/components/widget/config/basic/cards/label-card-basic-config.component';
import {
LabelValueCardBasicConfigComponent
} from '@home/components/widget/config/basic/cards/label-value-card-basic-config.component';
@NgModule({
declarations: [
@ -177,7 +183,9 @@ import { MobileAppQrCodeBasicConfigComponent } from '@home/components/widget/con
PolarAreaChartBasicConfigComponent,
RadarChartBasicConfigComponent,
DigitalSimpleGaugeBasicConfigComponent,
MobileAppQrCodeBasicConfigComponent
MobileAppQrCodeBasicConfigComponent,
LabelCardBasicConfigComponent,
LabelValueCardBasicConfigComponent
],
imports: [
CommonModule,
@ -224,7 +232,9 @@ import { MobileAppQrCodeBasicConfigComponent } from '@home/components/widget/con
PolarAreaChartBasicConfigComponent,
RadarChartBasicConfigComponent,
DigitalSimpleGaugeBasicConfigComponent,
MobileAppQrCodeBasicConfigComponent
MobileAppQrCodeBasicConfigComponent,
LabelCardBasicConfigComponent,
LabelValueCardBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
@ -265,5 +275,7 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-polar-area-chart-basic-config': PolarAreaChartBasicConfigComponent,
'tb-radar-chart-basic-config': RadarChartBasicConfigComponent,
'tb-digital-simple-gauge-basic-config': DigitalSimpleGaugeBasicConfigComponent,
'tb-mobile-app-qr-code-basic-config': MobileAppQrCodeBasicConfigComponent
'tb-mobile-app-qr-code-basic-config': MobileAppQrCodeBasicConfigComponent,
'tb-label-card-basic-config': LabelCardBasicConfigComponent,
'tb-label-value-card-basic-config': LabelValueCardBasicConfigComponent
};

88
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-card-basic-config.component.html

@ -0,0 +1,88 @@
<!--
Copyright © 2016-2024 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.
-->
<ng-container [formGroup]="labelCardWidgetConfigForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.label-card.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>widgets.label-card.label</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="labelFont"
[previewText]="labelCardWidgetConfigForm.get('label').value">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="labelColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.label-card.icon' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
[color]="labelCardWidgetConfigForm.get('iconColor').value"
formControlName="icon">
</tb-material-icon-select>
<tb-color-input asBoxInput
colorClearButton
formControlName="iconColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
<div class="tb-form-row space-between column-lt-md">
<div translate>widget-config.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons">
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-border-radius' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-padding' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

130
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-card-basic-config.component.ts

@ -0,0 +1,130 @@
///
/// Copyright © 2016-2024 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.
///
import { Component } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { WidgetConfig, } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { isUndefined } from '@core/utils';
import {
labelCardWidgetDefaultSettings,
LabelCardWidgetSettings
} from '@home/components/widget/lib/cards/label-card-widget.models';
@Component({
selector: 'tb-label-card-basic-config',
templateUrl: './label-card-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class LabelCardBasicConfigComponent extends BasicWidgetConfigComponent {
labelCardWidgetConfigForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.labelCardWidgetConfigForm;
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: LabelCardWidgetSettings = {...labelCardWidgetDefaultSettings, ...(configData.config.settings || {})};
this.labelCardWidgetConfigForm = this.fb.group({
autoScale: [settings.autoScale, []],
label: [settings.label, []],
labelFont: [settings.labelFont, []],
labelColor: [settings.labelColor, []],
showIcon: [settings.showIcon, []],
iconSize: [settings.iconSize, [Validators.min(0)]],
iconSizeUnit: [settings.iconSizeUnit, []],
icon: [settings.icon, []],
iconColor: [settings.iconColor, []],
background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
padding: [settings.padding, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.autoScale = config.autoScale;
this.widgetConfig.config.settings.label = config.label;
this.widgetConfig.config.settings.labelFont = config.labelFont;
this.widgetConfig.config.settings.labelColor = config.labelColor;
this.widgetConfig.config.settings.showIcon = config.showIcon;
this.widgetConfig.config.settings.iconSize = config.iconSize;
this.widgetConfig.config.settings.iconSizeUnit = config.iconSizeUnit;
this.widgetConfig.config.settings.icon = config.icon;
this.widgetConfig.config.settings.iconColor = config.iconColor;
this.widgetConfig.config.settings.background = config.background;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.settings.padding = config.padding;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
protected validatorTriggers(): string[] {
return ['showIcon'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showIcon: boolean = this.labelCardWidgetConfigForm.get('showIcon').value;
if (showIcon) {
this.labelCardWidgetConfigForm.get('iconSize').enable();
this.labelCardWidgetConfigForm.get('iconSizeUnit').enable();
this.labelCardWidgetConfigForm.get('icon').enable();
this.labelCardWidgetConfigForm.get('iconColor').enable();
} else {
this.labelCardWidgetConfigForm.get('iconSize').disable();
this.labelCardWidgetConfigForm.get('iconSizeUnit').disable();
this.labelCardWidgetConfigForm.get('icon').disable();
this.labelCardWidgetConfigForm.get('iconColor').disable();
}
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.enableFullscreen = buttons.includes('fullscreen');
}
}

114
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.html

@ -0,0 +1,114 @@
<!--
Copyright © 2016-2024 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.
-->
<ng-container [formGroup]="labelValueCardWidgetConfigForm">
<tb-timewindow-config-panel *ngIf="displayTimewindowConfig"
[onlyHistoryTimewindow]="onlyHistoryTimewindow()"
formControlName="timewindowConfig">
</tb-timewindow-config-panel>
<tb-datasources
[configMode]="basicMode"
hideDatasourceLabel
hideDataKeyLabel
hideDataKeyColor
hideDataKeyUnits
hideDataKeyDecimals
formControlName="datasources">
</tb-datasources>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.label-card.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel">
{{ 'widgets.label-card.label' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="labelFont"
[previewText]="labelValueCardWidgetConfigForm.get('label').value">
</tb-font-settings>
<tb-color-settings formControlName="labelColor" settingsKey="{{'widgets.label-card.label' | translate }}">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.label-card.icon' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
[color]="labelValueCardWidgetConfigForm.get('iconColor').value?.color"
formControlName="icon">
</tb-material-icon-select>
<tb-color-settings formControlName="iconColor" settingsKey="{{'widgets.label-card.icon' | translate }}">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>widgets.label-value-card.value</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-unit-input class="flex" formControlName="units"></tb-unit-input>
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix fxHide.lt-md translate>widget-config.decimals-suffix</div>
</mat-form-field>
<tb-font-settings formControlName="valueFont"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="valueColor" settingsKey="{{'widgets.label-value-card.value' | translate }}">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
<div class="tb-form-row space-between column-lt-md">
<div translate>widget-config.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons">
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-border-radius' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-padding' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

191
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/label-value-card-basic-config.component.ts

@ -0,0 +1,191 @@
///
/// Copyright © 2016-2024 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.
///
import { Component } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import {
DataKey,
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation,
WidgetConfig,
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { formatValue, isUndefined } from '@core/utils';
import {
labelValueCardWidgetDefaultSettings,
LabelValueCardWidgetSettings
} from '@home/components/widget/lib/cards/label-value-card-widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import {
getTimewindowConfig,
setTimewindowConfig
} from '@home/components/widget/config/timewindow-config-panel.component';
@Component({
selector: 'tb-label-value-card-basic-config',
templateUrl: './label-value-card-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class LabelValueCardBasicConfigComponent extends BasicWidgetConfigComponent {
public get displayTimewindowConfig(): boolean {
const datasources = this.labelValueCardWidgetConfigForm.get('datasources').value;
return datasourcesHasAggregation(datasources);
}
public onlyHistoryTimewindow(): boolean {
const datasources = this.labelValueCardWidgetConfigForm.get('datasources').value;
return datasourcesHasOnlyComparisonAggregation(datasources);
}
labelValueCardWidgetConfigForm: UntypedFormGroup;
valuePreviewFn = this._valuePreviewFn.bind(this);
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.labelValueCardWidgetConfigForm;
}
protected defaultDataKeys(configData: WidgetConfigComponentData): DataKey[] {
return [{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries }];
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: LabelValueCardWidgetSettings = {...labelValueCardWidgetDefaultSettings, ...(configData.config.settings || {})};
this.labelValueCardWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
autoScale: [settings.autoScale, []],
showLabel: [settings.showLabel, []],
label: [settings.label, []],
labelFont: [settings.labelFont, []],
labelColor: [settings.labelColor, []],
showIcon: [settings.showIcon, []],
iconSize: [settings.iconSize, [Validators.min(0)]],
iconSizeUnit: [settings.iconSizeUnit, []],
icon: [settings.icon, []],
iconColor: [settings.iconColor, []],
units: [configData.config.units, []],
decimals: [configData.config.decimals, []],
valueFont: [settings.valueFont, []],
valueColor: [settings.valueColor, []],
background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
padding: [settings.padding, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.datasources = config.datasources;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.autoScale = config.autoScale;
this.widgetConfig.config.settings.showLabel = config.showLabel;
this.widgetConfig.config.settings.label = config.label;
this.widgetConfig.config.settings.labelFont = config.labelFont;
this.widgetConfig.config.settings.labelColor = config.labelColor;
this.widgetConfig.config.settings.showIcon = config.showIcon;
this.widgetConfig.config.settings.iconSize = config.iconSize;
this.widgetConfig.config.settings.iconSizeUnit = config.iconSizeUnit;
this.widgetConfig.config.settings.icon = config.icon;
this.widgetConfig.config.settings.iconColor = config.iconColor;
this.widgetConfig.config.units = config.units;
this.widgetConfig.config.decimals = config.decimals;
this.widgetConfig.config.settings.valueFont = config.valueFont;
this.widgetConfig.config.settings.valueColor = config.valueColor;
this.widgetConfig.config.settings.background = config.background;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.settings.padding = config.padding;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
protected validatorTriggers(): string[] {
return ['showLabel', 'showIcon'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showLabel: boolean = this.labelValueCardWidgetConfigForm.get('showLabel').value;
const showIcon: boolean = this.labelValueCardWidgetConfigForm.get('showIcon').value;
if (showLabel) {
this.labelValueCardWidgetConfigForm.get('label').enable();
this.labelValueCardWidgetConfigForm.get('labelFont').enable();
this.labelValueCardWidgetConfigForm.get('labelColor').enable();
} else {
this.labelValueCardWidgetConfigForm.get('label').disable();
this.labelValueCardWidgetConfigForm.get('labelFont').disable();
this.labelValueCardWidgetConfigForm.get('labelColor').disable();
}
if (showIcon) {
this.labelValueCardWidgetConfigForm.get('iconSize').enable();
this.labelValueCardWidgetConfigForm.get('iconSizeUnit').enable();
this.labelValueCardWidgetConfigForm.get('icon').enable();
this.labelValueCardWidgetConfigForm.get('iconColor').enable();
} else {
this.labelValueCardWidgetConfigForm.get('iconSize').disable();
this.labelValueCardWidgetConfigForm.get('iconSizeUnit').disable();
this.labelValueCardWidgetConfigForm.get('icon').disable();
this.labelValueCardWidgetConfigForm.get('iconColor').disable();
}
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.enableFullscreen = buttons.includes('fullscreen');
}
private _valuePreviewFn(): string {
const units: string = this.labelValueCardWidgetConfigForm.get('units').value;
const decimals: number = this.labelValueCardWidgetConfigForm.get('decimals').value;
return formatValue(22, decimals, units, true);
}
}

30
ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.html

@ -0,0 +1,30 @@
<!--
Copyright © 2016-2024 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.
-->
<div #labelCardPanel class="tb-label-card-panel" [style.padding]="padding" [style]="backgroundStyle$ | async"
[class.tb-label-card-pointer]="hasCardClickAction" (click)="cardClick($event)">
<div class="tb-label-card-overlay" [style]="overlayStyle"></div>
<div class="tb-label-card-title-panel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
</div>
<div #labelCardContent class="tb-label-card-content">
<div #labelCardRow class="tb-label-card-row">
<tb-icon *ngIf="showIcon" [style]="iconStyle">{{ icon }}</tb-icon>
<div [class.tb-label-card-label]="!settings.autoScale" [style]="labelStyle" [innerHTML]="label | safe:'html'"></div>
</div>
</div>
</div>

70
ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.scss

@ -0,0 +1,70 @@
/**
* Copyright © 2016-2024 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.
*/
.tb-label-card-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 20px 24px;
&.tb-label-card-pointer {
cursor: pointer;
}
> div:not(.tb-label-card-overlay), > tb-icon {
z-index: 1;
}
.tb-label-card-overlay {
position: absolute;
top: 12px;
left: 12px;
bottom: 12px;
right: 12px;
}
> div.tb-label-card-title-panel {
position: absolute;
top: 12px;
left: 12px;
right: 12px;
z-index: 2;
}
div.tb-widget-title {
padding: 0;
}
.tb-label-card-content {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
.tb-label-card-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 8px;
}
.tb-label-card-label {
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
display: -webkit-box;
-webkit-box-orient: vertical;
}
}
}

170
ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.component.ts

@ -0,0 +1,170 @@
///
/// Copyright © 2016-2024 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.
///
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnDestroy,
OnInit,
Renderer2,
TemplateRef,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import {
labelCardWidgetDefaultSettings,
LabelCardWidgetSettings
} from '@home/components/widget/lib/cards/label-card-widget.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { Observable } from 'rxjs';
import {
backgroundStyle,
ComponentStyle,
iconStyle,
overlayStyle,
resolveCssSize,
textStyle
} from '@shared/models/widget-settings.models';
import { ResizeObserver } from '@juggle/resize-observer';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'tb-label-card-widget',
templateUrl: './label-card-widget.component.html',
styleUrls: ['./label-card-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class LabelCardWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('labelCardPanel', {static: false})
labelCardPanel: ElementRef<HTMLElement>;
@ViewChild('labelCardContent', {static: false})
labelCardContent: ElementRef<HTMLElement>;
@ViewChild('labelCardRow', {static: false})
labelCardRow: ElementRef<HTMLElement>;
settings: LabelCardWidgetSettings;
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
padding: string;
label: string;
labelStyle: ComponentStyle = {};
showIcon = true;
icon = '';
iconStyle: ComponentStyle = {};
hasCardClickAction = false;
private panelResize$: ResizeObserver;
constructor(private imagePipe: ImagePipe,
private sanitizer: DomSanitizer,
private renderer: Renderer2,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
this.ctx.$scope.labelCardWidget = this;
this.settings = {...labelCardWidgetDefaultSettings, ...this.ctx.settings};
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding;
this.showIcon = this.settings.showIcon;
this.icon = this.settings.icon;
this.iconStyle = iconStyle(this.settings.iconSize, this.settings.iconSizeUnit);
this.iconStyle.color = this.settings.iconColor;
this.label = this.settings.label;
this.labelStyle = textStyle(this.settings.labelFont);
this.labelStyle.color = this.settings.labelColor;
this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0;
}
public ngAfterViewInit() {
if (this.settings.autoScale) {
this.renderer.setStyle(this.labelCardContent.nativeElement, 'overflow', 'visible');
this.renderer.setStyle(this.labelCardContent.nativeElement, 'position', 'absolute');
this.panelResize$ = new ResizeObserver(() => {
this.onResize();
});
this.panelResize$.observe(this.labelCardPanel.nativeElement);
this.onResize();
}
}
ngOnDestroy() {
if (this.panelResize$) {
this.panelResize$.disconnect();
}
}
public onInit() {
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
this.cd.detectChanges();
}
public cardClick($event: Event) {
this.ctx.actionsApi.cardClick($event);
}
private onResize() {
const paddingLeft = getComputedStyle(this.labelCardPanel.nativeElement).paddingLeft;
const paddingRight = getComputedStyle(this.labelCardPanel.nativeElement).paddingRight;
const paddingTop = getComputedStyle(this.labelCardPanel.nativeElement).paddingTop;
const paddingBottom = getComputedStyle(this.labelCardPanel.nativeElement).paddingBottom;
const pLeft = resolveCssSize(paddingLeft)[0];
const pRight = resolveCssSize(paddingRight)[0];
const pTop = resolveCssSize(paddingTop)[0];
const pBottom = resolveCssSize(paddingBottom)[0];
const panelWidth = this.labelCardPanel.nativeElement.getBoundingClientRect().width - (pLeft + pRight);
const panelHeight = this.labelCardPanel.nativeElement.getBoundingClientRect().height - (pTop + pBottom);
this.renderer.setStyle(this.labelCardContent.nativeElement, 'width', 'auto');
this.renderer.setStyle(this.labelCardContent.nativeElement, 'transform', `none`);
const contentWidth = this.labelCardRow.nativeElement.getBoundingClientRect().width;
const contentHeight = this.labelCardRow.nativeElement.getBoundingClientRect().height;
const panelAspect = panelWidth / panelHeight;
const contentAspect = contentWidth / contentHeight;
let scale: number;
if (contentAspect > panelAspect) {
scale = panelWidth / contentWidth;
} else {
scale = panelHeight / contentHeight;
}
const width = panelWidth / scale;
this.renderer.setStyle(this.labelCardContent.nativeElement, 'width', width + 'px');
this.renderer.setStyle(this.labelCardContent.nativeElement, 'transform', `scale(${scale})`);
}
}

60
ui-ngx/src/app/modules/home/components/widget/lib/cards/label-card-widget.models.ts

@ -0,0 +1,60 @@
///
/// Copyright © 2016-2024 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.
///
import { BackgroundSettings, BackgroundType, cssUnit, Font } from '@shared/models/widget-settings.models';
export interface LabelCardWidgetSettings {
autoScale: boolean;
label: string;
labelFont: Font;
labelColor: string;
showIcon: boolean;
icon: string;
iconSize: number;
iconSizeUnit: cssUnit;
iconColor: string;
background: BackgroundSettings;
padding: string;
}
export const labelCardWidgetDefaultSettings: LabelCardWidgetSettings = {
autoScale: true,
label: 'Thermostat',
labelFont: {
family: 'Roboto',
size: 20,
sizeUnit: 'px',
style: 'normal',
weight: '400',
lineHeight: '24px'
},
labelColor: 'rgba(0, 0, 0, 0.87)',
showIcon: true,
icon: 'thermostat',
iconSize: 24,
iconSizeUnit: 'px',
iconColor: '#5469FF',
background: {
type: BackgroundType.color,
color: '#fff',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
},
padding: '12px'
};

34
ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.html

@ -0,0 +1,34 @@
<!--
Copyright © 2016-2024 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.
-->
<div #labelCardPanel class="tb-label-value-card-panel" [style.padding]="padding" [style]="backgroundStyle$ | async"
[class.tb-label-value-card-pointer]="hasCardClickAction" (click)="cardClick($event)">
<div class="tb-label-value-card-overlay" [style]="overlayStyle"></div>
<div class="tb-label-value-card-title-panel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
</div>
<div #labelCardContent class="tb-label-value-card-content">
<div #labelCardRow class="tb-label-value-card-row">
<div *ngIf="showIcon || showLabel" class="tb-label-value-card-icon-label">
<tb-icon *ngIf="showIcon" [style]="iconStyle" [style.color]="iconColor.color">{{ icon }}</tb-icon>
<div *ngIf="showLabel" [class.tb-label-value-card-label]="!settings.autoScale" [style]="labelStyle" [style.color]="labelColor.color"
[innerHTML]="label$ | async | safe:'html'"></div>
</div>
<div class="tb-label-value-card-value" [style]="valueStyle" [style.color]="valueColor.color">{{ valueText }}</div>
</div>
</div>
</div>

80
ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.scss

@ -0,0 +1,80 @@
/**
* Copyright © 2016-2024 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.
*/
.tb-label-value-card-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 20px 24px;
&.tb-label-value-card-pointer {
cursor: pointer;
}
> div:not(.tb-label-value-card-overlay), > tb-icon {
z-index: 1;
}
.tb-label-value-card-overlay {
position: absolute;
top: 12px;
left: 12px;
bottom: 12px;
right: 12px;
}
> div.tb-label-value-card-title-panel {
position: absolute;
top: 12px;
left: 12px;
right: 12px;
z-index: 2;
}
div.tb-widget-title {
padding: 0;
}
.tb-label-value-card-content {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
.tb-label-value-card-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 16px;
}
.tb-label-value-card-icon-label {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 8px;
}
.tb-label-value-card-label {
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
display: -webkit-box;
-webkit-box-orient: vertical;
}
.tb-label-value-card-value {
white-space: nowrap;
}
}
}

218
ui-ngx/src/app/modules/home/components/widget/lib/cards/label-value-card-widget.component.ts

@ -0,0 +1,218 @@
///
/// Copyright © 2016-2024 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.
///
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnDestroy,
OnInit,
Renderer2,
TemplateRef,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { WidgetContext } from '@home/models/widget-component.models';
import { Observable } from 'rxjs';
import {
backgroundStyle,
ColorProcessor,
ComponentStyle,
getDataKey,
getSingleTsValue,
iconStyle,
overlayStyle,
resolveCssSize,
textStyle
} from '@shared/models/widget-settings.models';
import { ResizeObserver } from '@juggle/resize-observer';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import {
labelValueCardWidgetDefaultSettings,
LabelValueCardWidgetSettings
} from '@home/components/widget/lib/cards/label-value-card-widget.models';
import { formatValue, isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-label-value-card-widget',
templateUrl: './label-value-card-widget.component.html',
styleUrls: ['./label-value-card-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class LabelValueCardWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('labelCardPanel', {static: false})
labelCardPanel: ElementRef<HTMLElement>;
@ViewChild('labelCardContent', {static: false})
labelCardContent: ElementRef<HTMLElement>;
@ViewChild('labelCardRow', {static: false})
labelCardRow: ElementRef<HTMLElement>;
settings: LabelValueCardWidgetSettings;
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
padding: string;
showLabel = true;
label$: Observable<string>;
labelStyle: ComponentStyle = {};
labelColor: ColorProcessor;
valueText = 'N/A';
valueStyle: ComponentStyle = {};
valueColor: ColorProcessor;
showIcon = true;
icon = '';
iconStyle: ComponentStyle = {};
iconColor: ColorProcessor;
hasCardClickAction = false;
private panelResize$: ResizeObserver;
private decimals = 0;
private units = '';
constructor(private imagePipe: ImagePipe,
private sanitizer: DomSanitizer,
private renderer: Renderer2,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
this.ctx.$scope.labelValueCardWidget = this;
this.settings = {...labelValueCardWidgetDefaultSettings, ...this.ctx.settings};
this.decimals = this.ctx.decimals;
this.units = this.ctx.units;
const dataKey = getDataKey(this.ctx.datasources);
if (isDefinedAndNotNull(dataKey?.decimals)) {
this.decimals = dataKey.decimals;
}
if (dataKey?.units) {
this.units = dataKey.units;
}
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding;
this.showIcon = this.settings.showIcon;
this.icon = this.settings.icon;
this.iconStyle = iconStyle(this.settings.iconSize, this.settings.iconSizeUnit);
this.iconColor = ColorProcessor.fromSettings(this.settings.iconColor);
this.showLabel = this.settings.showLabel;
const label = this.settings.label;
this.label$ = this.ctx.registerLabelPattern(label, this.label$);
this.labelStyle = textStyle(this.settings.labelFont);
this.labelColor = ColorProcessor.fromSettings(this.settings.labelColor);
this.valueStyle = textStyle(this.settings.valueFont);
this.valueColor = ColorProcessor.fromSettings(this.settings.valueColor);
this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0;
}
public ngAfterViewInit() {
if (this.settings.autoScale) {
this.renderer.setStyle(this.labelCardContent.nativeElement, 'overflow', 'visible');
this.renderer.setStyle(this.labelCardContent.nativeElement, 'position', 'absolute');
this.panelResize$ = new ResizeObserver(() => {
this.onResize();
});
this.panelResize$.observe(this.labelCardPanel.nativeElement);
this.onResize();
}
}
ngOnDestroy() {
if (this.panelResize$) {
this.panelResize$.disconnect();
}
}
public onInit() {
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
this.cd.detectChanges();
}
public onDataUpdated() {
const tsValue = getSingleTsValue(this.ctx.data);
let value;
if (tsValue && isDefinedAndNotNull(tsValue[1]) && tsValue[0] !== 0) {
value = tsValue[1];
this.valueText = formatValue(value, this.decimals, this.units, false);
} else {
this.valueText = 'N/A';
}
this.iconColor.update(value);
this.labelColor.update(value);
this.valueColor.update(value);
this.cd.detectChanges();
if (this.settings.autoScale) {
setTimeout(() => {
this.onResize();
}, 0);
}
}
public cardClick($event: Event) {
this.ctx.actionsApi.cardClick($event);
}
private onResize() {
const paddingLeft = getComputedStyle(this.labelCardPanel.nativeElement).paddingLeft;
const paddingRight = getComputedStyle(this.labelCardPanel.nativeElement).paddingRight;
const paddingTop = getComputedStyle(this.labelCardPanel.nativeElement).paddingTop;
const paddingBottom = getComputedStyle(this.labelCardPanel.nativeElement).paddingBottom;
const pLeft = resolveCssSize(paddingLeft)[0];
const pRight = resolveCssSize(paddingRight)[0];
const pTop = resolveCssSize(paddingTop)[0];
const pBottom = resolveCssSize(paddingBottom)[0];
const panelWidth = this.labelCardPanel.nativeElement.getBoundingClientRect().width - (pLeft + pRight);
const panelHeight = this.labelCardPanel.nativeElement.getBoundingClientRect().height - (pTop + pBottom);
this.renderer.setStyle(this.labelCardContent.nativeElement, 'width', 'auto');
this.renderer.setStyle(this.labelCardContent.nativeElement, 'transform', `none`);
const contentWidth = this.labelCardRow.nativeElement.getBoundingClientRect().width;
const contentHeight = this.labelCardRow.nativeElement.getBoundingClientRect().height;
const panelAspect = panelWidth / panelHeight;
const contentAspect = contentWidth / contentHeight;
let scale: number;
if (contentAspect > panelAspect) {
scale = panelWidth / contentWidth;
} else {
scale = panelHeight / contentHeight;
}
const width = panelWidth / scale;
this.renderer.setStyle(this.labelCardContent.nativeElement, 'width', width + 'px');
this.renderer.setStyle(this.labelCardContent.nativeElement, 'transform', `scale(${scale})`);
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save