Browse Source

Merge remote-tracking branch 'upstream/develop/3.5' into feature-alarm-assigning

pull/8090/head
zbeacon 4 years ago
parent
commit
2524965ea1
  1. 107
      application/src/main/data/json/edge/install_instructions/docker/instructions.md
  2. 3
      application/src/main/data/json/edge/install_instructions/docker/localhost_warning.md
  3. 2
      application/src/main/data/upgrade/3.4.3/schema_update.sql
  4. 7
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  5. 4
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  6. 22
      application/src/main/java/org/thingsboard/server/controller/EdgeController.java
  7. 6
      application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
  8. 3
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  9. 94
      application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallService.java
  10. 28
      application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallService.java
  11. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  12. 9
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java
  13. 4
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java
  14. 4
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java
  15. 12
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java
  16. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java
  17. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  18. 7
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java
  19. 16
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RelationEdgeProcessor.java
  20. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java
  21. 81
      application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java
  22. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java
  23. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java
  24. 6
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java
  25. 16
      application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java
  26. 6
      application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
  27. 4
      application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java
  28. 20
      application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java
  29. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java
  30. 27
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  31. 8
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  32. 35
      application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java
  33. 44
      application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java
  34. 39
      application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java
  35. 24
      application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java
  36. 37
      application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java
  37. 73
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  38. 8
      application/src/test/java/org/thingsboard/server/edge/BaseAssetProfileEdgeTest.java
  39. 34
      application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java
  40. 25
      application/src/test/java/org/thingsboard/server/edge/BaseDeviceProfileEdgeTest.java
  41. 8
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java
  42. 6
      common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java
  43. 32
      common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInstallInstructions.java
  44. 6
      common/edge-api/src/main/proto/edge.proto
  45. 2
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  46. 9
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetProfileEntity.java
  47. 9
      dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java
  48. 8
      dao/src/main/resources/sql/schema-entities.sql
  49. 8
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  50. 3
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  51. 22
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/DeduplicationId.java
  52. 22
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/DeduplicationStrategy.java
  53. 237
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java
  54. 46
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNodeConfiguration.java
  55. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java
  56. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  57. 402
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgDeduplicationNodeTest.java
  58. 6
      ui-ngx/src/app/core/http/edge.service.ts
  59. 6
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html
  60. 7
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts
  61. 6
      ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html
  62. 8
      ui-ngx/src/app/modules/home/components/profile/asset-profile.component.ts
  63. 6
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.html
  64. 8
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts
  65. 5
      ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html
  66. 10
      ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts
  67. 43
      ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html
  68. 46
      ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts
  69. 9
      ui-ngx/src/app/modules/home/pages/edge/edge.component.html
  70. 4
      ui-ngx/src/app/modules/home/pages/edge/edge.module.ts
  71. 26
      ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts
  72. 1
      ui-ngx/src/app/shared/models/asset.models.ts
  73. 1
      ui-ngx/src/app/shared/models/device.models.ts
  74. 4
      ui-ngx/src/app/shared/models/edge.models.ts
  75. 5
      ui-ngx/src/assets/locale/locale.constant-en_US.json

107
application/src/main/data/json/edge/install_instructions/docker/instructions.md

@ -0,0 +1,107 @@
## Install ThingsBoard Edge and connect to cloud instructions
Here is the list of commands, that can be used to quickly install and connect ThingsBoard Edge to the cloud using docker compose.
### Prerequisites
Install <a href="https://docs.docker.com/engine/install/" target="_blank"> Docker CE</a> and <a href="https://docs.docker.com/compose/install/" target="_blank"> Docker Compose</a>.
### Create data and logs folders
Run following commands, before starting docker container(s), to create folders for storing data and logs.
These commands additionally will change owner of newly created folders to docker container user.
To do this (to change user) **chown** command is used, and this command requires *sudo* permissions (command will request password for a *sudo* access):
```bash
mkdir -p ~/.mytb-edge-data && sudo chown -R 799:799 ~/.mytb-edge-data
mkdir -p ~/.mytb-edge-logs && sudo chown -R 799:799 ~/.mytb-edge-logs
{:copy-code}
```
### Running ThingsBoard Edge as docker service
${LOCALHOST_WARNING}
Create docker compose file for ThingsBoard Edge service:
```bash
nano docker-compose.yml
{:copy-code}
```
Add the following lines to the yml file:
```bash
version: '3.0'
services:
mytbedge:
restart: always
image: "thingsboard/tb-edge:${TB_EDGE_VERSION}"
ports:
- "8080:8080"
- "1883:1883"
- "5683-5688:5683-5688/udp"
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/tb-edge
CLOUD_ROUTING_KEY: ${CLOUD_ROUTING_KEY}
CLOUD_ROUTING_SECRET: ${CLOUD_ROUTING_SECRET}
CLOUD_RPC_HOST: ${BASE_URL}
CLOUD_RPC_PORT: ${CLOUD_RPC_PORT}
CLOUD_RPC_SSL_ENABLED: ${CLOUD_RPC_SSL_ENABLED}
volumes:
- ~/.mytb-edge-data:/data
- ~/.mytb-edge-logs:/var/log/tb-edge
postgres:
restart: always
image: "postgres:12"
ports:
- "5432"
environment:
POSTGRES_DB: tb-edge
POSTGRES_PASSWORD: postgres
volumes:
- ~/.mytb-edge-data/db:/var/lib/postgresql/data
{:copy-code}
```
#### [Optional] Update bind ports
If ThingsBoard Edge is going to be running on the same machine where ThingsBoard server (cloud) is running, you'll need to update docker compose port mapping to avoid port collision between ThingsBoard server and ThingsBoard Edge.
Please update next lines of `docker-compose.yml` file:
```bash
ports:
- "18080:8080"
- "11883:1883"
- "15683-15688:5683-5688/udp"
```
Make sure that ports above (18080, 11883, 15683-15688) are not used by any other application.
#### Start ThingsBoard Edge
Set the terminal in the directory which contains the `docker-compose.yml` file and execute the following commands to up this docker compose directly:
```bash
docker compose up -d
docker compose logs -f mytbedge
{:copy-code}
```
###### NOTE: Docker Compose V2 vs docker-compose (with a hyphen)
ThingsBoard supports Docker Compose V2 (Docker Desktop or Compose plugin) starting from **3.4.2** release, because **docker-compose** as standalone setup is no longer supported by Docker.
We **strongly** recommend to update to Docker Compose V2 and use it.
If you still rely on using Docker Compose as docker-compose (with a hyphen), then please execute the following commands to start ThingsBoard Edge:
```bash
docker-compose up -d
docker-compose logs -f mytbedge
```
#### Open ThingsBoard Edge UI
Once started, you will be able to open **ThingsBoard Edge UI** using the following link http://localhost:8080.
###### NOTE: Edge HTTP bind port update
Use next **ThingsBoard Edge UI** link **http://localhost:18080** if you updated HTTP 8080 bind port to **18080**.

3
application/src/main/data/json/edge/install_instructions/docker/localhost_warning.md

@ -0,0 +1,3 @@
###### WARNING NOTE: 'localhost' can not be used as CLOUD_RPC_HOST
Please note that your ThingsBoard base URL is **'localhost'** at the moment. **'localhost'** cannot be used for docker containers - please update **CLOUD_RPC_HOST** environment variable below to the IP address of your machine (*docker **host** machine*). IP address must be `192.168.1.XX` or similar format. In other case - ThingsBoard Edge service, that is running in docker container, will not be able to connect to the cloud.

2
application/src/main/data/upgrade/3.4.3/schema_update.sql

@ -31,4 +31,4 @@ CREATE TABLE IF NOT EXISTS alarm_comment (
comment varchar(10000),
CONSTRAINT fk_alarm_comment_alarm_id FOREIGN KEY (alarm_id) REFERENCES alarm(id) ON DELETE CASCADE
) PARTITION BY RANGE (created_time);
CREATE INDEX IF NOT EXISTS idx_alarm_comment_alarm_id ON alarm_comment(alarm_id);
CREATE INDEX IF NOT EXISTS idx_alarm_comment_alarm_id ON alarm_comment(alarm_id);

7
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -105,6 +105,7 @@ import org.thingsboard.server.service.script.RuleNodeTbelScriptEngine;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@ -794,6 +795,12 @@ class DefaultTbContext implements TbContext {
return metaData;
}
@Override
public void schedule(Runnable runnable, long delay, TimeUnit timeUnit) {
mainCtx.getScheduler().schedule(runnable, delay, timeUnit);
}
@Override
public void checkTenantEntity(EntityId entityId) {
if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) {

4
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -135,6 +135,7 @@ import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.edge.instructions.EdgeInstallService;
import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.entitiy.TbNotificationEntityService;
import org.thingsboard.server.service.ota.OtaPackageStateService;
@ -287,6 +288,9 @@ public abstract class BaseController {
@Autowired(required = false)
protected EdgeRpcService edgeRpcService;
@Autowired(required = false)
protected EdgeInstallService edgeInstallService;
@Autowired
protected TbNotificationEntityService notificationEntityService;

22
application/src/main/java/org/thingsboard/server/controller/EdgeController.java

@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeInfo;
import org.thingsboard.server.common.data.edge.EdgeInstallInstructions;
import org.thingsboard.server.common.data.edge.EdgeSearchQuery;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -63,6 +64,7 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@ -595,4 +597,24 @@ public class EdgeController extends BaseController {
return edgeBulkImportService.processBulkImport(request, user);
}
@ApiOperation(value = "Get Edge Docker Install Instructions (getEdgeDockerInstallInstructions)",
notes = "Get a docker install instructions for provided edge id." + TENANT_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/instructions/{edgeId}", method = RequestMethod.GET)
@ResponseBody
public EdgeInstallInstructions getEdgeDockerInstallInstructions(
@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("edgeId") String strEdgeId,
HttpServletRequest request) throws ThingsboardException {
try {
EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
edgeId = checkNotNull(edgeId);
Edge edge = checkEdgeId(edgeId, Operation.READ);
return checkNotNull(edgeInstallService.getDockerInstallInstructions(getTenantId(), edge, request));
} catch (Exception e) {
throw handleException(e);
}
}
}

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

@ -426,11 +426,7 @@ public class EntityViewController extends BaseController {
checkParameter(ENTITY_VIEW_ID, strEntityViewId);
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
checkEntityViewId(entityViewId, Operation.ASSIGN_TO_CUSTOMER);
Customer publicCustomer = customerService.findOrCreatePublicCustomer(getTenantId());
return tbEntityViewService.assignEntityViewToPublicCustomer(getTenantId(), getCurrentUser().getCustomerId(),
publicCustomer, entityViewId, getCurrentUser());
return tbEntityViewService.assignEntityViewToPublicCustomer(getTenantId(), entityViewId, getCurrentUser());
}
@ApiOperation(value = "Assign entity view to edge (assignEntityViewToEdge)",

3
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -233,9 +233,8 @@ public class ThingsboardInstallService {
log.info("Upgrading ThingsBoard from version 3.4.1 to 3.4.2 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.4.1");
dataUpdateService.updateData("3.4.1");
break;
case "3.4.3":
log.info("Upgrading ThingsBoard from version 3.4.3 to 3.5 ...");
log.info("Upgrading ThingsBoard from version 3.4.3 to 3.5.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.4.3");
log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets();

94
application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallService.java

@ -0,0 +1,94 @@
/**
* Copyright © 2016-2022 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.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeInstallInstructions;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.install.InstallScripts;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
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 DefaultEdgeInstallService implements EdgeInstallService {
private static final String EDGE_DIR = "edge";
private static final String EDGE_INSTALL_INSTRUCTIONS_DIR = "install_instructions";
private final InstallScripts installScripts;
@Value("${edges.rpc.port}")
private int rpcPort;
@Value("${edges.rpc.ssl.enabled}")
private boolean sslEnabled;
@Value("${app.version:unknown}")
private String appVersion;
@Override
public EdgeInstallInstructions getDockerInstallInstructions(TenantId tenantId, Edge edge, HttpServletRequest request) {
String dockerInstallInstructions = readFile(resolveFile("docker", "instructions.md"));
String baseUrl = request.getServerName();
if (baseUrl.contains("localhost") || baseUrl.contains("127.0.0.1")) {
String localhostWarning = readFile(resolveFile("docker", "localhost_warning.md"));
dockerInstallInstructions = dockerInstallInstructions.replace("${LOCALHOST_WARNING}", localhostWarning);
dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", "!!!REPLACE_ME_TO_HOST_IP_ADDRESS!!!");
} else {
dockerInstallInstructions = dockerInstallInstructions.replace("${LOCALHOST_WARNING}", "");
dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", baseUrl);
}
dockerInstallInstructions = dockerInstallInstructions.replace("${TB_EDGE_VERSION}", appVersion + "EDGE");
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_ROUTING_KEY}", edge.getRoutingKey());
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_ROUTING_SECRET}", edge.getSecret());
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_RPC_PORT}", Integer.toString(rpcPort));
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_RPC_SSL_ENABLED}", Boolean.toString(sslEnabled));
return new EdgeInstallInstructions(dockerInstallInstructions);
}
private String readFile(Path file) {
try {
return new String(Files.readAllBytes(file), StandardCharsets.UTF_8);
} 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, EDGE_INSTALL_INSTRUCTIONS_DIR);
}
}

28
application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallService.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2022 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 org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeInstallInstructions;
import org.thingsboard.server.common.data.id.TenantId;
import javax.servlet.http.HttpServletRequest;
public interface EdgeInstallService {
EdgeInstallInstructions getDockerInstallInstructions(TenantId tenantId, Edge edge, HttpServletRequest request);
}

6
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -193,8 +193,10 @@ public final class EdgeGrpcSession implements Closeable {
private void doSync(EdgeSyncCursor cursor) {
if (cursor.hasNext()) {
log.info("[{}][{}] starting sync process, cursor current idx = {}", edge.getTenantId(), edge.getId(), cursor.getCurrentIdx());
ListenableFuture<UUID> uuidListenableFuture = startProcessingEdgeEvents(cursor.getNext());
EdgeEventFetcher next = cursor.getNext();
log.info("[{}][{}] starting sync process, cursor current idx = {}, class = {}",
edge.getTenantId(), edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName());
ListenableFuture<UUID> uuidListenableFuture = startProcessingEdgeEvents(next);
Futures.addCallback(uuidListenableFuture, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable UUID result) {

9
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.edge.rpc;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.edge.EdgeContextComponent;
@ -41,9 +42,9 @@ import java.util.NoSuchElementException;
public class EdgeSyncCursor {
List<EdgeEventFetcher> fetchers = new LinkedList<>();
private final List<EdgeEventFetcher> fetchers = new LinkedList<>();
int currentIdx = 0;
private int currentIdx = 0;
public EdgeSyncCursor(EdgeContextComponent ctx, Edge edge, boolean fullSync) {
if (fullSync) {
@ -53,8 +54,10 @@ public class EdgeSyncCursor {
fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService()));
fetchers.add(new AssetProfilesEdgeEventFetcher(ctx.getAssetProfileService()));
fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService()));
Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId());
fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId()));
if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) {
fetchers.add(new CustomerEdgeEventFetcher());
fetchers.add(new CustomerEdgeEventFetcher(edge.getCustomerId()));
fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId()));
}
}

4
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java

@ -49,6 +49,10 @@ public class AssetProfileMsgConstructor {
if (assetProfile.getImage() != null) {
builder.setImage(ByteString.copyFrom(assetProfile.getImage().getBytes(StandardCharsets.UTF_8)));
}
if (assetProfile.getDefaultEdgeRuleChainId() != null) {
builder.setDefaultRuleChainIdMSB(assetProfile.getDefaultEdgeRuleChainId().getId().getMostSignificantBits())
.setDefaultRuleChainIdLSB(assetProfile.getDefaultEdgeRuleChainId().getId().getLeastSignificantBits());
}
return builder.build();
}

4
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java

@ -66,6 +66,10 @@ public class DeviceMsgConstructor {
builder.setFirmwareIdMSB(device.getFirmwareId().getId().getMostSignificantBits())
.setFirmwareIdLSB(device.getFirmwareId().getId().getLeastSignificantBits());
}
if (device.getSoftwareId() != null) {
builder.setSoftwareIdMSB(device.getSoftwareId().getId().getMostSignificantBits())
.setSoftwareIdLSB(device.getSoftwareId().getId().getLeastSignificantBits());
}
if (conflictName != null) {
builder.setConflictName(conflictName);
}

12
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java

@ -65,6 +65,18 @@ public class DeviceProfileMsgConstructor {
builder.setFirmwareIdMSB(deviceProfile.getFirmwareId().getId().getMostSignificantBits())
.setFirmwareIdLSB(deviceProfile.getFirmwareId().getId().getLeastSignificantBits());
}
if (deviceProfile.getSoftwareId() != null) {
builder.setSoftwareIdMSB(deviceProfile.getSoftwareId().getId().getMostSignificantBits())
.setSoftwareIdLSB(deviceProfile.getSoftwareId().getId().getLeastSignificantBits());
}
if (deviceProfile.getDefaultEdgeRuleChainId() != null) {
builder.setDefaultRuleChainIdMSB(deviceProfile.getDefaultEdgeRuleChainId().getId().getMostSignificantBits())
.setDefaultRuleChainIdLSB(deviceProfile.getDefaultEdgeRuleChainId().getId().getLeastSignificantBits());
}
if (deviceProfile.getDefaultDashboardId() != null) {
builder.setDefaultDashboardIdMSB(deviceProfile.getDefaultDashboardId().getId().getMostSignificantBits())
.setDefaultDashboardIdLSB(deviceProfile.getDefaultDashboardId().getId().getLeastSignificantBits());
}
return builder.build();
}

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CustomerEdgeEventFetcher.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -33,6 +34,8 @@ import java.util.List;
@AllArgsConstructor
public class CustomerEdgeEventFetcher implements EdgeEventFetcher {
private final CustomerId customerId;
@Override
public PageLink getPageLink(int pageSize) {
return null;
@ -42,7 +45,7 @@ public class CustomerEdgeEventFetcher implements EdgeEventFetcher {
public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) {
List<EdgeEvent> result = new ArrayList<>();
result.add(EdgeUtils.constructEdgeEvent(edge.getTenantId(), edge.getId(),
EdgeEventType.CUSTOMER, EdgeEventActionType.ADDED, edge.getCustomerId(), null));
EdgeEventType.CUSTOMER, EdgeEventActionType.ADDED, customerId, null));
// @voba - returns PageData object to be in sync with other fetchers
return new PageData<>(result, 1, result.size(), false);
}

6
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java

@ -306,6 +306,12 @@ public abstract class BaseEdgeProcessor {
return futures;
}
protected ListenableFuture<Void> handleUnsupportedMsgType(UpdateMsgType msgType) {
String errMsg = String.format("Unsupported msg type %s", msgType);
log.error(errMsg);
return Futures.immediateFailedFuture(new RuntimeException(errMsg));
}
protected UpdateMsgType getUpdateMsgType(EdgeEventActionType actionType) {
switch (actionType) {
case UPDATED:

7
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java

@ -132,8 +132,7 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor {
return Futures.immediateFuture(null);
case UNRECOGNIZED:
default:
log.error("Unsupported msg type {}", deviceUpdateMsg.getMsgType());
return Futures.immediateFailedFuture(new RuntimeException("Unsupported msg type " + deviceUpdateMsg.getMsgType()));
return handleUnsupportedMsgType(deviceUpdateMsg.getMsgType());
}
}
@ -246,11 +245,9 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor {
if (deviceDataOpt.isPresent()) {
device.setDeviceData(deviceDataOpt.get());
}
deviceValidator.validate(device, Device::getTenantId);
if (created) {
deviceValidator.validate(device, Device::getTenantId);
device.setId(deviceId);
} else {
deviceValidator.validate(device, Device::getTenantId);
}
Device savedDevice = deviceService.saveDevice(device, false);
tbClusterService.onDeviceUpdated(savedDevice, created ? null : device, false);

16
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RelationEdgeProcessor.java

@ -80,23 +80,25 @@ public class RelationEdgeProcessor extends BaseEdgeProcessor {
case ENTITY_UPDATED_RPC_MESSAGE:
if (isEntityExists(tenantId, entityRelation.getTo())
&& isEntityExists(tenantId, entityRelation.getFrom())) {
relationService.saveRelationAsync(tenantId, entityRelation);
return Futures.transform(relationService.saveRelationAsync(tenantId, entityRelation),
(result) -> null, dbCallbackExecutorService);
} else {
log.warn("Skipping relating update msg because from/to entity doesn't exists on edge, {}", relationUpdateMsg);
return Futures.immediateFuture(null);
}
break;
case ENTITY_DELETED_RPC_MESSAGE:
relationService.deleteRelation(tenantId, entityRelation);
break;
return Futures.transform(relationService.deleteRelationAsync(tenantId, entityRelation),
(result) -> null, dbCallbackExecutorService);
case UNRECOGNIZED:
log.error("Unsupported msg type");
default:
return handleUnsupportedMsgType(relationUpdateMsg.getMsgType());
}
return Futures.immediateFuture(null);
} catch (Exception e) {
log.error("Failed to process relation update msg [{}]", relationUpdateMsg, e);
return Futures.immediateFailedFuture(new RuntimeException("Failed to process relation update msg", e));
}
}
private boolean isEntityExists(TenantId tenantId, EntityId entityId) throws ThingsboardException {
switch (entityId.getEntityType()) {
case DEVICE:

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java

@ -22,6 +22,7 @@ import com.google.common.util.concurrent.SettableFuture;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
@ -334,7 +335,7 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor {
return null;
}
return constructEntityDataProtoMsg(entityId, edgeEvent.getAction(),
JsonUtils.parse(JacksonUtil.OBJECT_MAPPER.writeValueAsString(edgeEvent.getBody())));
JsonParser.parseString(JacksonUtil.OBJECT_MAPPER.writeValueAsString(edgeEvent.getBody())));
}
private DownlinkMsg constructEntityDataProtoMsg(EntityId entityId, EdgeEventActionType actionType, JsonElement entityData) {

81
application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java

@ -137,49 +137,50 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
}
String scope = attributesRequestMsg.getScope();
ListenableFuture<List<AttributeKvEntry>> findAttrFuture = attributesService.findAll(tenantId, entityId, scope);
return Futures.transformAsync(findAttrFuture, ssAttributes -> {
if (ssAttributes == null || ssAttributes.isEmpty()) {
log.trace("[{}][{}] No attributes found for entity {} [{}]", tenantId,
edge.getName(),
entityId.getEntityType(),
entityId.getId());
return Futures.immediateFuture(null);
}
return processEntityAttributesAndAddToEdgeQueue(tenantId, entityId, edge, entityType, scope, ssAttributes, attributesRequestMsg);
}, dbCallbackExecutorService);
return Futures.transformAsync(findAttrFuture, ssAttributes
-> processEntityAttributesAndAddToEdgeQueue(tenantId, entityId, edge, entityType, scope, ssAttributes, attributesRequestMsg),
dbCallbackExecutorService);
}
private ListenableFuture<Void> processEntityAttributesAndAddToEdgeQueue(TenantId tenantId, EntityId entityId, Edge edge,
EdgeEventType entityType, String scope, List<AttributeKvEntry> ssAttributes,
AttributesRequestMsg attributesRequestMsg) {
try {
Map<String, Object> entityData = new HashMap<>();
ObjectNode attributes = JacksonUtil.OBJECT_MAPPER.createObjectNode();
for (AttributeKvEntry attr : ssAttributes) {
if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(attr.getKey())
&& !DefaultDeviceStateService.INACTIVITY_TIMEOUT.equals(attr.getKey())) {
continue;
ListenableFuture<Void> future;
if (ssAttributes == null || ssAttributes.isEmpty()) {
log.trace("[{}][{}] No attributes found for entity {} [{}]", tenantId,
edge.getName(),
entityId.getEntityType(),
entityId.getId());
future = Futures.immediateFuture(null);
} else {
Map<String, Object> entityData = new HashMap<>();
ObjectNode attributes = JacksonUtil.OBJECT_MAPPER.createObjectNode();
for (AttributeKvEntry attr : ssAttributes) {
if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(attr.getKey())
&& !DefaultDeviceStateService.INACTIVITY_TIMEOUT.equals(attr.getKey())) {
continue;
}
if (attr.getDataType() == DataType.BOOLEAN && attr.getBooleanValue().isPresent()) {
attributes.put(attr.getKey(), attr.getBooleanValue().get());
} else if (attr.getDataType() == DataType.DOUBLE && attr.getDoubleValue().isPresent()) {
attributes.put(attr.getKey(), attr.getDoubleValue().get());
} else if (attr.getDataType() == DataType.LONG && attr.getLongValue().isPresent()) {
attributes.put(attr.getKey(), attr.getLongValue().get());
} else {
attributes.put(attr.getKey(), attr.getValueAsString());
}
}
if (attr.getDataType() == DataType.BOOLEAN && attr.getBooleanValue().isPresent()) {
attributes.put(attr.getKey(), attr.getBooleanValue().get());
} else if (attr.getDataType() == DataType.DOUBLE && attr.getDoubleValue().isPresent()) {
attributes.put(attr.getKey(), attr.getDoubleValue().get());
} else if (attr.getDataType() == DataType.LONG && attr.getLongValue().isPresent()) {
attributes.put(attr.getKey(), attr.getLongValue().get());
if (attributes.size() > 0) {
entityData.put("kv", attributes);
entityData.put("scope", scope);
JsonNode body = JacksonUtil.OBJECT_MAPPER.valueToTree(entityData);
log.debug("Sending attributes data msg, entityId [{}], attributes [{}]", entityId, body);
future = saveEdgeEvent(tenantId, edge.getId(), entityType, EdgeEventActionType.ATTRIBUTES_UPDATED, entityId, body);
} else {
attributes.put(attr.getKey(), attr.getValueAsString());
future = Futures.immediateFuture(null);
}
}
ListenableFuture<Void> future;
if (attributes.size() > 0) {
entityData.put("kv", attributes);
entityData.put("scope", scope);
JsonNode body = JacksonUtil.OBJECT_MAPPER.valueToTree(entityData);
log.debug("Sending attributes data msg, entityId [{}], attributes [{}]", entityId, body);
future = saveEdgeEvent(tenantId, edge.getId(), entityType, EdgeEventActionType.ATTRIBUTES_UPDATED, entityId, body);
} else {
future = Futures.immediateFuture(null);
}
return Futures.transformAsync(future, v -> processLatestTimeseriesAndAddToEdgeQueue(tenantId, entityId, edge, entityType), dbCallbackExecutorService);
} catch (Exception e) {
String errMsg = String.format("[%s] Failed to save attribute updates to the edge [%s]", edge.getId(), attributesRequestMsg);
@ -199,16 +200,18 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
entityId.getId());
return Futures.immediateFuture(null);
}
List<ListenableFuture<Void>> futures = new ArrayList<>();
Map<Long, Map<String, Object>> tsData = new HashMap<>();
for (TsKvEntry tsKvEntry : tsKvEntries) {
if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(tsKvEntry.getKey())) {
continue;
}
ObjectNode entityBody = JacksonUtil.OBJECT_MAPPER.createObjectNode();
ObjectNode ts = JacksonUtil.OBJECT_MAPPER.createObjectNode();
ts.put(tsKvEntry.getKey(), tsKvEntry.getValueAsString());
entityBody.set("data", ts);
entityBody.put("ts", tsKvEntry.getTs());
tsData.computeIfAbsent(tsKvEntry.getTs(), k -> new HashMap<>()).put(tsKvEntry.getKey(), tsKvEntry.getValue());
}
List<ListenableFuture<Void>> futures = new ArrayList<>();
for (Map.Entry<Long, Map<String, Object>> entry : tsData.entrySet()) {
Map<String, Object> entityBody = new HashMap<>();
entityBody.put("data", entry.getValue());
entityBody.put("ts", entry.getKey());
futures.add(saveEdgeEvent(tenantId, edge.getId(), entityType, EdgeEventActionType.TIMESERIES_UPDATED, entityId, JacksonUtil.valueToTree(entityBody)));
}
return Futures.transform(Futures.allAsList(futures), v -> null, dbCallbackExecutorService);

8
application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java

@ -131,13 +131,9 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS
public <E extends HasName, I extends EntityId> void notifyAssignOrUnassignEntityToCustomer(TenantId tenantId, I entityId,
CustomerId customerId, E entity,
ActionType actionType,
User user, boolean sendToEdge,
Object... additionalInfo) {
User user, Object... additionalInfo) {
logEntityAction(tenantId, entityId, entity, customerId, actionType, user, additionalInfo);
if (sendToEdge) {
sendEntityNotificationMsg(tenantId, entityId, edgeTypeByActionType(actionType), JacksonUtil.toString(customerId));
}
sendEntityNotificationMsg(tenantId, entityId, edgeTypeByActionType(actionType), JacksonUtil.toString(customerId));
}
@Override

3
application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java

@ -74,8 +74,7 @@ public interface TbNotificationEntityService {
<E extends HasName, I extends EntityId> void notifyAssignOrUnassignEntityToCustomer(TenantId tenantId, I entityId,
CustomerId customerId, E entity,
ActionType actionType,
User user, boolean sendToEdge,
Object... additionalInfo);
User user, Object... additionalInfo);
<E extends HasName, I extends EntityId> void notifyAssignOrUnassignEntityToEdge(TenantId tenantId, I entityId,
CustomerId customerId, EdgeId edgeId,

6
application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java

@ -98,7 +98,7 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb
try {
Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(tenantId, assetId, customerId));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, assetId, customerId, savedAsset,
actionType, user, true, assetId.toString(), customerId.toString(), customer.getName());
actionType, user, assetId.toString(), customerId.toString(), customer.getName());
return savedAsset;
} catch (Exception e) {
@ -115,7 +115,7 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb
Asset savedAsset = checkNotNull(assetService.unassignAssetFromCustomer(tenantId, assetId));
CustomerId customerId = customer.getId();
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, assetId, customerId, savedAsset,
actionType, user, true, assetId.toString(), customerId.toString(), customer.getName());
actionType, user, assetId.toString(), customerId.toString(), customer.getName());
return savedAsset;
} catch (Exception e) {
@ -131,7 +131,7 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb
Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId);
Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(tenantId, assetId, publicCustomer.getId()));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, assetId, savedAsset.getCustomerId(), savedAsset,
actionType, user, false, actionType.toString(), publicCustomer.getId().toString(), publicCustomer.getName());
actionType, user, assetId.toString(), publicCustomer.getId().toString(), publicCustomer.getName());
return savedAsset;
} catch (Exception e) {

16
application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java

@ -84,7 +84,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement
try {
Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, customerId));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customerId, savedDashboard,
actionType, user, true, dashboardId.toString(), customerId.toString(), customer.getName());
actionType, user, dashboardId.toString(), customerId.toString(), customer.getName());
return savedDashboard;
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DASHBOARD), actionType,
@ -102,7 +102,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement
Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId);
Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, publicCustomer.getId()));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, publicCustomer.getId(), savedDashboard,
actionType, user, false, dashboardId.toString(),
actionType, user, dashboardId.toString(),
publicCustomer.getId().toString(), publicCustomer.getName());
return savedDashboard;
} catch (Exception e) {
@ -120,7 +120,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement
Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId);
Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboardId, publicCustomer.getId()));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, publicCustomer.getId(), dashboard,
actionType, user, false, dashboardId.toString(),
actionType, user, dashboardId.toString(),
publicCustomer.getId().toString(), publicCustomer.getName());
return savedDashboard;
} catch (Exception e) {
@ -160,14 +160,14 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement
savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, customerId));
ShortCustomerInfo customerInfo = savedDashboard.getAssignedCustomerInfo(customerId);
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, savedDashboard.getId(), customerId, savedDashboard,
actionType, user, true, dashboardId.toString(), customerId.toString(), customerInfo.getTitle());
actionType, user, dashboardId.toString(), customerId.toString(), customerInfo.getTitle());
}
actionType = ActionType.UNASSIGNED_FROM_CUSTOMER;
for (CustomerId customerId : removedCustomerIds) {
ShortCustomerInfo customerInfo = dashboard.getAssignedCustomerInfo(customerId);
savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboardId, customerId));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, savedDashboard.getId(), customerId, savedDashboard,
ActionType.UNASSIGNED_FROM_CUSTOMER, user, true, dashboardId.toString(), customerId.toString(), customerInfo.getTitle());
ActionType.UNASSIGNED_FROM_CUSTOMER, user, dashboardId.toString(), customerId.toString(), customerInfo.getTitle());
}
return savedDashboard;
}
@ -197,7 +197,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement
savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(tenantId, dashboardId, customerId));
ShortCustomerInfo customerInfo = savedDashboard.getAssignedCustomerInfo(customerId);
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customerId, savedDashboard,
actionType, user, true, dashboardId.toString(), customerId.toString(), customerInfo.getTitle());
actionType, user, dashboardId.toString(), customerId.toString(), customerInfo.getTitle());
}
return savedDashboard;
}
@ -227,7 +227,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement
ShortCustomerInfo customerInfo = dashboard.getAssignedCustomerInfo(customerId);
savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboardId, customerId));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customerId, savedDashboard,
actionType, user, true, dashboardId.toString(), customerId.toString(), customerInfo.getTitle());
actionType, user, dashboardId.toString(), customerId.toString(), customerInfo.getTitle());
}
return savedDashboard;
}
@ -282,7 +282,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement
try {
Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(tenantId, dashboardId, customer.getId()));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, dashboardId, customer.getId(), savedDashboard,
actionType, user, true, dashboardId.toString(), customer.getId().toString(), customer.getName());
actionType, user, dashboardId.toString(), customer.getId().toString(), customer.getName());
return savedDashboard;
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DASHBOARD), actionType, user, e, dashboardId.toString());

6
application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java

@ -117,7 +117,7 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T
try {
Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(tenantId, deviceId, customerId));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, deviceId, customerId, savedDevice,
actionType, user, true, deviceId.toString(), customerId.toString(), customer.getName());
actionType, user, deviceId.toString(), customerId.toString(), customer.getName());
return savedDevice;
} catch (Exception e) {
@ -137,7 +137,7 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T
CustomerId customerId = customer.getId();
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, deviceId, customerId, savedDevice,
actionType, user, true, deviceId.toString(), customerId.toString(), customer.getName());
actionType, user, deviceId.toString(), customerId.toString(), customer.getName());
return savedDevice;
} catch (Exception e) {
@ -155,7 +155,7 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T
Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(tenantId, deviceId, publicCustomer.getId()));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, deviceId, savedDevice.getCustomerId(), savedDevice,
actionType, user, false, deviceId.toString(),
actionType, user, deviceId.toString(),
publicCustomer.getId().toString(), publicCustomer.getName());
return savedDevice;

4
application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java

@ -90,7 +90,7 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE
try {
Edge savedEdge = checkNotNull(edgeService.assignEdgeToCustomer(tenantId, edgeId, customerId));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, edgeId, customerId, savedEdge,
actionType, user, true, edgeId.toString(), customerId.toString(), customer.getName());
actionType, user, edgeId.toString(), customerId.toString(), customer.getName());
return savedEdge;
} catch (Exception e) {
@ -109,7 +109,7 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE
try {
Edge savedEdge = checkNotNull(edgeService.unassignEdgeFromCustomer(tenantId, edgeId));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, edgeId, customerId, savedEdge,
actionType, user, true, edgeId.toString(), customerId.toString(), customer.getName());
actionType, user, edgeId.toString(), customerId.toString(), customer.getName());
return savedEdge;
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.EDGE),

20
application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java

@ -149,7 +149,7 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen
try {
EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToCustomer(tenantId, entityViewId, customerId));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, customerId, savedEntityView,
ActionType.ASSIGNED_TO_CUSTOMER, user, true, entityViewId.toString(), customerId.toString(), customer.getName());
ActionType.ASSIGNED_TO_CUSTOMER, user, entityViewId.toString(), customerId.toString(), customer.getName());
return savedEntityView;
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ENTITY_VIEW),
@ -159,18 +159,19 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen
}
@Override
public EntityView assignEntityViewToPublicCustomer(TenantId tenantId, CustomerId customerId, Customer publicCustomer,
EntityViewId entityViewId, User user) throws ThingsboardException {
public EntityView assignEntityViewToPublicCustomer(TenantId tenantId, EntityViewId entityViewId, User user) throws ThingsboardException {
ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER;
Customer publicCustomer = customerService.findOrCreatePublicCustomer(tenantId);
try {
EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToCustomer(tenantId,
entityViewId, publicCustomer.getId()));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, customerId, savedEntityView,
ActionType.ASSIGNED_TO_CUSTOMER, user, false, savedEntityView.getEntityId().toString(),
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, savedEntityView.getCustomerId(), savedEntityView,
actionType, user, savedEntityView.getId().toString(),
publicCustomer.getId().toString(), publicCustomer.getName());
return savedEntityView;
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ENTITY_VIEW),
ActionType.ASSIGNED_TO_CUSTOMER, user, e, entityViewId.toString());
actionType, user, e, entityViewId.toString());
throw e;
}
}
@ -211,14 +212,17 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen
@Override
public EntityView unassignEntityViewFromCustomer(TenantId tenantId, EntityViewId entityViewId, Customer customer, User user) throws ThingsboardException {
ActionType actionType = ActionType.UNASSIGNED_FROM_CUSTOMER;
try {
EntityView savedEntityView = checkNotNull(entityViewService.unassignEntityViewFromCustomer(tenantId, entityViewId));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, customer.getId(), savedEntityView,
ActionType.UNASSIGNED_FROM_CUSTOMER, user, true, customer.getId().toString(), customer.getName());
actionType, user, savedEntityView.getId().toString(), customer.getId().toString(), customer.getName());
return savedEntityView;
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ENTITY_VIEW),
ActionType.UNASSIGNED_FROM_CUSTOMER, user, e, entityViewId.toString());
actionType, user, e, entityViewId.toString());
throw e;
}
}

3
application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java

@ -39,8 +39,7 @@ public interface TbEntityViewService extends ComponentLifecycleListener {
EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, Customer customer, User user) throws ThingsboardException;
EntityView assignEntityViewToPublicCustomer(TenantId tenantId, CustomerId customerId, Customer publicCustomer,
EntityViewId entityViewId, User user) throws ThingsboardException;
EntityView assignEntityViewToPublicCustomer(TenantId tenantId, EntityViewId entityViewId, User user) throws ThingsboardException;
EntityView assignEntityViewToEdge(TenantId tenantId, CustomerId customerId, EntityViewId entityViewId, Edge edge, User user) throws ThingsboardException;

27
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -680,10 +680,29 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
case "3.4.3":
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
log.info("Updating schema ...");
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.4.3", SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, conn);
log.info("Updating schema settings...");
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3004004;");
if (isOldSchema(conn, 3004002)) {
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.4.3", SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, conn);
try {
conn.createStatement().execute("ALTER TABLE asset_profile ADD COLUMN default_edge_rule_chain_id uuid"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
} catch (Exception e) {
}
try {
conn.createStatement().execute("ALTER TABLE device_profile ADD COLUMN default_edge_rule_chain_id uuid"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
} catch (Exception e) {
}
try {
conn.createStatement().execute("ALTER TABLE asset_profile ADD CONSTRAINT fk_default_edge_rule_chain_asset_profile FOREIGN KEY (default_edge_rule_chain_id) REFERENCES rule_chain(id)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
} catch (Exception e) {
}
try {
conn.createStatement().execute("ALTER TABLE device_profile ADD CONSTRAINT fk_default_edge_rule_chain_device_profile FOREIGN KEY (default_edge_rule_chain_id) REFERENCES rule_chain(id)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
} catch (Exception e) {
}
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005000;");
}
log.info("Schema updated.");
} catch (Exception e) {
log.error("Failed updating schema!!!", e);

8
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -704,16 +704,16 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
}
protected Edge constructEdge(String name, String type) {
return constructEdge(tenantId, name, type);
return constructEdge(tenantId, name, type, StringUtils.randomAlphanumeric(20), StringUtils.randomAlphanumeric(20));
}
protected Edge constructEdge(TenantId tenantId, String name, String type) {
protected Edge constructEdge(TenantId tenantId, String name, String type, String routingKey, String secret) {
Edge edge = new Edge();
edge.setTenantId(tenantId);
edge.setName(name);
edge.setType(type);
edge.setSecret(StringUtils.randomAlphanumeric(20));
edge.setRoutingKey(StringUtils.randomAlphanumeric(20));
edge.setRoutingKey(routingKey);
edge.setSecret(secret);
return edge;
}

35
application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java

@ -398,6 +398,41 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest {
Assert.assertEquals(ModelConstants.NULL_UUID, foundAsset.getCustomerId().getId());
}
@Test
public void testAssignUnassignAssetToPublicCustomer() throws Exception {
Asset asset = new Asset();
asset.setName("My asset");
asset.setType("default");
Asset savedAsset = doPost("/api/asset", asset, Asset.class);
Mockito.reset(tbClusterService, auditLogService);
Asset assignedAsset = doPost("/api/customer/public/asset/" + savedAsset.getId().getId().toString(), Asset.class);
Customer publicCustomer = doGet("/api/customer/" + assignedAsset.getCustomerId(), Customer.class);
Assert.assertTrue(publicCustomer.isPublic());
testNotifyEntityAllOneTime(assignedAsset, assignedAsset.getId(), assignedAsset.getId(),
savedTenant.getId(), publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.ASSIGNED_TO_CUSTOMER, assignedAsset.getId().toString(), publicCustomer.getId().toString(), publicCustomer.getTitle());
Asset foundAsset = doGet("/api/asset/" + savedAsset.getId().getId().toString(), Asset.class);
Assert.assertEquals(publicCustomer.getId(), foundAsset.getCustomerId());
Mockito.reset(tbClusterService, auditLogService);
Asset unassignedAsset =
doDelete("/api/customer/asset/" + savedAsset.getId().getId().toString(), Asset.class);
Assert.assertEquals(ModelConstants.NULL_UUID, unassignedAsset.getCustomerId().getId());
testNotifyEntityAllOneTime(savedAsset, savedAsset.getId(), savedAsset.getId(),
savedTenant.getId(), publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.UNASSIGNED_FROM_CUSTOMER, savedAsset.getId().toString(), publicCustomer.getId().toString(), publicCustomer.getTitle());
foundAsset = doGet("/api/asset/" + savedAsset.getId().getId().toString(), Asset.class);
Assert.assertEquals(ModelConstants.NULL_UUID, foundAsset.getCustomerId().getId());
}
@Test
public void testAssignAssetToNonExistentCustomer() throws Exception {
Asset asset = new Asset();

44
application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java

@ -30,6 +30,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.ShortCustomerInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
@ -216,6 +217,49 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest
Assert.assertTrue(foundDashboard.getAssignedCustomers() == null || foundDashboard.getAssignedCustomers().isEmpty());
}
@Test
public void testAssignUnassignDashboardToPublicCustomer() throws Exception {
Dashboard dashboard = new Dashboard();
dashboard.setTitle("My dashboard");
Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
Mockito.reset(tbClusterService, auditLogService);
Dashboard assignedDashboard = doPost("/api/customer/public/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
CustomerId publicCustomerId = null;
for (ShortCustomerInfo assignedCustomer : assignedDashboard.getAssignedCustomers()) {
if (assignedCustomer.isPublic()) {
publicCustomerId = assignedCustomer.getCustomerId();
}
}
Assert.assertNotNull(publicCustomerId);
Customer publicCustomer = doGet("/api/customer/" + publicCustomerId, Customer.class);
Assert.assertTrue(publicCustomer.isPublic());
testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(assignedDashboard, assignedDashboard.getId(), assignedDashboard.getId(),
savedTenant.getId(), publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ASSIGNED_TO_CUSTOMER,
assignedDashboard .getId().getId().toString(), publicCustomer.getId().getId().toString(), publicCustomer.getTitle());
Dashboard foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
Assert.assertTrue(foundDashboard.getAssignedCustomers().contains(publicCustomer.toShortCustomerInfo()));
Mockito.reset(tbClusterService, auditLogService);
Dashboard unassignedDashboard =
doDelete("/api/customer/public/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(assignedDashboard, assignedDashboard.getId(), assignedDashboard.getId(),
savedTenant.getId(), publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.UNASSIGNED_FROM_CUSTOMER,
unassignedDashboard.getId().getId().toString(), publicCustomer.getId().getId().toString(), publicCustomer.getTitle());
Assert.assertTrue(unassignedDashboard.getAssignedCustomers() == null || unassignedDashboard.getAssignedCustomers().isEmpty());
foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
Assert.assertTrue(foundDashboard.getAssignedCustomers() == null || foundDashboard.getAssignedCustomers().isEmpty());
}
@Test
public void testAssignDashboardToNonExistentCustomer() throws Exception {
Dashboard dashboard = new Dashboard();

39
application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java

@ -521,6 +521,45 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest {
Assert.assertEquals(ModelConstants.NULL_UUID, foundDevice.getCustomerId().getId());
}
@Test
public void testAssignUnassignDeviceToPublicCustomer() throws Exception {
Device device = new Device();
device.setName("My device");
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
Mockito.reset(tbClusterService, auditLogService, gatewayNotificationsService);
Device assignedDevice = doPost("/api/customer/public/device/" + savedDevice.getId().getId(), Device.class);
Customer publicCustomer = doGet("/api/customer/" + assignedDevice.getCustomerId(), Customer.class);
Assert.assertTrue(publicCustomer.isPublic());
testNotifyEntityAllOneTime(assignedDevice, assignedDevice.getId(), assignedDevice.getId(), savedTenant.getId(),
publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ASSIGNED_TO_CUSTOMER,
assignedDevice.getId().getId().toString(), publicCustomer.getId().getId().toString(),
publicCustomer.getTitle());
testNotificationUpdateGatewayNever();
Device foundDevice = doGet("/api/device/" + savedDevice.getId().getId(), Device.class);
Assert.assertEquals(publicCustomer.getId(), foundDevice.getCustomerId());
Mockito.reset(tbClusterService, auditLogService, gatewayNotificationsService);
Device unassignedDevice =
doDelete("/api/customer/device/" + savedDevice.getId().getId(), Device.class);
Assert.assertEquals(ModelConstants.NULL_UUID, unassignedDevice.getCustomerId().getId());
testNotifyEntityAllOneTime(unassignedDevice, unassignedDevice.getId(), unassignedDevice.getId(), savedTenant.getId(),
publicCustomer.getId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.UNASSIGNED_FROM_CUSTOMER,
unassignedDevice.getId().getId().toString(), publicCustomer.getId().getId().toString(),
publicCustomer.getTitle());
testNotificationDeleteGatewayNever();
foundDevice = doGet("/api/device/" + savedDevice.getId().getId(), Device.class);
Assert.assertEquals(ModelConstants.NULL_UUID, foundDevice.getCustomerId().getId());
}
@Test
public void testAssignDeviceToNonExistentCustomer() throws Exception {
Device device = new Device();

24
application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java

@ -50,6 +50,7 @@ import org.thingsboard.server.edge.imitator.EdgeImitator;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg;
@ -57,6 +58,10 @@ import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import java.io.File;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -92,7 +97,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest {
}
}
@Before
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
@ -327,7 +332,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest {
String customerIdStr = customerId.getId().toString();
String msgError = msgErrorNoFound("Customer", customerIdStr);
doPost("/api/customer/" + customerIdStr+ "/edge/" + savedEdge.getId().getId().toString())
doPost("/api/customer/" + customerIdStr + "/edge/" + savedEdge.getId().getId().toString())
.andExpect(status().isNotFound())
.andExpect(statusReason(containsString(msgError)));
@ -820,7 +825,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest {
EdgeImitator edgeImitator = new EdgeImitator(EDGE_HOST, EDGE_PORT, edge.getRoutingKey(), edge.getSecret());
edgeImitator.ignoreType(UserCredentialsUpdateMsg.class);
edgeImitator.expectMessageAmount(19);
edgeImitator.expectMessageAmount(20);
edgeImitator.connect();
assertThat(edgeImitator.waitForMessages()).as("await for messages on first connect").isTrue();
@ -833,9 +838,10 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest {
assertThat(edgeImitator.findAllMessagesByType(AssetUpdateMsg.class)).as("two msgs - one during sync process, and one more once asset assigned to edge").hasSize(2);
assertThat(edgeImitator.findAllMessagesByType(UserUpdateMsg.class)).as("one msg during sync process for tenant admin user").hasSize(1);
assertThat(edgeImitator.findAllMessagesByType(AdminSettingsUpdateMsg.class)).as("admin setting update").hasSize(4);
assertThat(edgeImitator.findAllMessagesByType(CustomerUpdateMsg.class)).as("one msg during sync process for 'Public' customer").hasSize(1);
verifyRuleChainMsgsAreRoot(ruleChainUpdateMsgs);
edgeImitator.expectMessageAmount(14);
edgeImitator.expectMessageAmount(15);
doPost("/api/edge/sync/" + edge.getId());
assertThat(edgeImitator.waitForMessages()).as("await for messages after edge sync rest api call").isTrue();
@ -848,6 +854,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest {
assertThat(edgeImitator.findAllMessagesByType(UserUpdateMsg.class)).as("user update msg").hasSize(1);
assertThat(edgeImitator.findAllMessagesByType(AdminSettingsUpdateMsg.class)).as("admin setting update msg").hasSize(4);
assertThat(edgeImitator.findAllMessagesByType(DeviceUpdateMsg.class)).as("asset update msg").hasSize(1);
assertThat(edgeImitator.findAllMessagesByType(CustomerUpdateMsg.class)).as("one msg during sync process for 'Public' customer").hasSize(1);
verifyRuleChainMsgsAreRoot(ruleChainUpdateMsgs);
edgeImitator.allowIgnoredTypes();
@ -886,4 +893,13 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest {
Edge edge = constructEdge(name, "default");
return doPost("/api/edge", edge, Edge.class);
}
@Test
public void testGetEdgeInstallInstructions() throws Exception {
Edge edge = constructEdge(tenantId, "Edge for Test Docker Install Instructions", "default", "7390c3a6-69b0-9910-d155-b90aca4b772e", "l7q4zsjplzwhk16geqxy");
Edge savedEdge = doPost("/api/edge", edge, Edge.class);
String installInstructions = doGet("/api/edge/instructions/" + savedEdge.getId().getId().toString(), String.class);
Assert.assertTrue(installInstructions.contains("l7q4zsjplzwhk16geqxy"));
Assert.assertTrue(installInstructions.contains("7390c3a6-69b0-9910-d155-b90aca4b772e"));
}
}

37
application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java

@ -310,7 +310,40 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
testNotifyEntityAllOneTime(unAssignedView, savedView.getId(), savedView.getId(),
tenantId, savedView.getCustomerId(), tenantAdminUserId, TENANT_ADMIN_EMAIL,
ActionType.UNASSIGNED_FROM_CUSTOMER,
savedView.getCustomerId().getId().toString(), savedCustomer.getTitle());
assignedView.getId().getId().toString(), savedView.getCustomerId().getId().toString(), savedCustomer.getTitle());
}
@Test
public void testAssignAndUnAssignedEntityViewToPublicCustomer() throws Exception {
EntityView savedView = getNewSavedEntityView("Test entity view");
Mockito.reset(tbClusterService, auditLogService);
EntityView assignedView = doPost(
"/api/customer/public/entityView/" + savedView.getId().getId().toString(),
EntityView.class);
Customer publicCustomer = doGet("/api/customer/" + assignedView.getCustomerId(), Customer.class);
Assert.assertTrue(publicCustomer.isPublic());
testBroadcastEntityStateChangeEventNever(assignedView.getId());
testNotifyEntityAllOneTime(assignedView, assignedView.getId(), assignedView.getId(),
tenantId, assignedView.getCustomerId(), tenantAdminUserId, TENANT_ADMIN_EMAIL,
ActionType.ASSIGNED_TO_CUSTOMER,
assignedView.getId().getId().toString(), assignedView.getCustomerId().getId().toString(), publicCustomer.getTitle());
EntityView foundView = doGet("/api/entityView/" + savedView.getId().getId().toString(), EntityView.class);
assertEquals(publicCustomer.getId(), foundView.getCustomerId());
EntityView unAssignedView = doDelete("/api/customer/entityView/" + savedView.getId().getId().toString(), EntityView.class);
assertEquals(ModelConstants.NULL_UUID, unAssignedView.getCustomerId().getId());
foundView = doGet("/api/entityView/" + savedView.getId().getId().toString(), EntityView.class);
assertEquals(ModelConstants.NULL_UUID, foundView.getCustomerId().getId());
testBroadcastEntityStateChangeEventNever(foundView.getId());
testNotifyEntityAllOneTime(unAssignedView, unAssignedView.getId(), unAssignedView.getId(),
tenantId, publicCustomer.getId(), tenantAdminUserId, TENANT_ADMIN_EMAIL,
ActionType.UNASSIGNED_FROM_CUSTOMER,
unAssignedView.getId().getId().toString(), publicCustomer.getId().getId().toString(), publicCustomer.getTitle());
}
@Test
@ -428,7 +461,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
testBroadcastEntityStateChangeEventNever(loadedNamesOfView1.get(0).getId());
testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAnyAdditionalInfoAny(new EntityView(), new EntityView(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL,
ActionType.UNASSIGNED_FROM_CUSTOMER, ActionType.UNASSIGNED_FROM_CUSTOMER, cntEntity, cntEntity, 2);
ActionType.UNASSIGNED_FROM_CUSTOMER, ActionType.UNASSIGNED_FROM_CUSTOMER, cntEntity, cntEntity, 3);
PageData<EntityView> pageData = doGetTypedWithPageLink(urlTemplate, PAGE_DATA_ENTITY_VIEW_TYPE_REF,
new PageLink(4, 0, name1));

73
application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java

@ -27,6 +27,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.OtaPackageInfo;
@ -51,11 +53,13 @@ import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.query.EntityKeyValueType;
@ -63,6 +67,7 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.NumericFilterPredicate;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.edge.EdgeEventService;
@ -70,6 +75,7 @@ import org.thingsboard.server.edge.imitator.EdgeImitator;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.EdgeConfiguration;
@ -142,7 +148,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
installation();
edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret());
edgeImitator.expectMessageAmount(21);
edgeImitator.expectMessageAmount(22);
edgeImitator.connect();
requestEdgeRuleChainMetadata();
@ -269,6 +275,9 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
// 1 message from user fetcher
validateUsers();
// 1 message from public customer fetcher
validatePublicCustomer();
}
private void validateEdgeConfiguration() throws Exception {
@ -442,6 +451,17 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
testAutoGeneratedCodeByProtobuf(userUpdateMsg);
}
private void validatePublicCustomer() throws Exception {
Optional<CustomerUpdateMsg> customerUpdateMsgOpt = edgeImitator.findMessageByType(CustomerUpdateMsg.class);
Assert.assertTrue(customerUpdateMsgOpt.isPresent());
CustomerUpdateMsg customerUpdateMsg = customerUpdateMsgOpt.get();
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, customerUpdateMsg.getMsgType());
UUID customerUUID = new UUID(customerUpdateMsg.getIdMSB(), customerUpdateMsg.getIdLSB());
Customer customer = doGet("/api/customer/" + customerUUID, Customer.class);
Assert.assertNotNull(customer);
Assert.assertTrue(customer.isPublic());
}
protected Device saveDeviceOnCloudAndVerifyDeliveryToEdge() throws Exception {
// create device and assign to edge
Device savedDevice = saveDevice(StringUtils.randomAlphanumeric(15), thermostatDeviceProfile.getName());
@ -500,13 +520,13 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
return doPost("/api/asset", asset, Asset.class);
}
protected OtaPackageInfo saveOtaPackageInfo(DeviceProfileId deviceProfileId) {
protected OtaPackageInfo saveOtaPackageInfo(DeviceProfileId deviceProfileId, OtaPackageType type) {
SaveOtaPackageInfoRequest firmwareInfo = new SaveOtaPackageInfoRequest();
firmwareInfo.setDeviceProfileId(deviceProfileId);
firmwareInfo.setType(FIRMWARE);
firmwareInfo.setTitle("Firmware Edge " + StringUtils.randomAlphanumeric(3));
firmwareInfo.setType(type);
firmwareInfo.setTitle(type.name() + " Edge " + StringUtils.randomAlphanumeric(3));
firmwareInfo.setVersion("v1.0");
firmwareInfo.setTag("My firmware #1 v1.0");
firmwareInfo.setTag("My " + type.name() + " #1 v1.0");
firmwareInfo.setUsesUrl(true);
firmwareInfo.setUrl("http://localhost:8080/v1/package");
firmwareInfo.setAdditionalInfo(JacksonUtil.newObjectNode());
@ -541,6 +561,49 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
Assert.assertEquals(source.hashCode(), target.hashCode());
}
protected RuleChainId createEdgeRuleChainAndAssignToEdge(String ruleChainName) throws Exception {
edgeImitator.expectMessageAmount(1);
RuleChain ruleChain = new RuleChain();
ruleChain.setName(ruleChainName);
ruleChain.setType(RuleChainType.EDGE);
RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class);
doPost("/api/edge/" + edge.getUuidId()
+ "/ruleChain/" + savedRuleChain.getUuidId(), RuleChain.class);
Assert.assertTrue(edgeImitator.waitForMessages());
return savedRuleChain.getId();
}
protected void unAssignFromEdgeAndDeleteRuleChain(RuleChainId ruleChainId) throws Exception {
edgeImitator.expectMessageAmount(1);
doDelete("/api/edge/" + edge.getUuidId()
+ "/ruleChain/" + ruleChainId.getId(), RuleChain.class);
Assert.assertTrue(edgeImitator.waitForMessages());
// delete rule chain
doDelete("/api/ruleChain/" + ruleChainId.getId())
.andExpect(status().isOk());
}
protected DashboardId createDashboardAndAssignToEdge(String dashboardName) throws Exception {
edgeImitator.expectMessageAmount(1);
Dashboard dashboard = new Dashboard();
dashboard.setTitle(dashboardName);
Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
doPost("/api/edge/" + edge.getUuidId()
+ "/dashboard/" + savedDashboard.getUuidId(), Dashboard.class);
Assert.assertTrue(edgeImitator.waitForMessages());
return savedDashboard.getId();
}
protected void unAssignFromEdgeAndDeleteDashboard(DashboardId dashboardId) throws Exception {
edgeImitator.expectMessageAmount(1);
doDelete("/api/edge/" + edge.getUuidId()
+ "/dashboard/" + dashboardId.getId(), RuleChain.class);
Assert.assertTrue(edgeImitator.waitForMessages());
// delete dashboard
doDelete("/api/dashboard/" + dashboardId.getId())
.andExpect(status().isOk());
}
}

8
application/src/test/java/org/thingsboard/server/edge/BaseAssetProfileEdgeTest.java

@ -20,6 +20,7 @@ import com.google.protobuf.ByteString;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
@ -31,8 +32,11 @@ abstract public class BaseAssetProfileEdgeTest extends AbstractEdgeTest {
@Test
public void testAssetProfiles() throws Exception {
RuleChainId buildingsRuleChainId = createEdgeRuleChainAndAssignToEdge("Buildings Rule Chain");
// create asset profile
AssetProfile assetProfile = this.createAssetProfile("Building");
assetProfile.setDefaultEdgeRuleChainId(buildingsRuleChainId);
edgeImitator.expectMessageAmount(1);
assetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class);
Assert.assertTrue(edgeImitator.waitForMessages());
@ -43,6 +47,8 @@ abstract public class BaseAssetProfileEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(assetProfile.getUuidId().getMostSignificantBits(), assetProfileUpdateMsg.getIdMSB());
Assert.assertEquals(assetProfile.getUuidId().getLeastSignificantBits(), assetProfileUpdateMsg.getIdLSB());
Assert.assertEquals("Building", assetProfileUpdateMsg.getName());
Assert.assertEquals(buildingsRuleChainId.getId().getMostSignificantBits(), assetProfileUpdateMsg.getDefaultRuleChainIdMSB());
Assert.assertEquals(buildingsRuleChainId.getId().getLeastSignificantBits(), assetProfileUpdateMsg.getDefaultRuleChainIdLSB());
// update asset profile
assetProfile.setImage("IMAGE");
@ -66,5 +72,7 @@ abstract public class BaseAssetProfileEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, assetProfileUpdateMsg.getMsgType());
Assert.assertEquals(assetProfile.getUuidId().getMostSignificantBits(), assetProfileUpdateMsg.getIdMSB());
Assert.assertEquals(assetProfile.getUuidId().getLeastSignificantBits(), assetProfileUpdateMsg.getIdLSB());
unAssignFromEdgeAndDeleteRuleChain(buildingsRuleChainId);
}
}

34
application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java

@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.DeviceCredentials;
@ -174,7 +175,7 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
// create device and assign to edge; update device
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
verifyUpdateFirmwareIdAndDeviceData(savedDevice);
verifyUpdateFirmwareIdSoftwareIdAndDeviceData(savedDevice);
// update device credentials - ACCESS_TOKEN
edgeImitator.expectMessageAmount(1);
@ -210,15 +211,20 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(deviceCredentials.getCredentialsValue(), deviceCredentialsUpdateMsg.getCredentialsValue());
}
private void verifyUpdateFirmwareIdAndDeviceData(Device savedDevice) throws InterruptedException {
// create ota package
private void verifyUpdateFirmwareIdSoftwareIdAndDeviceData(Device savedDevice) throws InterruptedException {
// create ota packages
edgeImitator.expectMessageAmount(1);
OtaPackageInfo firmwareOtaPackageInfo = saveOtaPackageInfo(thermostatDeviceProfile.getId());
OtaPackageInfo firmwareOtaPackageInfo = saveOtaPackageInfo(thermostatDeviceProfile.getId(), OtaPackageType.FIRMWARE);
Assert.assertTrue(edgeImitator.waitForMessages());
edgeImitator.expectMessageAmount(1);
OtaPackageInfo softwareOtaPackageInfo = saveOtaPackageInfo(thermostatDeviceProfile.getId(), OtaPackageType.SOFTWARE);
Assert.assertTrue(edgeImitator.waitForMessages());
// update device
edgeImitator.expectMessageAmount(1);
savedDevice.setFirmwareId(firmwareOtaPackageInfo.getId());
savedDevice.setSoftwareId(softwareOtaPackageInfo.getId());
DeviceData deviceData = new DeviceData();
deviceData.setConfiguration(new DefaultDeviceConfiguration());
@ -239,6 +245,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(savedDevice.getType(), deviceUpdateMsg.getType());
Assert.assertEquals(firmwareOtaPackageInfo.getUuidId().getMostSignificantBits(), deviceUpdateMsg.getFirmwareIdMSB());
Assert.assertEquals(firmwareOtaPackageInfo.getUuidId().getLeastSignificantBits(), deviceUpdateMsg.getFirmwareIdLSB());
Assert.assertEquals(softwareOtaPackageInfo.getUuidId().getMostSignificantBits(), deviceUpdateMsg.getSoftwareIdMSB());
Assert.assertEquals(softwareOtaPackageInfo.getUuidId().getLeastSignificantBits(), deviceUpdateMsg.getSoftwareIdLSB());
Optional<DeviceData> deviceDataOpt =
dataDecodingEncodingService.decode(deviceUpdateMsg.getDeviceDataBytes().toByteArray());
Assert.assertTrue(deviceDataOpt.isPresent());
@ -693,7 +701,7 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
public void testVerifyDeliveryOfLatestTimeseriesOnAttributesRequest() throws Exception {
Device device = findDeviceByName("Edge Device 1");
JsonNode timeseriesData = mapper.readTree("{\"temperature\":25}");
JsonNode timeseriesData = mapper.readTree("{\"temperature\":25, \"isEnabled\": true}");
doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE,
timeseriesData);
@ -732,9 +740,17 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
TransportProtos.PostTelemetryMsg timeseriesUpdatedMsg = latestEntityDataMsg.getPostTelemetryMsg();
Assert.assertEquals(1, timeseriesUpdatedMsg.getTsKvListList().size());
TransportProtos.TsKvListProto tsKvListProto = timeseriesUpdatedMsg.getTsKvListList().get(0);
Assert.assertEquals(1, tsKvListProto.getKvList().size());
TransportProtos.KeyValueProto keyValueProto = tsKvListProto.getKvList().get(0);
Assert.assertEquals(25, keyValueProto.getLongV());
Assert.assertEquals("temperature", keyValueProto.getKey());
Assert.assertEquals(2, tsKvListProto.getKvList().size());
for (TransportProtos.KeyValueProto keyValueProto : tsKvListProto.getKvList()) {
if ("temperature".equals(keyValueProto.getKey())) {
Assert.assertEquals(TransportProtos.KeyValueType.LONG_V, keyValueProto.getType());
Assert.assertEquals(25, keyValueProto.getLongV());
} else if ("isEnabled".equals(keyValueProto.getKey())) {
Assert.assertEquals(TransportProtos.KeyValueType.BOOLEAN_V, keyValueProto.getType());
Assert.assertTrue(keyValueProto.getBoolV());
} else {
Assert.fail("Unexpected key: " + keyValueProto.getKey());
}
}
}
}

25
application/src/test/java/org/thingsboard/server/edge/BaseDeviceProfileEdgeTest.java

@ -36,7 +36,10 @@ import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingC
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.AbstractLwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.NoSecLwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.transport.snmp.SnmpMapping;
import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig;
import org.thingsboard.server.common.data.transport.snmp.config.impl.TelemetryQueryingSnmpCommunicationConfig;
@ -55,8 +58,11 @@ abstract public class BaseDeviceProfileEdgeTest extends AbstractEdgeTest {
@Test
public void testDeviceProfiles() throws Exception {
RuleChainId thermostatsRuleChainId = createEdgeRuleChainAndAssignToEdge("Thermostats Rule Chain");
// create device profile
DeviceProfile deviceProfile = this.createDeviceProfile("ONE_MORE_DEVICE_PROFILE", null);
deviceProfile.setDefaultEdgeRuleChainId(thermostatsRuleChainId);
extendDeviceProfileData(deviceProfile);
edgeImitator.expectMessageAmount(1);
deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
@ -67,13 +73,23 @@ abstract public class BaseDeviceProfileEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType());
Assert.assertEquals(deviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB());
Assert.assertEquals(deviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB());
Assert.assertEquals(thermostatsRuleChainId.getId().getMostSignificantBits(), deviceProfileUpdateMsg.getDefaultRuleChainIdMSB());
Assert.assertEquals(thermostatsRuleChainId.getId().getLeastSignificantBits(), deviceProfileUpdateMsg.getDefaultRuleChainIdLSB());
// update device profile
OtaPackageInfo firmwareOtaPackageInfo = saveOtaPackageInfo(deviceProfile.getId());
edgeImitator.expectMessageAmount(1);
OtaPackageInfo firmwareOtaPackageInfo = saveOtaPackageInfo(deviceProfile.getId(), OtaPackageType.FIRMWARE);
Assert.assertTrue(edgeImitator.waitForMessages());
edgeImitator.expectMessageAmount(1);
OtaPackageInfo softwareOtaPackageInfo = saveOtaPackageInfo(deviceProfile.getId(), OtaPackageType.SOFTWARE);
Assert.assertTrue(edgeImitator.waitForMessages());
DashboardId thermostatsDashboardId = createDashboardAndAssignToEdge("Thermostats Dashboard");
deviceProfile.setFirmwareId(firmwareOtaPackageInfo.getId());
deviceProfile.setSoftwareId(softwareOtaPackageInfo.getId());
deviceProfile.setDefaultDashboardId(thermostatsDashboardId);
edgeImitator.expectMessageAmount(1);
deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
Assert.assertTrue(edgeImitator.waitForMessages());
@ -82,6 +98,10 @@ abstract public class BaseDeviceProfileEdgeTest extends AbstractEdgeTest {
deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage;
Assert.assertEquals(firmwareOtaPackageInfo.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getFirmwareIdMSB());
Assert.assertEquals(firmwareOtaPackageInfo.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getFirmwareIdLSB());
Assert.assertEquals(softwareOtaPackageInfo.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getSoftwareIdMSB());
Assert.assertEquals(softwareOtaPackageInfo.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getSoftwareIdLSB());
Assert.assertEquals(thermostatsDashboardId.getId().getMostSignificantBits(), deviceProfileUpdateMsg.getDefaultDashboardIdMSB());
Assert.assertEquals(thermostatsDashboardId.getId().getLeastSignificantBits(), deviceProfileUpdateMsg.getDefaultDashboardIdLSB());
// delete profile
edgeImitator.expectMessageAmount(1);
@ -94,6 +114,9 @@ abstract public class BaseDeviceProfileEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType());
Assert.assertEquals(deviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB());
Assert.assertEquals(deviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB());
unAssignFromEdgeAndDeleteRuleChain(thermostatsRuleChainId);
unAssignFromEdgeAndDeleteDashboard(thermostatsDashboardId);
}
@Test

8
common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java

@ -16,7 +16,6 @@
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 io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
@ -28,7 +27,6 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.QueueId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length;
@ -93,6 +91,11 @@ public class DeviceProfile extends SearchTextBased<DeviceProfileId> implements H
@ApiModelProperty(position = 10, value = "Reference to the software OTA package. If present, the specified package will be used as default device software. ")
private OtaPackageId softwareId;
@ApiModelProperty(position = 17, value = "Reference to the edge rule chain. " +
"If present, the specified edge rule chain will be used on the edge to process all messages related to device, including telemetry, attribute updates, etc. " +
"Otherwise, the edge root rule chain will be used to process those messages.")
private RuleChainId defaultEdgeRuleChainId;
private DeviceProfileId externalId;
public DeviceProfile() {
@ -117,6 +120,7 @@ public class DeviceProfile extends SearchTextBased<DeviceProfileId> implements H
this.provisionDeviceKey = deviceProfile.getProvisionDeviceKey();
this.firmwareId = deviceProfile.getFirmwareId();
this.softwareId = deviceProfile.getSoftwareId();
this.defaultEdgeRuleChainId = deviceProfile.getDefaultEdgeRuleChainId();
this.externalId = deviceProfile.getExternalId();
}

6
common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java

@ -68,6 +68,11 @@ public class AssetProfile extends SearchTextBased<AssetProfileId> implements Has
"Otherwise, the 'Main' queue will be used to store those messages.")
private String defaultQueueName;
@ApiModelProperty(position = 13, value = "Reference to the edge rule chain. " +
"If present, the specified edge rule chain will be used on the edge to process all messages related to asset, including asset updates, telemetry, attribute updates, etc. " +
"Otherwise, the edge root rule chain will be used to process those messages.")
private RuleChainId defaultEdgeRuleChainId;
private AssetProfileId externalId;
public AssetProfile() {
@ -88,6 +93,7 @@ public class AssetProfile extends SearchTextBased<AssetProfileId> implements Has
this.defaultRuleChainId = assetProfile.getDefaultRuleChainId();
this.defaultDashboardId = assetProfile.getDefaultDashboardId();
this.defaultQueueName = assetProfile.getDefaultQueueName();
this.defaultEdgeRuleChainId = assetProfile.getDefaultEdgeRuleChainId();
this.externalId = assetProfile.getExternalId();
}

32
common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInstallInstructions.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2022 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.common.data.edge;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@ApiModel
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EdgeInstallInstructions {
@ApiModelProperty(position = 1, value = "Markdown with docker install instructions")
private String dockerInstallInstructions;
}

6
common/edge-api/src/main/proto/edge.proto

@ -203,6 +203,8 @@ message DeviceUpdateMsg {
optional int64 firmwareIdMSB = 13;
optional int64 firmwareIdLSB = 14;
optional bytes deviceDataBytes = 15;
optional int64 softwareIdMSB = 16;
optional int64 softwareIdLSB = 17;
}
message DeviceProfileUpdateMsg {
@ -223,6 +225,10 @@ message DeviceProfileUpdateMsg {
optional bytes image = 15;
optional int64 firmwareIdMSB = 16;
optional int64 firmwareIdLSB = 17;
optional int64 softwareIdMSB = 18;
optional int64 softwareIdLSB = 19;
optional int64 defaultDashboardIdMSB = 20;
optional int64 defaultDashboardIdLSB = 21;
}
message AssetProfileUpdateMsg {

2
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -186,6 +186,7 @@ public class ModelConstants {
public static final String DEVICE_PROFILE_PROVISION_DEVICE_KEY = "provision_device_key";
public static final String DEVICE_PROFILE_FIRMWARE_ID_PROPERTY = "firmware_id";
public static final String DEVICE_PROFILE_SOFTWARE_ID_PROPERTY = "software_id";
public static final String DEVICE_PROFILE_DEFAULT_EDGE_RULE_CHAIN_ID_PROPERTY = "default_edge_rule_chain_id";
/**
* Asset profile constants.
@ -199,6 +200,7 @@ public class ModelConstants {
public static final String ASSET_PROFILE_DEFAULT_RULE_CHAIN_ID_PROPERTY = "default_rule_chain_id";
public static final String ASSET_PROFILE_DEFAULT_DASHBOARD_ID_PROPERTY = "default_dashboard_id";
public static final String ASSET_PROFILE_DEFAULT_QUEUE_NAME_PROPERTY = "default_queue_name";
public static final String ASSET_PROFILE_DEFAULT_EDGE_RULE_CHAIN_ID_PROPERTY = "default_edge_rule_chain_id";
/**
* Cassandra entityView constants.

9
dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetProfileEntity.java

@ -64,6 +64,9 @@ public final class AssetProfileEntity extends BaseSqlEntity<AssetProfile> implem
@Column(name = ModelConstants.ASSET_PROFILE_DEFAULT_QUEUE_NAME_PROPERTY)
private String defaultQueueName;
@Column(name = ModelConstants.ASSET_PROFILE_DEFAULT_EDGE_RULE_CHAIN_ID_PROPERTY, columnDefinition = "uuid")
private UUID defaultEdgeRuleChainId;
@Column(name = ModelConstants.EXTERNAL_ID_PROPERTY)
private UUID externalId;
@ -90,6 +93,9 @@ public final class AssetProfileEntity extends BaseSqlEntity<AssetProfile> implem
this.defaultDashboardId = assetProfile.getDefaultDashboardId().getId();
}
this.defaultQueueName = assetProfile.getDefaultQueueName();
if (assetProfile.getDefaultEdgeRuleChainId() != null) {
this.defaultEdgeRuleChainId = assetProfile.getDefaultEdgeRuleChainId().getId();
}
if (assetProfile.getExternalId() != null) {
this.externalId = assetProfile.getExternalId().getId();
}
@ -127,6 +133,9 @@ public final class AssetProfileEntity extends BaseSqlEntity<AssetProfile> implem
if (defaultDashboardId != null) {
assetProfile.setDefaultDashboardId(new DashboardId(defaultDashboardId));
}
if (defaultEdgeRuleChainId != null) {
assetProfile.setDefaultEdgeRuleChainId(new RuleChainId(defaultEdgeRuleChainId));
}
if (externalId != null) {
assetProfile.setExternalId(new AssetProfileId(externalId));
}

9
dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java

@ -103,6 +103,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity<DeviceProfile> impl
@Column(name = ModelConstants.DEVICE_PROFILE_SOFTWARE_ID_PROPERTY)
private UUID softwareId;
@Column(name = ModelConstants.DEVICE_PROFILE_DEFAULT_EDGE_RULE_CHAIN_ID_PROPERTY, columnDefinition = "uuid")
private UUID defaultEdgeRuleChainId;
@Column(name = ModelConstants.EXTERNAL_ID_PROPERTY)
private UUID externalId;
@ -140,6 +143,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity<DeviceProfile> impl
if (deviceProfile.getSoftwareId() != null) {
this.softwareId = deviceProfile.getSoftwareId().getId();
}
if (deviceProfile.getDefaultEdgeRuleChainId() != null) {
this.defaultEdgeRuleChainId = deviceProfile.getDefaultEdgeRuleChainId().getId();
}
if (deviceProfile.getExternalId() != null) {
this.externalId = deviceProfile.getExternalId().getId();
}
@ -189,6 +195,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity<DeviceProfile> impl
if (softwareId != null) {
deviceProfile.setSoftwareId(new OtaPackageId(softwareId));
}
if (defaultEdgeRuleChainId != null) {
deviceProfile.setDefaultEdgeRuleChainId(new RuleChainId(defaultEdgeRuleChainId));
}
if (externalId != null) {
deviceProfile.setExternalId(new DeviceProfileId(externalId));
}

8
dao/src/main/resources/sql/schema-entities.sql

@ -249,11 +249,13 @@ CREATE TABLE IF NOT EXISTS asset_profile (
default_rule_chain_id uuid,
default_dashboard_id uuid,
default_queue_name varchar(255),
default_edge_rule_chain_id uuid,
external_id uuid,
CONSTRAINT asset_profile_name_unq_key UNIQUE (tenant_id, name),
CONSTRAINT asset_profile_external_id_unq_key UNIQUE (tenant_id, external_id),
CONSTRAINT fk_default_rule_chain_asset_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id),
CONSTRAINT fk_default_dashboard_asset_profile FOREIGN KEY (default_dashboard_id) REFERENCES dashboard(id)
CONSTRAINT fk_default_dashboard_asset_profile FOREIGN KEY (default_dashboard_id) REFERENCES dashboard(id),
CONSTRAINT fk_default_edge_rule_chain_asset_profile FOREIGN KEY (default_edge_rule_chain_id) REFERENCES rule_chain(id)
);
CREATE TABLE IF NOT EXISTS asset (
@ -292,6 +294,7 @@ CREATE TABLE IF NOT EXISTS device_profile (
default_dashboard_id uuid,
default_queue_name varchar(255),
provision_device_key varchar,
default_edge_rule_chain_id uuid,
external_id uuid,
CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name),
CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key),
@ -299,7 +302,8 @@ CREATE TABLE IF NOT EXISTS device_profile (
CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id),
CONSTRAINT fk_default_dashboard_device_profile FOREIGN KEY (default_dashboard_id) REFERENCES dashboard(id),
CONSTRAINT fk_firmware_device_profile FOREIGN KEY (firmware_id) REFERENCES ota_package(id),
CONSTRAINT fk_software_device_profile FOREIGN KEY (software_id) REFERENCES ota_package(id)
CONSTRAINT fk_software_device_profile FOREIGN KEY (software_id) REFERENCES ota_package(id),
CONSTRAINT fk_default_edge_rule_chain_device_profile FOREIGN KEY (default_edge_rule_chain_id) REFERENCES rule_chain(id)
);
DO

8
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -81,6 +81,7 @@ import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeInfo;
import org.thingsboard.server.common.data.edge.EdgeInstallInstructions;
import org.thingsboard.server.common.data.edge.EdgeSearchQuery;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
import org.thingsboard.server.common.data.id.AlarmId;
@ -3138,6 +3139,12 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}).getBody();
}
public Optional<EdgeInstallInstructions> getEdgeDockerInstallInstructions(EdgeId edgeId) {
ResponseEntity<EdgeInstallInstructions> edgeInstallInstructionsResult =
restTemplate.getForEntity(baseURL + "/api/edge/instructions/{edgeId}", EdgeInstallInstructions.class, edgeId.getId());
return Optional.ofNullable(edgeInstallInstructionsResult.getBody());
}
public UUID saveEntitiesVersion(VersionCreateRequest request) {
return restTemplate.postForEntity(baseURL + "/api/entities/vc/version", request, UUID.class).getBody();
}
@ -3154,6 +3161,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}
}
}
public PageData<EntityVersion> listEntityVersions(EntityId externalEntityId, String branch, PageLink pageLink) {
Map<String, String> params = new HashMap<>();
params.put("entityType", externalEntityId.getEntityType().name());

3
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java

@ -70,6 +70,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@ -202,6 +203,8 @@ public interface TbContext {
*
*/
void schedule(Runnable runnable, long delay, TimeUnit timeUnit);
void checkTenantEntity(EntityId entityId);
boolean isLocalEntity(EntityId entityId);

22
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/DeduplicationId.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2022 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.deduplication;
public enum DeduplicationId {
ORIGINATOR, TENANT, CUSTOMER
}

22
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/DeduplicationStrategy.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2022 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.deduplication;
public enum DeduplicationStrategy {
FIRST, LAST, ALL
}

237
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java

@ -0,0 +1,237 @@
/**
* Copyright © 2016-2022 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.deduplication;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.util.Pair;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleNode;
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.rule.engine.api.TbRelationTypes;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@RuleNode(
type = ComponentType.ACTION,
name = "deduplication",
configClazz = TbMsgDeduplicationNodeConfiguration.class,
nodeDescription = "Deduplicate messages for a configurable period based on a specified deduplication strategy.",
nodeDetails = "Rule node allows you to select one of the following strategy to deduplicate messages: <br></br>" +
"<b>FIRST</b> - return first message that arrived during deduplication period.<br></br>" +
"<b>LAST</b> - return last message that arrived during deduplication period.<br></br>" +
"<b>ALL</b> - return all messages as a single JSON array message. Where each element represents object with <b>msg</b> and <b>metadata</b> inner properties.<br></br>",
icon = "content_copy",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeMsgDeduplicationConfig"
)
@Slf4j
public class TbMsgDeduplicationNode implements TbNode {
private static final String TB_MSG_DEDUPLICATION_TIMEOUT_MSG = "TbMsgDeduplicationNodeMsg";
private static final int TB_MSG_DEDUPLICATION_TIMEOUT = 5000;
public static final int TB_MSG_DEDUPLICATION_RETRY_DELAY = 10;
private TbMsgDeduplicationNodeConfiguration config;
private final Map<EntityId, List<TbMsg>> deduplicationMap;
private long deduplicationInterval;
private long lastScheduledTs;
private DeduplicationId deduplicationId;
public TbMsgDeduplicationNode() {
this.deduplicationMap = new HashMap<>();
}
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbMsgDeduplicationNodeConfiguration.class);
this.deduplicationInterval = TimeUnit.SECONDS.toMillis(config.getInterval());
this.deduplicationId = config.getId();
scheduleTickMsg(ctx);
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
if (TB_MSG_DEDUPLICATION_TIMEOUT_MSG.equals(msg.getType())) {
try {
processDeduplication(ctx);
} finally {
scheduleTickMsg(ctx);
}
} else {
processOnRegularMsg(ctx, msg);
}
}
@Override
public void destroy() {
deduplicationMap.clear();
}
private void processOnRegularMsg(TbContext ctx, TbMsg msg) {
EntityId id = getDeduplicationId(ctx, msg);
List<TbMsg> deduplicationMsgs = deduplicationMap.computeIfAbsent(id, k -> new LinkedList<>());
if (deduplicationMsgs.size() < config.getMaxPendingMsgs()) {
log.trace("[{}][{}] Adding msg: [{}][{}] to the pending msgs map ...", ctx.getSelfId(), id, msg.getId(), msg.getMetaDataTs());
deduplicationMsgs.add(msg);
ctx.ack(msg);
} else {
log.trace("[{}] Max limit of pending messages reached for deduplication id: [{}]", ctx.getSelfId(), id);
ctx.tellFailure(msg, new RuntimeException("[" + ctx.getSelfId() + "] Max limit of pending messages reached for deduplication id: [" + id + "]"));
}
}
private EntityId getDeduplicationId(TbContext ctx, TbMsg msg) {
switch (deduplicationId) {
case ORIGINATOR:
return msg.getOriginator();
case TENANT:
return ctx.getTenantId();
case CUSTOMER:
return msg.getCustomerId();
default:
throw new IllegalStateException("Unsupported deduplication id: " + deduplicationId);
}
}
private void processDeduplication(TbContext ctx) {
if (deduplicationMap.isEmpty()) {
return;
}
List<TbMsg> deduplicationResults = new ArrayList<>();
long deduplicationTimeoutMs = System.currentTimeMillis();
deduplicationMap.forEach((entityId, tbMsgs) -> {
if (tbMsgs.isEmpty()) {
return;
}
Optional<TbPair<Long, Long>> packBoundsOpt = findValidPack(tbMsgs, deduplicationTimeoutMs);
while (packBoundsOpt.isPresent()) {
TbPair<Long, Long> packBounds = packBoundsOpt.get();
if (DeduplicationStrategy.ALL.equals(config.getStrategy())) {
List<TbMsg> pack = new ArrayList<>();
for (Iterator<TbMsg> iterator = tbMsgs.iterator(); iterator.hasNext(); ) {
TbMsg msg = iterator.next();
long msgTs = msg.getMetaDataTs();
if (msgTs >= packBounds.getFirst() && msgTs < packBounds.getSecond()) {
pack.add(msg);
iterator.remove();
}
}
deduplicationResults.add(TbMsg.newMsg(
config.getQueueName(),
config.getOutMsgType(),
entityId,
getMetadata(),
getMergedData(pack)));
} else {
TbMsg resultMsg = null;
boolean searchMin = DeduplicationStrategy.FIRST.equals(config.getStrategy());
for (Iterator<TbMsg> iterator = tbMsgs.iterator(); iterator.hasNext(); ) {
TbMsg msg = iterator.next();
long msgTs = msg.getMetaDataTs();
if (msgTs >= packBounds.getFirst() && msgTs < packBounds.getSecond()) {
iterator.remove();
if (resultMsg == null
|| (searchMin && msg.getMetaDataTs() < resultMsg.getMetaDataTs())
|| (!searchMin && msg.getMetaDataTs() > resultMsg.getMetaDataTs())) {
resultMsg = msg;
}
}
}
deduplicationResults.add(resultMsg);
}
packBoundsOpt = findValidPack(tbMsgs, deduplicationTimeoutMs);
}
});
deduplicationResults.forEach(outMsg -> enqueueForTellNextWithRetry(ctx, outMsg, 0));
}
private Optional<TbPair<Long, Long>> findValidPack(List<TbMsg> msgs, long deduplicationTimeoutMs) {
Optional<TbMsg> min = msgs.stream().min(Comparator.comparing(TbMsg::getMetaDataTs));
return min.map(minTsMsg -> {
long packStartTs = minTsMsg.getMetaDataTs();
long packEndTs = packStartTs + deduplicationInterval;
if (packEndTs <= deduplicationTimeoutMs) {
return new TbPair<>(packStartTs, packEndTs);
}
return null;
});
}
private void enqueueForTellNextWithRetry(TbContext ctx, TbMsg msg, int retryAttempt) {
if (config.getMaxRetries() > retryAttempt) {
ctx.enqueueForTellNext(msg, TbRelationTypes.SUCCESS,
() -> {
log.trace("[{}][{}][{}] Successfully enqueue deduplication result message!", ctx.getSelfId(), msg.getOriginator(), retryAttempt);
},
throwable -> {
log.trace("[{}][{}][{}] Failed to enqueue deduplication output message due to: ", ctx.getSelfId(), msg.getOriginator(), retryAttempt, throwable);
ctx.schedule(() -> {
enqueueForTellNextWithRetry(ctx, msg, retryAttempt + 1);
}, TB_MSG_DEDUPLICATION_RETRY_DELAY, TimeUnit.SECONDS);
});
}
}
private void scheduleTickMsg(TbContext ctx) {
long curTs = System.currentTimeMillis();
if (lastScheduledTs == 0L) {
lastScheduledTs = curTs;
}
lastScheduledTs += TB_MSG_DEDUPLICATION_TIMEOUT;
long curDelay = Math.max(0L, (lastScheduledTs - curTs));
TbMsg tickMsg = ctx.newMsg(null, TB_MSG_DEDUPLICATION_TIMEOUT_MSG, ctx.getSelfId(), new TbMsgMetaData(), "");
ctx.tellSelf(tickMsg, curDelay);
}
private String getMergedData(List<TbMsg> msgs) {
ArrayNode mergedData = JacksonUtil.OBJECT_MAPPER.createArrayNode();
msgs.forEach(msg -> {
ObjectNode msgNode = JacksonUtil.newObjectNode();
msgNode.set("msg", JacksonUtil.toJsonNode(msg.getData()));
msgNode.set("metadata", JacksonUtil.valueToTree(msg.getMetaData().getData()));
mergedData.add(msgNode);
});
return JacksonUtil.toString(mergedData);
}
private TbMsgMetaData getMetadata() {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("ts", String.valueOf(System.currentTimeMillis()));
return metaData;
}
}

46
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNodeConfiguration.java

@ -0,0 +1,46 @@
/**
* Copyright © 2016-2022 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.deduplication;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
@Data
public class TbMsgDeduplicationNodeConfiguration implements NodeConfiguration<TbMsgDeduplicationNodeConfiguration> {
private int interval;
private DeduplicationId id;
private DeduplicationStrategy strategy;
// Advanced settings:
private int maxPendingMsgs;
private int maxRetries;
// only for DeduplicationStrategy.ALL:
private String outMsgType;
private String queueName;
@Override
public TbMsgDeduplicationNodeConfiguration defaultConfiguration() {
TbMsgDeduplicationNodeConfiguration configuration = new TbMsgDeduplicationNodeConfiguration();
configuration.setInterval(60);
configuration.setId(DeduplicationId.ORIGINATOR);
configuration.setStrategy(DeduplicationStrategy.FIRST);
configuration.setMaxPendingMsgs(100);
configuration.setMaxRetries(3);
return configuration;
}
}

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java

@ -48,7 +48,6 @@ import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeMsgDelayConfig"
)
public class TbMsgDelayNode implements TbNode {
private static final String TB_MSG_DELAY_NODE_MSG = "TbMsgDelayNodeMsg";

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

402
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbMsgDeduplicationNodeTest.java

@ -0,0 +1,402 @@
/**
* Copyright © 2016-2022 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.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
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.rule.engine.api.TbRelationTypes;
import org.thingsboard.rule.engine.deduplication.DeduplicationStrategy;
import org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNode;
import org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNodeConfiguration;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.session.SessionMsgType;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@Slf4j
public class TbMsgDeduplicationNodeTest {
private static final String MAIN_QUEUE_NAME = "Main";
private static final String HIGH_PRIORITY_QUEUE_NAME = "HighPriority";
private static final String TB_MSG_DEDUPLICATION_TIMEOUT_MSG = "TbMsgDeduplicationNodeMsg";
private TbContext ctx;
private final ThingsBoardThreadFactory factory = ThingsBoardThreadFactory.forName("de-duplication-node-test");
private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(factory);
private final int deduplicationInterval = 1;
private TenantId tenantId;
private TbMsgDeduplicationNode node;
private TbMsgDeduplicationNodeConfiguration config;
private TbNodeConfiguration nodeConfiguration;
private CountDownLatch awaitTellSelfLatch;
@BeforeEach
public void init() throws TbNodeException {
ctx = mock(TbContext.class);
tenantId = TenantId.fromUUID(UUID.randomUUID());
RuleNodeId ruleNodeId = new RuleNodeId(UUID.randomUUID());
when(ctx.getSelfId()).thenReturn(ruleNodeId);
when(ctx.getTenantId()).thenReturn(tenantId);
doAnswer((Answer<TbMsg>) invocationOnMock -> {
String type = (String) (invocationOnMock.getArguments())[1];
EntityId originator = (EntityId) (invocationOnMock.getArguments())[2];
TbMsgMetaData metaData = (TbMsgMetaData) (invocationOnMock.getArguments())[3];
String data = (String) (invocationOnMock.getArguments())[4];
return TbMsg.newMsg(type, originator, metaData.copy(), data);
}).when(ctx).newMsg(isNull(), eq(TB_MSG_DEDUPLICATION_TIMEOUT_MSG), nullable(EntityId.class), any(TbMsgMetaData.class), any(String.class));
node = spy(new TbMsgDeduplicationNode());
config = new TbMsgDeduplicationNodeConfiguration().defaultConfiguration();
}
private void invokeTellSelf(int maxNumberOfInvocation) {
invokeTellSelf(maxNumberOfInvocation, false, 0);
}
private void invokeTellSelf(int maxNumberOfInvocation, boolean delayScheduleTimeout, int delayMultiplier) {
AtomicLong scheduleTimeout = new AtomicLong(deduplicationInterval);
AtomicInteger scheduleCount = new AtomicInteger(0);
doAnswer((Answer<Void>) invocationOnMock -> {
scheduleCount.getAndIncrement();
if (scheduleCount.get() <= maxNumberOfInvocation) {
TbMsg msg = (TbMsg) (invocationOnMock.getArguments())[0];
executorService.schedule(() -> {
try {
node.onMsg(ctx, msg);
awaitTellSelfLatch.countDown();
} catch (ExecutionException | InterruptedException | TbNodeException e) {
log.error("Failed to execute tellSelf method call due to: ", e);
}
}, scheduleTimeout.get(), TimeUnit.SECONDS);
if (delayScheduleTimeout) {
scheduleTimeout.set(scheduleTimeout.get() * delayMultiplier);
}
}
return null;
}).when(ctx).tellSelf(ArgumentMatchers.any(TbMsg.class), ArgumentMatchers.anyLong());
}
@AfterEach
public void destroy() {
executorService.shutdown();
node.destroy();
}
@Test
public void given_100_messages_strategy_first_then_verifyOutput() throws TbNodeException, ExecutionException, InterruptedException {
int wantedNumberOfTellSelfInvocation = 2;
int msgCount = 100;
awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation);
invokeTellSelf(wantedNumberOfTellSelfInvocation);
config.setInterval(deduplicationInterval);
config.setMaxPendingMsgs(msgCount);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctx, nodeConfiguration);
DeviceId deviceId = new DeviceId(UUID.randomUUID());
long currentTimeMillis = System.currentTimeMillis();
List<TbMsg> inputMsgs = getTbMsgs(deviceId, msgCount, currentTimeMillis, 500);
for (TbMsg msg : inputMsgs) {
node.onMsg(ctx, msg);
}
TbMsg msgToReject = createMsg(deviceId, inputMsgs.get(inputMsgs.size() - 1).getMetaDataTs() + 2);
node.onMsg(ctx, msgToReject);
awaitTellSelfLatch.await();
ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
ArgumentCaptor<Runnable> successCaptor = ArgumentCaptor.forClass(Runnable.class);
ArgumentCaptor<Consumer<Throwable>> failureCaptor = ArgumentCaptor.forClass(Consumer.class);
verify(ctx, times(msgCount)).ack(any());
verify(ctx, times(1)).tellFailure(eq(msgToReject), any());
verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any());
verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture());
Assertions.assertEquals(inputMsgs.get(0), newMsgCaptor.getValue());
}
@Test
public void given_100_messages_strategy_last_then_verifyOutput() throws TbNodeException, ExecutionException, InterruptedException {
int wantedNumberOfTellSelfInvocation = 2;
int msgCount = 100;
awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation);
invokeTellSelf(wantedNumberOfTellSelfInvocation);
config.setStrategy(DeduplicationStrategy.LAST);
config.setInterval(deduplicationInterval);
config.setMaxPendingMsgs(msgCount);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctx, nodeConfiguration);
DeviceId deviceId = new DeviceId(UUID.randomUUID());
long currentTimeMillis = System.currentTimeMillis();
List<TbMsg> inputMsgs = getTbMsgs(deviceId, msgCount, currentTimeMillis, 500);
TbMsg msgWithLatestTs = getMsgWithLatestTs(inputMsgs);
for (TbMsg msg : inputMsgs) {
node.onMsg(ctx, msg);
}
TbMsg msgToReject = createMsg(deviceId, inputMsgs.get(inputMsgs.size() - 1).getMetaDataTs() + 2);
node.onMsg(ctx, msgToReject);
awaitTellSelfLatch.await();
ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
ArgumentCaptor<Runnable> successCaptor = ArgumentCaptor.forClass(Runnable.class);
ArgumentCaptor<Consumer<Throwable>> failureCaptor = ArgumentCaptor.forClass(Consumer.class);
verify(ctx, times(msgCount)).ack(any());
verify(ctx, times(1)).tellFailure(eq(msgToReject), any());
verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any());
verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture());
Assertions.assertEquals(msgWithLatestTs, newMsgCaptor.getValue());
}
@Test
public void given_100_messages_strategy_all_then_verifyOutput() throws TbNodeException, ExecutionException, InterruptedException {
int wantedNumberOfTellSelfInvocation = 2;
int msgCount = 100;
awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation);
invokeTellSelf(wantedNumberOfTellSelfInvocation);
config.setInterval(deduplicationInterval);
config.setStrategy(DeduplicationStrategy.ALL);
config.setOutMsgType(SessionMsgType.POST_ATTRIBUTES_REQUEST.name());
config.setQueueName(HIGH_PRIORITY_QUEUE_NAME);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctx, nodeConfiguration);
DeviceId deviceId = new DeviceId(UUID.randomUUID());
long currentTimeMillis = System.currentTimeMillis();
List<TbMsg> inputMsgs = getTbMsgs(deviceId, msgCount, currentTimeMillis, 500);
for (TbMsg msg : inputMsgs) {
node.onMsg(ctx, msg);
}
awaitTellSelfLatch.await();
ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
ArgumentCaptor<Runnable> successCaptor = ArgumentCaptor.forClass(Runnable.class);
ArgumentCaptor<Consumer<Throwable>> failureCaptor = ArgumentCaptor.forClass(Consumer.class);
verify(ctx, times(msgCount)).ack(any());
verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any());
verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture());
Assertions.assertEquals(1, newMsgCaptor.getAllValues().size());
TbMsg outMessage = newMsgCaptor.getAllValues().get(0);
Assertions.assertEquals(getMergedData(inputMsgs), outMessage.getData());
Assertions.assertEquals(deviceId, outMessage.getOriginator());
Assertions.assertEquals(config.getOutMsgType(), outMessage.getType());
Assertions.assertEquals(config.getQueueName(), outMessage.getQueueName());
}
@Test
public void given_100_messages_strategy_all_then_verifyOutput_2_packs() throws TbNodeException, ExecutionException, InterruptedException {
int wantedNumberOfTellSelfInvocation = 2;
int msgCount = 100;
awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation);
invokeTellSelf(wantedNumberOfTellSelfInvocation, true, 3);
config.setInterval(deduplicationInterval);
config.setStrategy(DeduplicationStrategy.ALL);
config.setOutMsgType(SessionMsgType.POST_ATTRIBUTES_REQUEST.name());
config.setQueueName(HIGH_PRIORITY_QUEUE_NAME);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctx, nodeConfiguration);
DeviceId deviceId = new DeviceId(UUID.randomUUID());
long currentTimeMillis = System.currentTimeMillis();
List<TbMsg> firstMsgPack = getTbMsgs(deviceId, msgCount / 2, currentTimeMillis, 500);
for (TbMsg msg : firstMsgPack) {
node.onMsg(ctx, msg);
}
long firstPackDeduplicationPackEndTs = firstMsgPack.get(0).getMetaDataTs() + TimeUnit.SECONDS.toMillis(deduplicationInterval);
List<TbMsg> secondMsgPack = getTbMsgs(deviceId, msgCount / 2, firstPackDeduplicationPackEndTs, 500);
for (TbMsg msg : secondMsgPack) {
node.onMsg(ctx, msg);
}
awaitTellSelfLatch.await();
ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
ArgumentCaptor<Runnable> successCaptor = ArgumentCaptor.forClass(Runnable.class);
ArgumentCaptor<Consumer<Throwable>> failureCaptor = ArgumentCaptor.forClass(Consumer.class);
verify(ctx, times(msgCount)).ack(any());
verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any());
verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture());
List<TbMsg> resultMsgs = newMsgCaptor.getAllValues();
Assertions.assertEquals(2, resultMsgs.size());
TbMsg firstMsg = resultMsgs.get(0);
Assertions.assertEquals(getMergedData(firstMsgPack), firstMsg.getData());
Assertions.assertEquals(deviceId, firstMsg.getOriginator());
Assertions.assertEquals(config.getOutMsgType(), firstMsg.getType());
Assertions.assertEquals(config.getQueueName(), firstMsg.getQueueName());
TbMsg secondMsg = resultMsgs.get(1);
Assertions.assertEquals(getMergedData(secondMsgPack), secondMsg.getData());
Assertions.assertEquals(deviceId, secondMsg.getOriginator());
Assertions.assertEquals(config.getOutMsgType(), secondMsg.getType());
Assertions.assertEquals(config.getQueueName(), secondMsg.getQueueName());
}
@Test
public void given_100_messages_strategy_last_then_verifyOutput_2_packs() throws TbNodeException, ExecutionException, InterruptedException {
int wantedNumberOfTellSelfInvocation = 2;
int msgCount = 100;
awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation);
invokeTellSelf(wantedNumberOfTellSelfInvocation, true, 3);
config.setInterval(deduplicationInterval);
config.setStrategy(DeduplicationStrategy.LAST);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctx, nodeConfiguration);
DeviceId deviceId = new DeviceId(UUID.randomUUID());
long currentTimeMillis = System.currentTimeMillis();
List<TbMsg> firstMsgPack = getTbMsgs(deviceId, msgCount / 2, currentTimeMillis, 500);
for (TbMsg msg : firstMsgPack) {
node.onMsg(ctx, msg);
}
long firstPackDeduplicationPackEndTs = firstMsgPack.get(0).getMetaDataTs() + TimeUnit.SECONDS.toMillis(deduplicationInterval);
TbMsg msgWithLatestTsInFirstPack = getMsgWithLatestTs(firstMsgPack);
List<TbMsg> secondMsgPack = getTbMsgs(deviceId, msgCount / 2, firstPackDeduplicationPackEndTs, 500);
for (TbMsg msg : secondMsgPack) {
node.onMsg(ctx, msg);
}
TbMsg msgWithLatestTsInSecondPack = getMsgWithLatestTs(secondMsgPack);
awaitTellSelfLatch.await();
ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
ArgumentCaptor<Runnable> successCaptor = ArgumentCaptor.forClass(Runnable.class);
ArgumentCaptor<Consumer<Throwable>> failureCaptor = ArgumentCaptor.forClass(Consumer.class);
verify(ctx, times(msgCount)).ack(any());
verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any());
verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture());
List<TbMsg> resultMsgs = newMsgCaptor.getAllValues();
Assertions.assertEquals(2, resultMsgs.size());
Assertions.assertTrue(resultMsgs.contains(msgWithLatestTsInFirstPack));
Assertions.assertTrue(resultMsgs.contains(msgWithLatestTsInSecondPack));
}
private TbMsg getMsgWithLatestTs(List<TbMsg> firstMsgPack) {
int indexOfLastMsgInArray = firstMsgPack.size() - 1;
int indexToSetMaxTs = new Random().nextInt(indexOfLastMsgInArray) + 1;
TbMsg currentMaxTsMsg = firstMsgPack.get(indexOfLastMsgInArray);
TbMsg newLastMsgOfArray = firstMsgPack.get(indexToSetMaxTs);
firstMsgPack.set(indexOfLastMsgInArray, newLastMsgOfArray);
firstMsgPack.set(indexToSetMaxTs, currentMaxTsMsg);
return currentMaxTsMsg;
}
private List<TbMsg> getTbMsgs(DeviceId deviceId, int msgCount, long currentTimeMillis, int initTsStep) {
List<TbMsg> inputMsgs = new ArrayList<>();
var ts = currentTimeMillis + initTsStep;
for (int i = 0; i < msgCount; i++) {
inputMsgs.add(createMsg(deviceId, ts));
ts += 2;
}
return inputMsgs;
}
private TbMsg createMsg(DeviceId deviceId, long ts) {
ObjectNode dataNode = JacksonUtil.newObjectNode();
dataNode.put("deviceId", deviceId.getId().toString());
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("ts", String.valueOf(ts));
return TbMsg.newMsg(
MAIN_QUEUE_NAME,
SessionMsgType.POST_TELEMETRY_REQUEST.name(),
deviceId,
metaData,
JacksonUtil.toString(dataNode));
}
private String getMergedData(List<TbMsg> msgs) {
ArrayNode mergedData = JacksonUtil.OBJECT_MAPPER.createArrayNode();
msgs.forEach(msg -> {
ObjectNode msgNode = JacksonUtil.newObjectNode();
msgNode.set("msg", JacksonUtil.toJsonNode(msg.getData()));
msgNode.set("metadata", JacksonUtil.valueToTree(msg.getMetaData().getData()));
mergedData.add(msgNode);
});
return JacksonUtil.toString(mergedData);
}
}

6
ui-ngx/src/app/core/http/edge.service.ts

@ -21,7 +21,7 @@ import { HttpClient } from '@angular/common/http';
import { PageLink, TimePageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
import { EntitySubtype } from '@app/shared/models/entity-type.models';
import { Edge, EdgeEvent, EdgeInfo, EdgeSearchQuery } from '@shared/models/edge.models';
import { Edge, EdgeEvent, EdgeInfo, EdgeInstallInstructions, EdgeSearchQuery } from '@shared/models/edge.models';
import { EntityId } from '@shared/models/id/entity-id';
import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models';
@ -113,4 +113,8 @@ export class EdgeService {
public bulkImportEdges(entitiesData: BulkImportRequest, config?: RequestConfig): Observable<BulkImportResult> {
return this.http.post<BulkImportResult>('/api/edge/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config));
}
public getEdgeDockerInstallInstructions(edgeId: string, config?: RequestConfig): Observable<EdgeInstallInstructions> {
return this.http.get<EdgeInstallInstructions>(`/api/edge/instructions/${edgeId}`, defaultHttpOptionsFromConfig(config));
}
}

6
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html

@ -58,6 +58,12 @@
[queueType]="serviceType"
formControlName="defaultQueueName">
</tb-queue-autocomplete>
<tb-rule-chain-autocomplete
labelText="device-profile.default-edge-rule-chain"
formControlName="defaultEdgeRuleChainId"
[ruleChainType]="edgeRuleChainType">
<div tb-hint>{{'device-profile.default-edge-rule-chain-hint' | translate}}</div>
</tb-rule-chain-autocomplete>
<mat-form-field fxHide class="mat-block">
<mat-label translate>device-profile.type</mat-label>
<mat-select formControlName="type" required>

7
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts

@ -50,6 +50,7 @@ import { StepperSelectionEvent } from '@angular/cdk/stepper';
import { deepTrim } from '@core/utils';
import { ServiceType } from '@shared/models/queue.models';
import { DashboardId } from '@shared/models/id/dashboard-id';
import { RuleChainType } from '@shared/models/rule-chain.models';
export interface AddDeviceProfileDialogData {
deviceProfileName: string;
@ -93,6 +94,8 @@ export class AddDeviceProfileDialogComponent extends
serviceType = ServiceType.TB_RULE_ENGINE;
edgeRuleChainType = RuleChainType.EDGE;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddDeviceProfileDialogData,
@ -111,6 +114,7 @@ export class AddDeviceProfileDialogComponent extends
defaultRuleChainId: [null, []],
defaultDashboardId: [null, []],
defaultQueueName: [null, []],
defaultEdgeRuleChainId: [null, []],
description: ['', []]
}
);
@ -205,6 +209,9 @@ export class AddDeviceProfileDialogComponent extends
if (this.deviceProfileDetailsFormGroup.get('defaultDashboardId').value) {
deviceProfile.defaultDashboardId = new DashboardId(this.deviceProfileDetailsFormGroup.get('defaultDashboardId').value);
}
if (this.deviceProfileDetailsFormGroup.get('defaultEdgeRuleChainId').value) {
deviceProfile.defaultEdgeRuleChainId = new RuleChainId(this.deviceProfileDetailsFormGroup.get('defaultEdgeRuleChainId').value);
}
this.deviceProfileService.saveDeviceProfile(deepTrim(deviceProfile)).subscribe(
(savedDeviceProfile) => {
this.dialogRef.close(savedDeviceProfile);

6
ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html

@ -77,6 +77,12 @@
[queueType]="serviceType"
formControlName="defaultQueueName">
</tb-queue-autocomplete>
<tb-rule-chain-autocomplete
labelText="asset-profile.default-edge-rule-chain"
formControlName="defaultEdgeRuleChainId"
[ruleChainType]="edgeRuleChainType">
<div tb-hint>{{'asset-profile.default-edge-rule-chain-hint' | translate}}</div>
</tb-rule-chain-autocomplete>
<tb-image-input fxFlex
label="{{'asset-profile.image' | translate}}"
maxSizeByte="524288"

8
ui-ngx/src/app/modules/home/components/profile/asset-profile.component.ts

@ -28,6 +28,7 @@ import { ServiceType } from '@shared/models/queue.models';
import { EntityId } from '@shared/models/id/entity-id';
import { DashboardId } from '@shared/models/id/dashboard-id';
import { AssetProfile, TB_SERVICE_QUEUE } from '@shared/models/asset.models';
import { RuleChainType } from '@shared/models/rule-chain.models';
@Component({
selector: 'tb-asset-profile',
@ -43,6 +44,8 @@ export class AssetProfileComponent extends EntityComponent<AssetProfile> {
serviceType = ServiceType.TB_RULE_ENGINE;
edgeRuleChainType = RuleChainType.EDGE;
TB_SERVICE_QUEUE = TB_SERVICE_QUEUE;
assetProfileId: EntityId;
@ -73,6 +76,7 @@ export class AssetProfileComponent extends EntityComponent<AssetProfile> {
defaultRuleChainId: [entity && entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null, []],
defaultDashboardId: [entity && entity.defaultDashboardId ? entity.defaultDashboardId.id : null, []],
defaultQueueName: [entity ? entity.defaultQueueName : null, []],
defaultEdgeRuleChainId: [entity && entity.defaultEdgeRuleChainId ? entity.defaultEdgeRuleChainId.id : null, []],
description: [entity ? entity.description : '', []],
}
);
@ -86,6 +90,7 @@ export class AssetProfileComponent extends EntityComponent<AssetProfile> {
this.entityForm.patchValue({defaultRuleChainId: entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null}, {emitEvent: false});
this.entityForm.patchValue({defaultDashboardId: entity.defaultDashboardId ? entity.defaultDashboardId.id : null}, {emitEvent: false});
this.entityForm.patchValue({defaultQueueName: entity.defaultQueueName}, {emitEvent: false});
this.entityForm.patchValue({defaultEdgeRuleChainId: entity.defaultEdgeRuleChainId ? entity.defaultEdgeRuleChainId.id : null}, {emitEvent: false});
this.entityForm.patchValue({description: entity.description}, {emitEvent: false});
}
@ -96,6 +101,9 @@ export class AssetProfileComponent extends EntityComponent<AssetProfile> {
if (formValue.defaultDashboardId) {
formValue.defaultDashboardId = new DashboardId(formValue.defaultDashboardId);
}
if (formValue.defaultEdgeRuleChainId) {
formValue.defaultEdgeRuleChainId = new RuleChainId(formValue.defaultEdgeRuleChainId);
}
return super.prepareFormValue(formValue);
}

6
ui-ngx/src/app/modules/home/components/profile/device-profile.component.html

@ -77,6 +77,12 @@
[queueType]="serviceType"
formControlName="defaultQueueName">
</tb-queue-autocomplete>
<tb-rule-chain-autocomplete
labelText="device-profile.default-edge-rule-chain"
formControlName="defaultEdgeRuleChainId"
[ruleChainType]="edgeRuleChainType">
<div tb-hint>{{'device-profile.default-edge-rule-chain-hint' | translate}}</div>
</tb-rule-chain-autocomplete>
<tb-ota-package-autocomplete
[useFullEntityId]="true"
[showDetailsPageLink]="true"

8
ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts

@ -42,6 +42,7 @@ import { ServiceType } from '@shared/models/queue.models';
import { EntityId } from '@shared/models/id/entity-id';
import { OtaUpdateType } from '@shared/models/ota-package.models';
import { DashboardId } from '@shared/models/id/dashboard-id';
import { RuleChainType } from '@shared/models/rule-chain.models';
@Component({
selector: 'tb-device-profile',
@ -71,6 +72,8 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
serviceType = ServiceType.TB_RULE_ENGINE;
edgeRuleChainType = RuleChainType.EDGE;
deviceProfileId: EntityId;
otaUpdateType = OtaUpdateType;
@ -118,6 +121,7 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
defaultRuleChainId: [entity && entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null, []],
defaultDashboardId: [entity && entity.defaultDashboardId ? entity.defaultDashboardId.id : null, []],
defaultQueueName: [entity ? entity.defaultQueueName : null, []],
defaultEdgeRuleChainId: [entity && entity.defaultEdgeRuleChainId ? entity.defaultEdgeRuleChainId.id : null, []],
firmwareId: [entity ? entity.firmwareId : null],
softwareId: [entity ? entity.softwareId : null],
description: [entity ? entity.description : '', []],
@ -198,6 +202,7 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
this.entityForm.patchValue({defaultRuleChainId: entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null}, {emitEvent: false});
this.entityForm.patchValue({defaultDashboardId: entity.defaultDashboardId ? entity.defaultDashboardId.id : null}, {emitEvent: false});
this.entityForm.patchValue({defaultQueueName: entity.defaultQueueName}, {emitEvent: false});
this.entityForm.patchValue({defaultEdgeRuleChainId: entity.defaultEdgeRuleChainId ? entity.defaultEdgeRuleChainId.id : null}, {emitEvent: false});
this.entityForm.patchValue({firmwareId: entity.firmwareId}, {emitEvent: false});
this.entityForm.patchValue({softwareId: entity.softwareId}, {emitEvent: false});
this.entityForm.patchValue({description: entity.description}, {emitEvent: false});
@ -210,6 +215,9 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
if (formValue.defaultDashboardId) {
formValue.defaultDashboardId = new DashboardId(formValue.defaultDashboardId);
}
if (formValue.defaultEdgeRuleChainId) {
formValue.defaultEdgeRuleChainId = new RuleChainId(formValue.defaultEdgeRuleChainId);
}
const deviceProvisionConfiguration: DeviceProvisionConfiguration = formValue.profileData.provisionConfiguration;
formValue.provisionType = deviceProvisionConfiguration.type;
formValue.provisionDeviceKey = deviceProvisionConfiguration.provisionDeviceKey;

5
ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html

@ -16,7 +16,7 @@
-->
<mat-form-field [formGroup]="selectRuleChainFormGroup" class="mat-block">
<input matInput type="text" placeholder="{{ ruleChainLabel | translate }}"
<input matInput type="text" placeholder="{{ labelText | translate }}"
#ruleChainInput
formControlName="ruleChainId"
(focusin)="onFocus()"
@ -54,4 +54,7 @@
<mat-error *ngIf="selectRuleChainFormGroup.get('ruleChainId').hasError('required')">
{{ 'rulechain.rulechain-required' | translate }}
</mat-error>
<mat-hint>
<ng-content select="[tb-hint]"></ng-content>
</mat-hint>
</mat-form-field>

10
ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts

@ -45,16 +45,17 @@ export class RuleChainAutocompleteComponent implements ControlValueAccessor, OnI
selectRuleChainFormGroup: FormGroup;
ruleChainLabel = 'rulechain.rulechain';
modelValue: string | null;
@Input()
labelText: string;
labelText: string = 'rulechain.rulechain';
@Input()
requiredText: string;
@Input()
ruleChainType: RuleChainType = RuleChainType.CORE;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
@ -191,9 +192,8 @@ export class RuleChainAutocompleteComponent implements ControlValueAccessor, OnI
fetchRuleChain(searchText?: string): Observable<Array<BaseData<EntityId>>> {
this.searchText = searchText;
// @voba: at the moment device profiles are not supported by edge, so 'core' hardcoded
return this.entityService.getEntitiesByNameFilter(EntityType.RULE_CHAIN, searchText,
50, RuleChainType.CORE, {ignoreLoading: true}).pipe(
50, this.ruleChainType, {ignoreLoading: true}).pipe(
catchError(() => of([]))
);
}

43
ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.html

@ -0,0 +1,43 @@
<!--
Copyright © 2016-2022 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 style="min-width: 800px;">
<mat-toolbar color="primary">
<h2><mat-icon>info_outline</mat-icon>
{{ 'edge.install-connect-instructions' | translate }}</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<tb-markdown [data]="instructions" lineNumbers fallbackToPlainMarkdown></tb-markdown>
</div>
<div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
</div>

46
ui-ngx/src/app/modules/home/pages/edge/edge-instructions-dialog.component.ts

@ -0,0 +1,46 @@
///
/// Copyright © 2016-2022 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, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
import { DialogComponent } from "@shared/components/dialog.component";
import { Store } from "@ngrx/store";
import { AppState } from "@core/core.state";
import { Router } from "@angular/router";
export interface EdgeInstructionsData {
instructions: string;
}
@Component({
selector: 'tb-edge-instructions',
templateUrl: './edge-instructions-dialog.component.html'
})
export class EdgeInstructionsDialogComponent extends DialogComponent<EdgeInstructionsDialogComponent, EdgeInstructionsData> {
instructions: string = this.data.instructions;
constructor(protected store: Store<AppState>,
protected router: Router,
public dialogRef: MatDialogRef<EdgeInstructionsDialogComponent, EdgeInstructionsData>,
@Inject(MAT_DIALOG_DATA) public data: EdgeInstructionsData) {
super(store, router, dialogRef);
}
cancel(): void {
this.dialogRef.close(null);
}
}

9
ui-ngx/src/app/modules/home/pages/edge/edge.component.html

@ -112,6 +112,15 @@
<span translate>edge.sync</span>
</button>
</div>
<div fxLayout="row" fxLayout.xs="column">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'openInstructions')"
[fxShow]="!isEdit">
<mat-icon>info_outline</mat-icon>
<span>{{ 'edge.install-connect-instructions' | translate }}</span>
</button>
</div>
</div>
<div class="mat-padding" fxLayout="column">
<mat-form-field class="mat-block"

4
ui-ngx/src/app/modules/home/pages/edge/edge.module.ts

@ -23,12 +23,14 @@ import { EdgeRoutingModule } from '@home/pages/edge/edge-routing.module';
import { EdgeComponent } from '@modules/home/pages/edge/edge.component';
import { EdgeTableHeaderComponent } from '@home/pages/edge/edge-table-header.component';
import { EdgeTabsComponent } from '@home/pages/edge/edge-tabs.component';
import { EdgeInstructionsDialogComponent } from './edge-instructions-dialog.component';
@NgModule({
declarations: [
EdgeComponent,
EdgeTableHeaderComponent,
EdgeTabsComponent
EdgeTabsComponent,
EdgeInstructionsDialogComponent
],
imports: [
CommonModule,

26
ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts

@ -51,13 +51,17 @@ import {
AddEntitiesToCustomerDialogData
} from '../../dialogs/add-entities-to-customer-dialog.component';
import { HomeDialogsService } from '@home/dialogs/home-dialogs.service';
import { Edge, EdgeInfo } from '@shared/models/edge.models';
import { Edge, EdgeInfo, EdgeInstallInstructions } from '@shared/models/edge.models';
import { EdgeService } from '@core/http/edge.service';
import { EdgeComponent } from '@home/pages/edge/edge.component';
import { EdgeTableHeaderComponent } from '@home/pages/edge/edge-table-header.component';
import { EdgeId } from '@shared/models/id/edge-id';
import { EdgeTabsComponent } from '@home/pages/edge/edge-tabs.component';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import {
EdgeInstructionsData,
EdgeInstructionsDialogComponent
} from "@home/pages/edge/edge-instructions-dialog.component";
@Injectable()
export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeInfo>> {
@ -526,6 +530,23 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI
);
}
openInstructions($event, edge) {
if ($event) {
$event.stopPropagation();
}
this.edgeService.getEdgeDockerInstallInstructions(edge.id.id).subscribe(
(edgeInstructionsTemplate: EdgeInstallInstructions) => {
this.dialog.open<EdgeInstructionsDialogComponent, EdgeInstructionsData>(EdgeInstructionsDialogComponent, {
disableClose: false,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
instructions: edgeInstructionsTemplate.dockerInstallInstructions
}
});
}
)
}
onEdgeAction(action: EntityAction<EdgeInfo>, config: EntityTableConfig<EdgeInfo>): boolean {
switch (action.action) {
case 'open':
@ -558,6 +579,9 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI
case 'syncEdge':
this.syncEdge(action.event, action.entity);
return true;
case 'openInstructions':
this.openInstructions(action.event, action.entity);
return true;
}
}

1
ui-ngx/src/app/shared/models/asset.models.ts

@ -35,6 +35,7 @@ export interface AssetProfile extends BaseData<AssetProfileId>, ExportableEntity
defaultRuleChainId?: RuleChainId;
defaultDashboardId?: DashboardId;
defaultQueueName?: string;
defaultEdgeRuleChainId?: RuleChainId;
}
export interface AssetProfileInfo extends EntityInfoData {

1
ui-ngx/src/app/shared/models/device.models.ts

@ -579,6 +579,7 @@ export interface DeviceProfile extends BaseData<DeviceProfileId>, ExportableEnti
firmwareId?: OtaPackageId;
softwareId?: OtaPackageId;
profileData: DeviceProfileData;
defaultEdgeRuleChainId?: RuleChainId;
}
export interface DeviceProfileInfo extends EntityInfoData {

4
ui-ngx/src/app/shared/models/edge.models.ts

@ -167,3 +167,7 @@ export interface EdgeEvent extends BaseData<EventId> {
uid: string;
body: string;
}
export interface EdgeInstallInstructions {
dockerInstallInstructions: string;
}

5
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1291,6 +1291,8 @@
"description": "Description",
"default": "Default",
"default-rule-chain": "Default rule chain",
"default-edge-rule-chain": "Default edge rule chain",
"default-edge-rule-chain-hint": "Used on edge as rule chain to process incoming data for assets of this asset profile",
"mobile-dashboard": "Mobile dashboard",
"mobile-dashboard-hint": "Used by mobile application as a asset details dashboard",
"select-queue-hint": "Select from a drop-down list.",
@ -1351,6 +1353,8 @@
"profile-configuration": "Profile configuration",
"transport-configuration": "Transport configuration",
"default-rule-chain": "Default rule chain",
"default-edge-rule-chain": "Default edge rule chain",
"default-edge-rule-chain-hint": "Used on edge as rule chain to process incoming data for devices of this device profile",
"mobile-dashboard": "Mobile dashboard",
"mobile-dashboard-hint": "Used by mobile application as a device details dashboard",
"select-queue-hint": "Select from a drop-down list.",
@ -1757,6 +1761,7 @@
"make-private-edge-title": "Are you sure you want to make the edge '{{edgeName}}' private?",
"make-private-edge-text": "After the confirmation the edge and all its data will be made private and won't be accessible by others.",
"import": "Import edge",
"install-connect-instructions": "Install & Connect Instructions",
"label": "Label",
"load-entity-error": "Failed to load data. Entity has been deleted.",
"assign-new-edge": "Assign new edge",

Loading…
Cancel
Save