Browse Source

Merge with develop/3.5.2

pull/8843/head
ViacheslavKlimov 3 years ago
parent
commit
8a023e9655
  1. 24
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 20
      application/src/main/java/org/thingsboard/server/controller/NotificationController.java
  3. 34
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  4. 4
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java
  5. 48
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  6. 96
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java
  7. 73
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java
  8. 77
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java
  9. 69
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProfileProcessor.java
  10. 77
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java
  11. 85
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java
  12. 14
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java
  13. 102
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProfileProcessor.java
  14. 30
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java
  15. 74
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java
  16. 76
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java
  17. 93
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java
  18. 1
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
  19. 4
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  20. 13
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  21. 4
      application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java
  22. 12
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  23. 120
      application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java
  24. 57
      application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java
  25. 80
      application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java
  26. 71
      application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java
  27. 154
      application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java
  28. 69
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  29. 12
      application/src/test/java/org/thingsboard/server/service/notification/TestNotificationSettingsService.java
  30. 215
      application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java
  31. 2
      application/src/test/resources/application-test.properties
  32. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java
  33. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
  34. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
  35. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java
  36. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
  37. 6
      common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java
  38. 41
      common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java
  39. 5
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java
  40. 82
      common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/UserNotificationSettings.java
  41. 2
      common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java
  42. 2
      common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java
  43. 18
      common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java
  44. 10
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java
  45. 4
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java
  46. 7
      common/edge-api/src/main/proto/edge.proto
  47. 73
      common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
  48. 16
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java
  49. 18
      dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
  50. 13
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
  51. 16
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java
  52. 16
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
  53. 64
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java
  54. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java
  55. 2
      dao/src/main/resources/sql/schema-entities-idx.sql
  56. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java
  57. 3
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNodeTest.java
  58. 8
      ui-ngx/src/app/core/auth/auth.service.ts
  59. 9
      ui-ngx/src/app/core/http/notification.service.ts
  60. 12
      ui-ngx/src/app/core/utils.ts
  61. 18
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  62. 94
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.html
  63. 88
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.scss
  64. 233
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts
  65. 50
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html
  66. 69
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.scss
  67. 173
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts
  68. 136
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.html
  69. 293
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts
  70. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html
  71. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html
  72. 1
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html
  73. 9
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts
  74. 2
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html
  75. 13
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts
  76. 6
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts
  77. 4
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts
  78. 5
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.html
  79. 3
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts
  80. 59
      ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts
  81. 90
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html
  82. 156
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.scss
  83. 261
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts
  84. 232
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card.models.ts
  85. 2
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html
  86. 2
      ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts
  87. 1
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts
  88. 46
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts
  89. 48
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html
  90. 64
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.ts
  91. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts
  92. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.html
  93. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts
  94. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  95. 9
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  96. 6
      ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts
  97. 8
      ui-ngx/src/app/modules/home/pages/notification/notification.module.ts
  98. 40
      ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html
  99. 24
      ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.scss
  100. 126
      ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.ts

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

File diff suppressed because one or more lines are too long

20
application/src/main/java/org/thingsboard/server/controller/NotificationController.java

@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestInfo;
import org.thingsboard.server.common.data.notification.NotificationRequestPreview;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
@ -299,7 +300,7 @@ public class NotificationController extends BaseController {
case PLATFORM_USERS: {
PageData<User> recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(),
(PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize, 0, null,
new SortOrder("createdTime", SortOrder.Direction.DESC)));
SortOrder.BY_CREATED_TIME_DESC));
recipientsCount = (int) recipients.getTotalElements();
recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList());
break;
@ -444,10 +445,23 @@ public class NotificationController extends BaseController {
notes = "Returns the list of delivery methods that are properly configured and are allowed to be used for sending notifications." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/notification/deliveryMethods")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public Set<NotificationDeliveryMethod> getAvailableDeliveryMethods(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.READ);
return notificationCenter.getAvailableDeliveryMethods(user.getTenantId());
}
@PostMapping("/notification/settings/user")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public UserNotificationSettings saveUserNotificationSettings(@RequestBody @Valid UserNotificationSettings settings,
@AuthenticationPrincipal SecurityUser user) {
return notificationSettingsService.saveUserNotificationSettings(user.getTenantId(), user.getId(), settings);
}
@GetMapping("/notification/settings/user")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public UserNotificationSettings getUserNotificationSettings(@AuthenticationPrincipal SecurityUser user) {
return notificationSettingsService.getUserNotificationSettings(user.getTenantId(), user.getId(), true);
}
}

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

@ -39,12 +39,16 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg;
import org.thingsboard.server.gen.edge.v1.ConnectResponseCode;
import org.thingsboard.server.gen.edge.v1.ConnectResponseMsg;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
@ -53,6 +57,7 @@ import org.thingsboard.server.gen.edge.v1.EdgeConfiguration;
import org.thingsboard.server.gen.edge.v1.EdgeUpdateMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.EntityDataProto;
import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg;
import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg;
import org.thingsboard.server.gen.edge.v1.RelationRequestMsg;
import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg;
@ -151,8 +156,8 @@ public final class EdgeGrpcSession implements Closeable {
}
if (connected) {
if (requestMsg.getMsgType().equals(RequestMsgType.SYNC_REQUEST_RPC_MESSAGE)) {
if (requestMsg.hasSyncRequestMsg() && requestMsg.getSyncRequestMsg().getSyncRequired()) {
boolean fullSync = true;
if (requestMsg.hasSyncRequestMsg()) {
boolean fullSync = false;
if (requestMsg.getSyncRequestMsg().hasFullSync()) {
fullSync = requestMsg.getSyncRequestMsg().getFullSync();
}
@ -654,6 +659,11 @@ public final class EdgeGrpcSession implements Closeable {
result.addAll(ctx.getTelemetryProcessor().processTelemetryMsg(edge.getTenantId(), entityData));
}
}
if (uplinkMsg.getDeviceProfileUpdateMsgCount() > 0) {
for (DeviceProfileUpdateMsg deviceProfileUpdateMsg : uplinkMsg.getDeviceProfileUpdateMsgList()) {
result.add(ctx.getDeviceProfileProcessor().processDeviceProfileMsgFromEdge(edge.getTenantId(), edge, deviceProfileUpdateMsg));
}
}
if (uplinkMsg.getDeviceUpdateMsgCount() > 0) {
for (DeviceUpdateMsg deviceUpdateMsg : uplinkMsg.getDeviceUpdateMsgList()) {
result.add(ctx.getDeviceProcessor().processDeviceMsgFromEdge(edge.getTenantId(), edge, deviceUpdateMsg));
@ -664,16 +674,36 @@ public final class EdgeGrpcSession implements Closeable {
result.add(ctx.getDeviceProcessor().processDeviceCredentialsMsg(edge.getTenantId(), deviceCredentialsUpdateMsg));
}
}
if (uplinkMsg.getAssetProfileUpdateMsgCount() > 0) {
for (AssetProfileUpdateMsg assetProfileUpdateMsg : uplinkMsg.getAssetProfileUpdateMsgList()) {
result.add(ctx.getAssetProfileProcessor().processAssetProfileMsgFromEdge(edge.getTenantId(), edge, assetProfileUpdateMsg));
}
}
if (uplinkMsg.getAssetUpdateMsgCount() > 0) {
for (AssetUpdateMsg assetUpdateMsg : uplinkMsg.getAssetUpdateMsgList()) {
result.add(ctx.getAssetProcessor().processAssetMsgFromEdge(edge.getTenantId(), edge, assetUpdateMsg));
}
}
if (uplinkMsg.getAlarmUpdateMsgCount() > 0) {
for (AlarmUpdateMsg alarmUpdateMsg : uplinkMsg.getAlarmUpdateMsgList()) {
result.add(ctx.getAlarmProcessor().processAlarmMsg(edge.getTenantId(), alarmUpdateMsg));
}
}
if (uplinkMsg.getEntityViewUpdateMsgCount() > 0) {
for (EntityViewUpdateMsg entityViewUpdateMsg : uplinkMsg.getEntityViewUpdateMsgList()) {
result.add(ctx.getEntityViewProcessor().processEntityViewMsgFromEdge(edge.getTenantId(), edge, entityViewUpdateMsg));
}
}
if (uplinkMsg.getRelationUpdateMsgCount() > 0) {
for (RelationUpdateMsg relationUpdateMsg : uplinkMsg.getRelationUpdateMsgList()) {
result.add(ctx.getRelationProcessor().processRelationMsg(edge.getTenantId(), relationUpdateMsg));
}
}
if (uplinkMsg.getDashboardUpdateMsgCount() > 0) {
for (DashboardUpdateMsg dashboardUpdateMsg : uplinkMsg.getDashboardUpdateMsgList()) {
result.add(ctx.getDashboardProcessor().processDashboardMsgFromEdge(edge.getTenantId(), edge, dashboardUpdateMsg));
}
}
if (uplinkMsg.getRuleChainMetadataRequestMsgCount() > 0) {
for (RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg : uplinkMsg.getRuleChainMetadataRequestMsgList()) {
result.add(ctx.getEdgeRequestsService().processRuleChainMetadataRequestMsg(edge.getTenantId(), edge, ruleChainMetadataRequestMsg));

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

@ -51,8 +51,6 @@ public class EdgeSyncCursor {
fetchers.add(new QueuesEdgeEventFetcher(ctx.getQueueService()));
fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService()));
fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService(), ctx.getFreemarkerConfig()));
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()));
@ -61,6 +59,8 @@ public class EdgeSyncCursor {
fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId()));
}
}
fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService()));
fetchers.add(new AssetProfilesEdgeEventFetcher(ctx.getAssetProfileService()));
fetchers.add(new DevicesEdgeEventFetcher(ctx.getDeviceService()));
fetchers.add(new AssetsEdgeEventFetcher(ctx.getAssetService()));
fetchers.add(new EntityViewsEdgeEventFetcher(ctx.getEntityViewService()));

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

@ -23,9 +23,14 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
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.EdgeUtils;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
@ -43,8 +48,11 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
@ -106,6 +114,7 @@ import java.util.concurrent.locks.ReentrantLock;
public abstract class BaseEdgeProcessor {
protected static final Lock deviceCreationLock = new ReentrantLock();
protected static final Lock assetCreationLock = new ReentrantLock();
protected static final int DEFAULT_PAGE_SIZE = 100;
@ -197,6 +206,21 @@ public abstract class BaseEdgeProcessor {
@Autowired
protected DataValidator<Device> deviceValidator;
@Autowired
protected DataValidator<DeviceProfile> deviceProfileValidator;
@Autowired
protected DataValidator<Asset> assetValidator;
@Autowired
protected DataValidator<AssetProfile> assetProfileValidator;
@Autowired
protected DataValidator<Dashboard> dashboardValidator;
@Autowired
protected DataValidator<EntityView> entityViewValidator;
@Autowired
protected EdgeMsgConstructor edgeMsgConstructor;
@ -520,4 +544,28 @@ public abstract class BaseEdgeProcessor {
return false;
}
}
protected void createRelationFromEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) {
EntityRelation relation = new EntityRelation();
relation.setFrom(edgeId);
relation.setTo(entityId);
relation.setTypeGroup(RelationTypeGroup.COMMON);
relation.setType(EntityRelation.EDGE_TYPE);
relationService.saveRelation(tenantId, relation);
}
protected TbMsgMetaData getActionTbMsgMetaData(Edge edge, CustomerId customerId) {
TbMsgMetaData metaData = getTbMsgMetaData(edge);
if (customerId != null && !customerId.isNullUid()) {
metaData.putValue("customerId", customerId.toString());
}
return metaData;
}
protected TbMsgMetaData getTbMsgMetaData(Edge edge) {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("edgeId", edge.getId().toString());
metaData.putValue("edgeName", edge.getName());
return metaData;
}
}

96
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java

@ -15,23 +15,113 @@
*/
package org.thingsboard.server.service.edge.rpc.processor.asset;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
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.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.dao.asset.BaseAssetService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import java.util.UUID;
@Component
@Slf4j
@TbCoreComponent
public class AssetEdgeProcessor extends BaseEdgeProcessor {
public class AssetEdgeProcessor extends BaseAssetProcessor {
public ListenableFuture<Void> processAssetMsgFromEdge(TenantId tenantId, Edge edge, AssetUpdateMsg assetUpdateMsg) {
log.trace("[{}] executing processAssetMsgFromEdge [{}] from edge [{}]", tenantId, assetUpdateMsg, edge.getName());
AssetId assetId = new AssetId(new UUID(assetUpdateMsg.getIdMSB(), assetUpdateMsg.getIdLSB()));
try {
edgeSynchronizationManager.getSync().set(true);
switch (assetUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE:
saveOrUpdateAsset(tenantId, assetId, assetUpdateMsg, edge);
return Futures.immediateFuture(null);
case ENTITY_DELETED_RPC_MESSAGE:
Asset assetToDelete = assetService.findAssetById(tenantId, assetId);
if (assetToDelete != null) {
assetService.unassignAssetFromEdge(tenantId, assetId, edge.getId());
}
return Futures.immediateFuture(null);
case UNRECOGNIZED:
default:
return handleUnsupportedMsgType(assetUpdateMsg.getMsgType());
}
} catch (DataValidationException e) {
if (e.getMessage().contains("limit reached")) {
log.warn("[{}] Number of allowed asset violated {}", tenantId, assetUpdateMsg, e);
return Futures.immediateFuture(null);
} else {
return Futures.immediateFailedFuture(e);
}
} finally {
edgeSynchronizationManager.getSync().remove();
}
}
private void saveOrUpdateAsset(TenantId tenantId, AssetId assetId, AssetUpdateMsg assetUpdateMsg, Edge edge) {
CustomerId customerId = safeGetCustomerId(assetUpdateMsg.getCustomerIdMSB(), assetUpdateMsg.getCustomerIdLSB());
Pair<Boolean, Boolean> resultPair = super.saveOrUpdateAsset(tenantId, assetId, assetUpdateMsg, customerId);
Boolean created = resultPair.getFirst();
if (created) {
createRelationFromEdge(tenantId, edge.getId(), assetId);
pushAssetCreatedEventToRuleEngine(tenantId, edge, assetId);
assetService.assignAssetToEdge(tenantId, assetId, edge.getId());
}
Boolean assetNameUpdated = resultPair.getSecond();
if (assetNameUpdated) {
saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ASSET, EdgeEventActionType.UPDATED, assetId, null);
}
}
private void pushAssetCreatedEventToRuleEngine(TenantId tenantId, Edge edge, AssetId assetId) {
try {
Asset asset = assetService.findAssetById(tenantId, assetId);
ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(asset);
TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, assetId, asset.getCustomerId(),
getActionTbMsgMetaData(edge, asset.getCustomerId()), TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode));
tbClusterService.pushMsgToRuleEngine(tenantId, assetId, tbMsg, new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
log.debug("Successfully send ENTITY_CREATED EVENT to rule engine [{}]", asset);
}
@Override
public void onFailure(Throwable t) {
log.warn("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", asset, t);
}
});
} catch (JsonProcessingException | IllegalArgumentException e) {
log.warn("[{}] Failed to push asset action to rule engine: {}", assetId, DataConstants.ENTITY_CREATED, e);
}
}
public DownlinkMsg convertAssetEventToDownlink(EdgeEvent edgeEvent) {
AssetId assetId = new AssetId(edgeEvent.getEntityId());
@ -43,7 +133,7 @@ public class AssetEdgeProcessor extends BaseEdgeProcessor {
case ASSIGNED_TO_CUSTOMER:
case UNASSIGNED_FROM_CUSTOMER:
Asset asset = assetService.findAssetById(edgeEvent.getTenantId(), assetId);
if (asset != null) {
if (asset != null && !BaseAssetService.TB_SERVICE_QUEUE.equals(asset.getType())) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
AssetUpdateMsg assetUpdateMsg =
assetMsgConstructor.constructAssetUpdatedMsg(msgType, asset);

73
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java

@ -15,22 +15,91 @@
*/
package org.thingsboard.server.service.edge.rpc.processor.asset;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import java.util.UUID;
@Component
@Slf4j
@TbCoreComponent
public class AssetProfileEdgeProcessor extends BaseEdgeProcessor {
public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor {
public ListenableFuture<Void> processAssetProfileMsgFromEdge(TenantId tenantId, Edge edge, AssetProfileUpdateMsg assetProfileUpdateMsg) {
log.trace("[{}] executing processAssetProfileMsgFromEdge [{}] from edge [{}]", tenantId, assetProfileUpdateMsg, edge.getName());
AssetProfileId assetProfileId = new AssetProfileId(new UUID(assetProfileUpdateMsg.getIdMSB(), assetProfileUpdateMsg.getIdLSB()));
try {
edgeSynchronizationManager.getSync().set(true);
switch (assetProfileUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE:
saveOrUpdateAssetProfile(tenantId, assetProfileId, assetProfileUpdateMsg, edge);
return Futures.immediateFuture(null);
case ENTITY_DELETED_RPC_MESSAGE:
case UNRECOGNIZED:
default:
return handleUnsupportedMsgType(assetProfileUpdateMsg.getMsgType());
}
} catch (DataValidationException e) {
log.warn("Failed to process AssetProfileUpdateMsg from Edge [{}]", assetProfileUpdateMsg, e);
return Futures.immediateFailedFuture(e);
} finally {
edgeSynchronizationManager.getSync().remove();
}
}
private void saveOrUpdateAssetProfile(TenantId tenantId, AssetProfileId assetProfileId, AssetProfileUpdateMsg assetProfileUpdateMsg, Edge edge) {
boolean created = super.saveOrUpdateAssetProfile(tenantId, assetProfileId, assetProfileUpdateMsg);
if (created) {
createRelationFromEdge(tenantId, edge.getId(), assetProfileId);
pushAssetProfileCreatedEventToRuleEngine(tenantId, edge, assetProfileId);
}
}
private void pushAssetProfileCreatedEventToRuleEngine(TenantId tenantId, Edge edge, AssetProfileId assetProfileId) {
try {
AssetProfile assetProfile = assetProfileService.findAssetProfileById(tenantId, assetProfileId);
ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(assetProfile);
TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, assetProfileId, getTbMsgMetaData(edge),
TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode));
tbClusterService.pushMsgToRuleEngine(tenantId, assetProfileId, tbMsg, new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
log.debug("Successfully send ENTITY_CREATED EVENT to rule engine [{}]", assetProfile);
}
@Override
public void onFailure(Throwable t) {
log.warn("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", assetProfile, t);
}
});
} catch (JsonProcessingException | IllegalArgumentException e) {
log.warn("[{}] Failed to push asset profile action to rule engine: {}", assetProfileId, DataConstants.ENTITY_CREATED, e);
}
}
public DownlinkMsg convertAssetProfileEventToDownlink(EdgeEvent edgeEvent) {
AssetProfileId assetProfileId = new AssetProfileId(edgeEvent.getEntityId());

77
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProcessor.java

@ -0,0 +1,77 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.asset;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.util.Pair;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import java.util.UUID;
@Slf4j
public abstract class BaseAssetProcessor extends BaseEdgeProcessor {
protected Pair<Boolean, Boolean> saveOrUpdateAsset(TenantId tenantId, AssetId assetId, AssetUpdateMsg assetUpdateMsg, CustomerId customerId) {
boolean created = false;
boolean assetNameUpdated = false;
assetCreationLock.lock();
try {
Asset asset = assetService.findAssetById(tenantId, assetId);
String assetName = assetUpdateMsg.getName();
if (asset == null) {
created = true;
asset = new Asset();
asset.setTenantId(tenantId);
asset.setCreatedTime(Uuids.unixTimestamp(assetId.getId()));
}
Asset assetByName = assetService.findAssetByTenantIdAndName(tenantId, assetName);
if (assetByName != null && !assetByName.getId().equals(assetId)) {
assetName = assetName + "_" + StringUtils.randomAlphanumeric(15);
log.warn("Asset with name {} already exists. Renaming asset name to {}",
assetUpdateMsg.getName(), assetName);
assetNameUpdated = true;
}
asset.setName(assetName);
asset.setType(assetUpdateMsg.getType());
asset.setLabel(assetUpdateMsg.hasLabel() ? assetUpdateMsg.getLabel() : null);
asset.setAdditionalInfo(assetUpdateMsg.hasAdditionalInfo()
? JacksonUtil.toJsonNode(assetUpdateMsg.getAdditionalInfo()) : null);
UUID assetProfileUUID = safeGetUUID(assetUpdateMsg.getAssetProfileIdMSB(), assetUpdateMsg.getAssetProfileIdLSB());
asset.setAssetProfileId(assetProfileUUID != null ? new AssetProfileId(assetProfileUUID) : null);
asset.setCustomerId(customerId);
assetValidator.validate(asset, Asset::getTenantId);
if (created) {
asset.setId(assetId);
}
assetService.saveAsset(asset, false);
} finally {
assetCreationLock.unlock();
}
return Pair.of(created, assetNameUpdated);
}
}

69
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/BaseAssetProfileProcessor.java

@ -0,0 +1,69 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.asset;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
@Slf4j
public class BaseAssetProfileProcessor extends BaseEdgeProcessor {
protected boolean saveOrUpdateAssetProfile(TenantId tenantId, AssetProfileId assetProfileId, AssetProfileUpdateMsg assetProfileUpdateMsg) {
boolean created = false;
assetCreationLock.lock();
try {
AssetProfile assetProfile = assetProfileService.findAssetProfileById(tenantId, assetProfileId);
String assetProfileName = assetProfileUpdateMsg.getName();
if (assetProfile == null) {
created = true;
assetProfile = new AssetProfile();
assetProfile.setTenantId(tenantId);
assetProfile.setCreatedTime(Uuids.unixTimestamp(assetProfileId.getId()));
}
assetProfile.setName(assetProfileName);
assetProfile.setDefault(assetProfileUpdateMsg.getDefault());
assetProfile.setDefaultQueueName(assetProfileUpdateMsg.hasDefaultQueueName() ? assetProfileUpdateMsg.getDefaultQueueName() : null);
assetProfile.setDescription(assetProfileUpdateMsg.hasDescription() ? assetProfileUpdateMsg.getDescription() : null);
assetProfile.setImage(assetProfileUpdateMsg.hasImage()
? new String(assetProfileUpdateMsg.getImage().toByteArray(), StandardCharsets.UTF_8) : null);
UUID defaultRuleChainUUID = safeGetUUID(assetProfileUpdateMsg.getDefaultRuleChainIdMSB(), assetProfileUpdateMsg.getDefaultRuleChainIdLSB());
assetProfile.setDefaultRuleChainId(defaultRuleChainUUID != null ? new RuleChainId(defaultRuleChainUUID) : null);
UUID defaultDashboardUUID = safeGetUUID(assetProfileUpdateMsg.getDefaultDashboardIdMSB(), assetProfileUpdateMsg.getDefaultDashboardIdLSB());
assetProfile.setDefaultDashboardId(defaultDashboardUUID != null ? new DashboardId(defaultDashboardUUID) : null);
assetProfileValidator.validate(assetProfile, AssetProfile::getTenantId);
if (created) {
assetProfile.setId(assetProfileId);
}
assetProfileService.saveAssetProfile(assetProfile, false);
} finally {
assetCreationLock.unlock();
}
return created;
}
}

77
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java

@ -0,0 +1,77 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.dashboard;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.ShortCustomerInfo;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import java.util.Set;
@Slf4j
public abstract class BaseDashboardProcessor extends BaseEdgeProcessor {
protected boolean saveOrUpdateDashboard(TenantId tenantId, DashboardId dashboardId, DashboardUpdateMsg dashboardUpdateMsg, CustomerId customerId) {
boolean created = false;
Dashboard dashboard = dashboardService.findDashboardById(tenantId, dashboardId);
if (dashboard == null) {
created = true;
dashboard = new Dashboard();
dashboard.setTenantId(tenantId);
dashboard.setCreatedTime(Uuids.unixTimestamp(dashboardId.getId()));
}
dashboard.setTitle(dashboardUpdateMsg.getTitle());
dashboard.setConfiguration(JacksonUtil.toJsonNode(dashboardUpdateMsg.getConfiguration()));
Set<ShortCustomerInfo> assignedCustomers = null;
if (dashboardUpdateMsg.hasAssignedCustomers()) {
assignedCustomers = JacksonUtil.fromString(dashboardUpdateMsg.getAssignedCustomers(), new TypeReference<>() {
});
dashboard.setAssignedCustomers(assignedCustomers);
}
dashboardValidator.validate(dashboard, Dashboard::getTenantId);
if (created) {
dashboard.setId(dashboardId);
}
Dashboard savedDashboard = dashboardService.saveDashboard(dashboard, false);
if (assignedCustomers != null && !assignedCustomers.isEmpty()) {
for (ShortCustomerInfo assignedCustomer : assignedCustomers) {
if (assignedCustomer.getCustomerId().equals(customerId)) {
dashboardService.assignDashboardToCustomer(tenantId, dashboardId, assignedCustomer.getCustomerId());
}
}
} else {
unassignCustomersFromDashboard(tenantId, savedDashboard);
}
return created;
}
private void unassignCustomersFromDashboard(TenantId tenantId, Dashboard dashboard) {
if (dashboard.getAssignedCustomers() != null && !dashboard.getAssignedCustomers().isEmpty()) {
for (ShortCustomerInfo assignedCustomer : dashboard.getAssignedCustomers()) {
dashboardService.unassignDashboardFromCustomer(tenantId, dashboard.getId(), assignedCustomer.getCustomerId());
}
}
}
}

85
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java

@ -15,22 +15,103 @@
*/
package org.thingsboard.server.service.edge.rpc.processor.dashboard;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import java.util.UUID;
@Component
@Slf4j
@TbCoreComponent
public class DashboardEdgeProcessor extends BaseEdgeProcessor {
public class DashboardEdgeProcessor extends BaseDashboardProcessor {
public ListenableFuture<Void> processDashboardMsgFromEdge(TenantId tenantId, Edge edge, DashboardUpdateMsg dashboardUpdateMsg) {
log.trace("[{}] executing processDashboardMsgFromEdge [{}] from edge [{}]", tenantId, dashboardUpdateMsg, edge.getName());
DashboardId dashboardId = new DashboardId(new UUID(dashboardUpdateMsg.getIdMSB(), dashboardUpdateMsg.getIdLSB()));
try {
edgeSynchronizationManager.getSync().set(true);
switch (dashboardUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE:
saveOrUpdateDashboard(tenantId, dashboardId, dashboardUpdateMsg, edge);
return Futures.immediateFuture(null);
case ENTITY_DELETED_RPC_MESSAGE:
Dashboard dashboardToDelete = dashboardService.findDashboardById(tenantId, dashboardId);
if (dashboardToDelete != null) {
dashboardService.unassignDashboardFromEdge(tenantId, dashboardId, edge.getId());
}
return Futures.immediateFuture(null);
case UNRECOGNIZED:
default:
return handleUnsupportedMsgType(dashboardUpdateMsg.getMsgType());
}
} catch (DataValidationException e) {
if (e.getMessage().contains("limit reached")) {
log.warn("[{}] Number of allowed dashboard violated {}", tenantId, dashboardUpdateMsg, e);
return Futures.immediateFuture(null);
} else {
return Futures.immediateFailedFuture(e);
}
} finally {
edgeSynchronizationManager.getSync().remove();
}
}
private void saveOrUpdateDashboard(TenantId tenantId, DashboardId dashboardId, DashboardUpdateMsg dashboardUpdateMsg, Edge edge) {
CustomerId customerId = safeGetCustomerId(dashboardUpdateMsg.getCustomerIdMSB(), dashboardUpdateMsg.getCustomerIdLSB());
boolean created = super.saveOrUpdateDashboard(tenantId, dashboardId, dashboardUpdateMsg, customerId);
if (created) {
createRelationFromEdge(tenantId, edge.getId(), dashboardId);
pushDashboardCreatedEventToRuleEngine(tenantId, edge, dashboardId);
dashboardService.assignDashboardToEdge(tenantId, dashboardId, edge.getId());
}
}
private void pushDashboardCreatedEventToRuleEngine(TenantId tenantId, Edge edge, DashboardId dashboardId) {
try {
Dashboard dashboard = dashboardService.findDashboardById(tenantId, dashboardId);
ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(dashboard);
TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, dashboardId, null,
getActionTbMsgMetaData(edge, null), TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode));
tbClusterService.pushMsgToRuleEngine(tenantId, dashboardId, tbMsg, new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
log.debug("Successfully send ENTITY_CREATED EVENT to rule engine [{}]", dashboard);
}
@Override
public void onFailure(Throwable t) {
log.warn("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", dashboard, t);
}
});
} catch (JsonProcessingException | IllegalArgumentException e) {
log.warn("[{}] Failed to push dashboard action to rule engine: {}", dashboardId, DataConstants.ENTITY_CREATED, e);
}
}
public DownlinkMsg convertDashboardEventToDownlink(EdgeEvent edgeEvent) {
DashboardId dashboardId = new DashboardId(edgeEvent.getEntityId());

14
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java

@ -57,13 +57,13 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor {
device = new Device();
device.setTenantId(tenantId);
device.setCreatedTime(Uuids.unixTimestamp(deviceId.getId()));
Device deviceByName = deviceService.findDeviceByTenantIdAndName(tenantId, deviceName);
if (deviceByName != null) {
deviceName = deviceName + "_" + StringUtils.randomAlphabetic(15);
log.warn("Device with name {} already exists. Renaming device name to {}",
deviceUpdateMsg.getName(), deviceName);
deviceNameUpdated = true;
}
}
Device deviceByName = deviceService.findDeviceByTenantIdAndName(tenantId, deviceName);
if (deviceByName != null && !deviceByName.getId().equals(deviceId)) {
deviceName = deviceName + "_" + StringUtils.randomAlphabetic(15);
log.warn("Device with name {} already exists. Renaming device name to {}",
deviceUpdateMsg.getName(), deviceName);
deviceNameUpdated = true;
}
device.setName(deviceName);
device.setType(deviceUpdateMsg.getType());

102
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProfileProcessor.java

@ -0,0 +1,102 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.device;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.StringUtils;
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.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.UUID;
@Slf4j
public class BaseDeviceProfileProcessor extends BaseEdgeProcessor {
@Autowired
private DataDecodingEncodingService dataDecodingEncodingService;
protected boolean saveOrUpdateDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId, DeviceProfileUpdateMsg deviceProfileUpdateMsg) {
boolean created = false;
deviceCreationLock.lock();
try {
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfileId);
if (deviceProfile == null) {
created = true;
deviceProfile = new DeviceProfile();
deviceProfile.setTenantId(tenantId);
deviceProfile.setCreatedTime(Uuids.unixTimestamp(deviceProfileId.getId()));
}
deviceProfile.setName(deviceProfileUpdateMsg.getName());
deviceProfile.setDescription(deviceProfileUpdateMsg.hasDescription() ? deviceProfileUpdateMsg.getDescription() : null);
deviceProfile.setDefault(deviceProfileUpdateMsg.getDefault());
deviceProfile.setType(DeviceProfileType.valueOf(deviceProfileUpdateMsg.getType()));
deviceProfile.setTransportType(deviceProfileUpdateMsg.hasTransportType()
? DeviceTransportType.valueOf(deviceProfileUpdateMsg.getTransportType()) : DeviceTransportType.DEFAULT);
deviceProfile.setImage(deviceProfileUpdateMsg.hasImage()
? new String(deviceProfileUpdateMsg.getImage().toByteArray(), StandardCharsets.UTF_8) : null);
deviceProfile.setProvisionType(deviceProfileUpdateMsg.hasProvisionType()
? DeviceProfileProvisionType.valueOf(deviceProfileUpdateMsg.getProvisionType()) : DeviceProfileProvisionType.DISABLED);
deviceProfile.setProvisionDeviceKey(deviceProfileUpdateMsg.hasProvisionDeviceKey()
? deviceProfileUpdateMsg.getProvisionDeviceKey() : null);
deviceProfile.setDefaultQueueName(deviceProfileUpdateMsg.getDefaultQueueName());
Optional<DeviceProfileData> profileDataOpt =
dataDecodingEncodingService.decode(deviceProfileUpdateMsg.getProfileDataBytes().toByteArray());
deviceProfile.setProfileData(profileDataOpt.orElse(null));
UUID defaultRuleChainUUID = safeGetUUID(deviceProfileUpdateMsg.getDefaultRuleChainIdMSB(), deviceProfileUpdateMsg.getDefaultRuleChainIdLSB());
deviceProfile.setDefaultRuleChainId(defaultRuleChainUUID != null ? new RuleChainId(defaultRuleChainUUID) : null);
UUID defaultDashboardUUID = safeGetUUID(deviceProfileUpdateMsg.getDefaultDashboardIdMSB(), deviceProfileUpdateMsg.getDefaultDashboardIdLSB());
deviceProfile.setDefaultDashboardId(defaultDashboardUUID != null ? new DashboardId(defaultDashboardUUID) : null);
String defaultQueueName = StringUtils.isNotBlank(deviceProfileUpdateMsg.getDefaultQueueName())
? deviceProfileUpdateMsg.getDefaultQueueName() : null;
deviceProfile.setDefaultQueueName(defaultQueueName);
UUID firmwareUUID = safeGetUUID(deviceProfileUpdateMsg.getFirmwareIdMSB(), deviceProfileUpdateMsg.getFirmwareIdLSB());
deviceProfile.setFirmwareId(firmwareUUID != null ? new OtaPackageId(firmwareUUID) : null);
UUID softwareUUID = safeGetUUID(deviceProfileUpdateMsg.getSoftwareIdMSB(), deviceProfileUpdateMsg.getSoftwareIdLSB());
deviceProfile.setSoftwareId(softwareUUID != null ? new OtaPackageId(softwareUUID) : null);
deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId);
if (created) {
deviceProfile.setId(deviceProfileId);
}
deviceProfileService.saveDeviceProfile(deviceProfile, false);
} finally {
deviceCreationLock.unlock();
}
return created;
}
}

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

@ -35,12 +35,8 @@ 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.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rpc.RpcError;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.msg.TbMsg;
@ -114,15 +110,6 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor {
}
}
private void createRelationFromEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) {
EntityRelation relation = new EntityRelation();
relation.setFrom(edgeId);
relation.setTo(entityId);
relation.setTypeGroup(RelationTypeGroup.COMMON);
relation.setType(EntityRelation.EDGE_TYPE);
relationService.saveRelation(tenantId, relation);
}
private void pushDeviceCreatedEventToRuleEngine(TenantId tenantId, Edge edge, DeviceId deviceId) {
try {
Device device = deviceService.findDeviceById(tenantId, deviceId);
@ -137,7 +124,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor {
@Override
public void onFailure(Throwable t) {
log.debug("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", device, t);
log.warn("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", device, t);
}
});
} catch (JsonProcessingException | IllegalArgumentException e) {
@ -145,21 +132,6 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor {
}
}
private TbMsgMetaData getActionTbMsgMetaData(Edge edge, CustomerId customerId) {
TbMsgMetaData metaData = getTbMsgMetaData(edge);
if (customerId != null && !customerId.isNullUid()) {
metaData.putValue("customerId", customerId.toString());
}
return metaData;
}
private TbMsgMetaData getTbMsgMetaData(Edge edge) {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("edgeId", edge.getId().toString());
metaData.putValue("edgeName", edge.getName());
return metaData;
}
public ListenableFuture<Void> processDeviceRpcCallFromEdge(TenantId tenantId, Edge edge, DeviceRpcCallMsg deviceRpcCallMsg) {
log.trace("[{}] processDeviceRpcCallFromEdge [{}]", tenantId, deviceRpcCallMsg);
if (deviceRpcCallMsg.hasResponseMsg()) {

74
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java

@ -15,22 +15,92 @@
*/
package org.thingsboard.server.service.edge.rpc.processor.device;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import java.util.UUID;
@Component
@Slf4j
@TbCoreComponent
public class DeviceProfileEdgeProcessor extends BaseEdgeProcessor {
public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor {
public ListenableFuture<Void> processDeviceProfileMsgFromEdge(TenantId tenantId, Edge edge, DeviceProfileUpdateMsg deviceProfileUpdateMsg) {
log.trace("[{}] executing processDeviceProfileMsgFromEdge [{}] from edge [{}]", tenantId, deviceProfileUpdateMsg, edge.getName());
DeviceProfileId deviceProfileId = new DeviceProfileId(new UUID(deviceProfileUpdateMsg.getIdMSB(), deviceProfileUpdateMsg.getIdLSB()));
try {
edgeSynchronizationManager.getSync().set(true);
switch (deviceProfileUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE:
saveOrUpdateDeviceProfile(tenantId, deviceProfileId, deviceProfileUpdateMsg, edge);
return Futures.immediateFuture(null);
case ENTITY_DELETED_RPC_MESSAGE:
case UNRECOGNIZED:
default:
return handleUnsupportedMsgType(deviceProfileUpdateMsg.getMsgType());
}
} catch (DataValidationException e) {
log.warn("Failed to process DeviceProfileUpdateMsg from Edge [{}]", deviceProfileUpdateMsg, e);
return Futures.immediateFailedFuture(e);
} finally {
edgeSynchronizationManager.getSync().remove();
}
}
private void saveOrUpdateDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId, DeviceProfileUpdateMsg deviceProfileUpdateMsg, Edge edge) {
boolean created = super.saveOrUpdateDeviceProfile(tenantId, deviceProfileId, deviceProfileUpdateMsg);
if (created) {
createRelationFromEdge(tenantId, edge.getId(), deviceProfileId);
pushDeviceProfileCreatedEventToRuleEngine(tenantId, edge, deviceProfileId);
}
}
private void pushDeviceProfileCreatedEventToRuleEngine(TenantId tenantId, Edge edge, DeviceProfileId deviceProfileId) {
try {
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfileId);
ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(deviceProfile);
TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceProfileId, getTbMsgMetaData(edge),
TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode));
tbClusterService.pushMsgToRuleEngine(tenantId, deviceProfileId, tbMsg, new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
log.debug("Successfully send ENTITY_CREATED EVENT to rule engine [{}]", deviceProfile);
}
@Override
public void onFailure(Throwable t) {
log.warn("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", deviceProfile, t);
}
});
} catch (JsonProcessingException | IllegalArgumentException e) {
log.warn("[{}] Failed to push device profile action to rule engine: {}", deviceProfileId, DataConstants.ENTITY_CREATED, e);
}
}
public DownlinkMsg convertDeviceProfileEventToDownlink(EdgeEvent edgeEvent) {
DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId());

76
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/BaseEntityViewProcessor.java

@ -0,0 +1,76 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.entityview;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.util.Pair;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.EdgeEntityType;
import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import java.util.UUID;
@Slf4j
public abstract class BaseEntityViewProcessor extends BaseEdgeProcessor {
protected Pair<Boolean, Boolean> saveOrUpdateEntityView(TenantId tenantId, EntityViewId entityViewId, EntityViewUpdateMsg entityViewUpdateMsg, CustomerId customerId) {
boolean created = false;
boolean entityViewNameUpdated = false;
EntityView entityView = entityViewService.findEntityViewById(tenantId, entityViewId);
String entityViewName = entityViewUpdateMsg.getName();
if (entityView == null) {
created = true;
entityView = new EntityView();
entityView.setTenantId(tenantId);
entityView.setCreatedTime(Uuids.unixTimestamp(entityViewId.getId()));
}
EntityView entityViewByName = entityViewService.findEntityViewByTenantIdAndName(tenantId, entityViewName);
if (entityViewByName != null && !entityViewByName.getId().equals(entityViewId)) {
entityViewName = entityViewName + "_" + StringUtils.randomAlphanumeric(15);
log.warn("Entity view with name {} already exists. Renaming entity view name to {}",
entityViewUpdateMsg.getName(), entityViewName);
entityViewNameUpdated = true;
}
entityView.setName(entityViewName);
entityView.setType(entityViewUpdateMsg.getType());
entityView.setCustomerId(customerId);
entityView.setAdditionalInfo(entityViewUpdateMsg.hasAdditionalInfo() ?
JacksonUtil.toJsonNode(entityViewUpdateMsg.getAdditionalInfo()) : null);
UUID entityIdUUID = safeGetUUID(entityViewUpdateMsg.getEntityIdMSB(), entityViewUpdateMsg.getEntityIdLSB());
if (EdgeEntityType.DEVICE.equals(entityViewUpdateMsg.getEntityType())) {
entityView.setEntityId(entityIdUUID != null ? new DeviceId(entityIdUUID) : null);
} else if (EdgeEntityType.ASSET.equals(entityViewUpdateMsg.getEntityType())) {
entityView.setEntityId(entityIdUUID != null ? new AssetId(entityIdUUID) : null);
}
entityViewValidator.validate(entityView, EntityView::getTenantId);
if (created) {
entityView.setId(entityViewId);
}
entityViewService.saveEntityView(entityView, false);
return Pair.of(created, entityViewNameUpdated);
}
}

93
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java

@ -15,22 +15,111 @@
*/
package org.thingsboard.server.service.edge.rpc.processor.entityview;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.EntityView;
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.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import java.util.UUID;
@Component
@Slf4j
@TbCoreComponent
public class EntityViewEdgeProcessor extends BaseEdgeProcessor {
public class EntityViewEdgeProcessor extends BaseEntityViewProcessor {
public ListenableFuture<Void> processEntityViewMsgFromEdge(TenantId tenantId, Edge edge, EntityViewUpdateMsg entityViewUpdateMsg) {
log.trace("[{}] executing processEntityViewMsgFromEdge [{}] from edge [{}]", tenantId, entityViewUpdateMsg, edge.getName());
EntityViewId entityViewId = new EntityViewId(new UUID(entityViewUpdateMsg.getIdMSB(), entityViewUpdateMsg.getIdLSB()));
try {
edgeSynchronizationManager.getSync().set(true);
switch (entityViewUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE:
saveOrUpdateEntityView(tenantId, entityViewId, entityViewUpdateMsg, edge);
return Futures.immediateFuture(null);
case ENTITY_DELETED_RPC_MESSAGE:
EntityView entityViewToDelete = entityViewService.findEntityViewById(tenantId, entityViewId);
if (entityViewToDelete != null) {
entityViewService.unassignEntityViewFromEdge(tenantId, entityViewId, edge.getId());
}
return Futures.immediateFuture(null);
case UNRECOGNIZED:
default:
return handleUnsupportedMsgType(entityViewUpdateMsg.getMsgType());
}
} catch (DataValidationException e) {
if (e.getMessage().contains("limit reached")) {
log.warn("[{}] Number of allowed entity views violated {}", tenantId, entityViewUpdateMsg, e);
return Futures.immediateFuture(null);
} else {
return Futures.immediateFailedFuture(e);
}
} finally {
edgeSynchronizationManager.getSync().remove();
}
}
private void saveOrUpdateEntityView(TenantId tenantId, EntityViewId entityViewId, EntityViewUpdateMsg entityViewUpdateMsg, Edge edge) {
CustomerId customerId = safeGetCustomerId(entityViewUpdateMsg.getCustomerIdMSB(), entityViewUpdateMsg.getCustomerIdLSB());
Pair<Boolean, Boolean> resultPair = super.saveOrUpdateEntityView(tenantId, entityViewId, entityViewUpdateMsg, customerId);
Boolean created = resultPair.getFirst();
if (created) {
createRelationFromEdge(tenantId, edge.getId(), entityViewId);
pushAssetCreatedEventToRuleEngine(tenantId, edge, entityViewId);
entityViewService.assignEntityViewToEdge(tenantId, entityViewId, edge.getId());
}
Boolean assetNameUpdated = resultPair.getSecond();
if (assetNameUpdated) {
saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ENTITY_VIEW, EdgeEventActionType.UPDATED, entityViewId, null);
}
}
private void pushAssetCreatedEventToRuleEngine(TenantId tenantId, Edge edge, EntityViewId entityViewId) {
try {
EntityView entityView = entityViewService.findEntityViewById(tenantId, entityViewId);
ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(entityView);
TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, entityViewId, entityView.getCustomerId(),
getActionTbMsgMetaData(edge, entityView.getCustomerId()), TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode));
tbClusterService.pushMsgToRuleEngine(tenantId, entityViewId, tbMsg, new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
log.debug("Successfully send ENTITY_CREATED EVENT to rule engine [{}]", entityView);
}
@Override
public void onFailure(Throwable t) {
log.warn("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", entityView, t);
}
});
} catch (JsonProcessingException | IllegalArgumentException e) {
log.warn("[{}] Failed to push entity view action to rule engine: {}", entityViewId, DataConstants.ENTITY_CREATED, e);
}
}
public DownlinkMsg convertEntityViewEventToDownlink(EdgeEvent edgeEvent) {
EntityViewId entityViewId = new EntityViewId(edgeEvent.getEntityId());

1
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java

@ -34,7 +34,6 @@ import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.TimePageLink;

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

@ -756,6 +756,10 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);");
} catch (Exception e) {
}
try {
conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';");
} catch (Exception e) {
}
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005002;");
}

13
application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java

@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
@ -243,7 +244,17 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
private void processForRecipient(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient, NotificationProcessingContext ctx) throws Exception {
if (ctx.getStats().contains(deliveryMethod, recipient.getId())) {
throw new AlreadySentException();
} else {
ctx.getStats().reportProcessed(deliveryMethod, recipient.getId());
}
if (recipient instanceof User) {
UserNotificationSettings settings = notificationSettingsService.getUserNotificationSettings(ctx.getTenantId(), ((User) recipient).getId(), false);
if (!settings.isEnabled(ctx.getNotificationType(), deliveryMethod)) {
throw new RuntimeException("User disabled " + deliveryMethod.getName() + " notifications of this type");
}
}
NotificationChannel notificationChannel = channels.get(deliveryMethod);
DeliveryMethodNotificationTemplate processedTemplate = ctx.getProcessedTemplate(deliveryMethod, recipient);
@ -257,7 +268,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
Notification notification = Notification.builder()
.requestId(request.getId())
.recipientId(recipient.getId())
.type(ctx.getNotificationTemplate().getNotificationType())
.type(ctx.getNotificationType())
.subject(processedTemplate.getSubject())
.text(processedTemplate.getBody())
.additionalConfig(processedTemplate.getAdditionalConfig())

4
application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.settings.NotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient;
@ -48,6 +49,8 @@ public class NotificationProcessingContext {
private final Set<NotificationDeliveryMethod> deliveryMethods;
@Getter
private final NotificationTemplate notificationTemplate;
@Getter
private final NotificationType notificationType;
private final Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> templates;
@Getter
@ -61,6 +64,7 @@ public class NotificationProcessingContext {
this.deliveryMethods = deliveryMethods;
this.settings = settings;
this.notificationTemplate = template;
this.notificationType = template.getNotificationType();
this.templates = new EnumMap<>(NotificationDeliveryMethod.class);
this.stats = new NotificationRequestStats();
init();

12
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -228,7 +228,6 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
save(deviceId, LAST_CONNECT_TIME, ts);
pushRuleEngineMessage(stateData, TbMsgType.CONNECT_EVENT);
checkAndUpdateState(deviceId, stateData);
}
@Override
@ -397,12 +396,17 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
}
void checkAndUpdateState(@Nonnull DeviceId deviceId, @Nonnull DeviceStateData state) {
if (state.getState().isActive()) {
var deviceState = state.getState();
if (deviceState.isActive()) {
updateInactivityStateIfExpired(System.currentTimeMillis(), deviceId, state);
} else {
//trying to fix activity state
if (isActive(System.currentTimeMillis(), state.getState())) {
updateActivityState(deviceId, state, state.getState().getLastActivityTime());
if (isActive(System.currentTimeMillis(), deviceState)) {
updateActivityState(deviceId, state, deviceState.getLastActivityTime());
if (deviceState.getLastInactivityAlarmTime() != 0L && deviceState.getLastInactivityAlarmTime() >= deviceState.getLastActivityTime()) {
deviceState.setLastInactivityAlarmTime(0L);
save(deviceId, INACTIVITY_ALARM_TIME, 0L);
}
}
}
}

120
application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java

@ -15,19 +15,29 @@
*/
package org.thingsboard.server.edge;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.protobuf.AbstractMessage;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@ -154,5 +164,115 @@ public class AssetEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(savedAsset.getUuidId().getLeastSignificantBits(), assetUpdateMsg.getIdLSB());
}
@Test
public void testSendAssetToCloud() throws Exception {
UUID uuid = Uuids.timeBased();
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
AssetUpdateMsg.Builder assetUpdateMsgBuilder = AssetUpdateMsg.newBuilder();
assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
assetUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
assetUpdateMsgBuilder.setName("Asset Edge 2");
assetUpdateMsgBuilder.setType("test");
assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder);
uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
Assert.assertTrue(latestResponseMsg.getSuccess());
Asset asset = doGet("/api/asset/" + uuid, Asset.class);
Assert.assertNotNull(asset);
Assert.assertEquals("Asset Edge 2", asset.getName());
}
@Test
public void testSendAssetToCloudWithNameThatAlreadyExistsOnCloud() throws Exception {
String assetOnCloudName = StringUtils.randomAlphanumeric(15);
Asset assetOnCloud = saveAsset(assetOnCloudName);
UUID uuid = Uuids.timeBased();
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
AssetUpdateMsg.Builder assetUpdateMsgBuilder = AssetUpdateMsg.newBuilder();
assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
assetUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
assetUpdateMsgBuilder.setName(assetOnCloudName);
assetUpdateMsgBuilder.setType("test");
assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder);
uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<AssetUpdateMsg> assetUpdateMsgOpt = edgeImitator.findMessageByType(AssetUpdateMsg.class);
Assert.assertTrue(assetUpdateMsgOpt.isPresent());
AssetUpdateMsg latestAssetUpdateMsg = assetUpdateMsgOpt.get();
Assert.assertNotEquals(assetOnCloudName, latestAssetUpdateMsg.getName());
UUID newAssetId = new UUID(latestAssetUpdateMsg.getIdMSB(), latestAssetUpdateMsg.getIdLSB());
Assert.assertNotEquals(assetOnCloud.getUuidId(), newAssetId);
Asset asset = doGet("/api/asset/" + newAssetId, Asset.class);
Assert.assertNotNull(asset);
Assert.assertNotEquals(assetOnCloudName, asset.getName());
}
@Test
public void testSendDeleteAssetOnEdgeToCloud() throws Exception {
Asset savedAsset = saveAssetOnCloudAndVerifyDeliveryToEdge();
UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder();
AssetUpdateMsg.Builder assetDeleteMsgBuilder = AssetUpdateMsg.newBuilder();
assetDeleteMsgBuilder.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE);
assetDeleteMsgBuilder.setIdMSB(savedAsset.getUuidId().getMostSignificantBits());
assetDeleteMsgBuilder.setIdLSB(savedAsset.getUuidId().getLeastSignificantBits());
testAutoGeneratedCodeByProtobuf(assetDeleteMsgBuilder);
upLinkMsgBuilder.addAssetUpdateMsg(assetDeleteMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(upLinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
AssetInfo assetInfo = doGet("/api/asset/info/" + savedAsset.getUuidId(), AssetInfo.class);
Assert.assertNotNull(assetInfo);
List<AssetInfo> edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/assets?",
new TypeReference<PageData<AssetInfo>>() {
}, new PageLink(100)).getData();
Assert.assertFalse(edgeAssets.contains(assetInfo));
}
private Asset saveAssetOnCloudAndVerifyDeliveryToEdge() throws Exception {
// create asset and assign to edge
Asset savedAsset = saveAsset(StringUtils.randomAlphanumeric(15));
edgeImitator.expectMessageAmount(1); // asset message
doPost("/api/edge/" + edge.getUuidId()
+ "/asset/" + savedAsset.getUuidId(), Device.class);
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<AssetUpdateMsg> assetUpdateMsgOpt = edgeImitator.findMessageByType(AssetUpdateMsg.class);
Assert.assertTrue(assetUpdateMsgOpt.isPresent());
AssetUpdateMsg assetUpdateMsg = assetUpdateMsgOpt.get();
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetUpdateMsg.getMsgType());
Assert.assertEquals(savedAsset.getUuidId().getMostSignificantBits(), assetUpdateMsg.getIdMSB());
Assert.assertEquals(savedAsset.getUuidId().getLeastSignificantBits(), assetUpdateMsg.getIdLSB());
return savedAsset;
}
}

57
application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java

@ -15,17 +15,22 @@
*/
package org.thingsboard.server.edge;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.protobuf.AbstractMessage;
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.DashboardId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -77,4 +82,56 @@ public class AssetProfileEdgeTest extends AbstractEdgeTest {
unAssignFromEdgeAndDeleteRuleChain(buildingsRuleChainId);
}
@Test
public void testSendAssetProfileToCloud() throws Exception {
RuleChainId ruleChainId = createEdgeRuleChainAndAssignToEdge("Asset Profile Rule Chain");
DashboardId dashboardId = createDashboardAndAssignToEdge("Asset Profile Dashboard");
UUID uuid = Uuids.timeBased();
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
AssetProfileUpdateMsg.Builder assetProfileUpdateMsgBuilder = AssetProfileUpdateMsg.newBuilder();
assetProfileUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
assetProfileUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
assetProfileUpdateMsgBuilder.setName("Asset Profile On Edge");
assetProfileUpdateMsgBuilder.setDefault(false);
assetProfileUpdateMsgBuilder.setDefaultRuleChainIdMSB(ruleChainId.getId().getMostSignificantBits());
assetProfileUpdateMsgBuilder.setDefaultRuleChainIdLSB(ruleChainId.getId().getLeastSignificantBits());
assetProfileUpdateMsgBuilder.setDefaultDashboardIdMSB(dashboardId.getId().getMostSignificantBits());
assetProfileUpdateMsgBuilder.setDefaultDashboardIdLSB(dashboardId.getId().getLeastSignificantBits());
assetProfileUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(assetProfileUpdateMsgBuilder);
uplinkMsgBuilder.addAssetProfileUpdateMsg(assetProfileUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
Assert.assertTrue(latestResponseMsg.getSuccess());
AssetProfile assetProfile = doGet("/api/assetProfile/" + uuid, AssetProfile.class);
Assert.assertNotNull(assetProfile);
Assert.assertEquals("Asset Profile On Edge", assetProfile.getName());
// delete profile
edgeImitator.expectMessageAmount(1);
doDelete("/api/assetProfile/" + assetProfile.getUuidId())
.andExpect(status().isOk());
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof AssetProfileUpdateMsg);
AssetProfileUpdateMsg assetProfileUpdateMsg = (AssetProfileUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, assetProfileUpdateMsg.getMsgType());
Assert.assertEquals(assetProfile.getUuidId().getMostSignificantBits(), assetProfileUpdateMsg.getIdMSB());
Assert.assertEquals(assetProfile.getUuidId().getLeastSignificantBits(), assetProfileUpdateMsg.getIdLSB());
// cleanup
unAssignFromEdgeAndDeleteDashboard(dashboardId);
unAssignFromEdgeAndDeleteRuleChain(ruleChainId);
}
}

80
application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.edge;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.protobuf.AbstractMessage;
import org.junit.Assert;
@ -22,13 +23,22 @@ import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
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.edge.Edge;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -149,4 +159,74 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(savedDashboard.getUuidId().getLeastSignificantBits(), dashboardUpdateMsg.getIdLSB());
}
@Test
public void testSendDashboardToCloud() throws Exception {
UUID uuid = Uuids.timeBased();
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
DashboardUpdateMsg.Builder dashboardUpdateMsgBuilder = DashboardUpdateMsg.newBuilder();
dashboardUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
dashboardUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
dashboardUpdateMsgBuilder.setTitle("Edge Test Dashboard");
dashboardUpdateMsgBuilder.setConfiguration("");
dashboardUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(dashboardUpdateMsgBuilder);
uplinkMsgBuilder.addDashboardUpdateMsg(dashboardUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
Dashboard dashboard = doGet("/api/dashboard/" + uuid, Dashboard.class);
Assert.assertNotNull(dashboard);
Assert.assertEquals("Edge Test Dashboard", dashboard.getName());
}
@Test
public void testSendDeleteEntityViewOnEdgeToCloud() throws Exception {
Dashboard savedDashboard = saveDashboardOnCloudAndVerifyDeliveryToEdge();
UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder();
DashboardUpdateMsg.Builder dashboardDeleteMsgBuilder = DashboardUpdateMsg.newBuilder();
dashboardDeleteMsgBuilder.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE);
dashboardDeleteMsgBuilder.setIdMSB(savedDashboard.getUuidId().getMostSignificantBits());
dashboardDeleteMsgBuilder.setIdLSB(savedDashboard.getUuidId().getLeastSignificantBits());
testAutoGeneratedCodeByProtobuf(dashboardDeleteMsgBuilder);
upLinkMsgBuilder.addDashboardUpdateMsg(dashboardDeleteMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(upLinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
DashboardInfo dashboardInfo = doGet("/api/dashboard/info/" + savedDashboard.getUuidId(), DashboardInfo.class);
Assert.assertNotNull(dashboardInfo);
List<DashboardInfo> edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/dashboards?",
new TypeReference<PageData<DashboardInfo>>() {
}, new PageLink(100)).getData();
Assert.assertFalse(edgeAssets.contains(dashboardInfo));
}
private Dashboard saveDashboardOnCloudAndVerifyDeliveryToEdge() throws Exception {
// create dashboard and assign to edge
Dashboard dashboard = new Dashboard();
dashboard.setTitle(StringUtils.randomAlphanumeric(15));
Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
edgeImitator.expectMessageAmount(1); // dashboard message
doPost("/api/edge/" + edge.getUuidId()
+ "/dashboard/" + savedDashboard.getUuidId(), Dashboard.class);
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<DashboardUpdateMsg> dashboardUpdateMsgOpt = edgeImitator.findMessageByType(DashboardUpdateMsg.class);
Assert.assertTrue(dashboardUpdateMsgOpt.isPresent());
DashboardUpdateMsg entityViewUpdateMsg = dashboardUpdateMsgOpt.get();
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType());
Assert.assertEquals(savedDashboard.getUuidId().getMostSignificantBits(), entityViewUpdateMsg.getIdMSB());
Assert.assertEquals(savedDashboard.getUuidId().getLeastSignificantBits(), entityViewUpdateMsg.getIdLSB());
return savedDashboard;
}
}

71
application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java

@ -15,19 +15,26 @@
*/
package org.thingsboard.server.edge;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.protobuf.AbstractMessage;
import com.google.protobuf.ByteString;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.device.data.PowerSavingConfiguration;
import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultCoapDeviceTypeConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration;
import org.thingsboard.server.common.data.device.profile.SnmpDeviceProfileTransportConfiguration;
@ -46,12 +53,15 @@ import org.thingsboard.server.common.data.transport.snmp.config.impl.TelemetryQu
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import org.thingsboard.server.transport.AbstractTransportIntegrationTest;
import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -260,6 +270,67 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest {
removeDeviceProfileAndDoBasicAssert(deviceProfile);
}
@Test
public void testSendDeviceProfileToCloud() throws Exception {
RuleChainId ruleChainId = createEdgeRuleChainAndAssignToEdge("Device Profile Rule Chain");
DashboardId dashboardId = createDashboardAndAssignToEdge("Device Profile Dashboard");
UUID uuid = Uuids.timeBased();
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
DeviceProfileUpdateMsg.Builder deviceProfileUpdateMsgBuilder = DeviceProfileUpdateMsg.newBuilder();
deviceProfileUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
deviceProfileUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
deviceProfileUpdateMsgBuilder.setName("Device Profile On Edge");
deviceProfileUpdateMsgBuilder.setDefault(false);
deviceProfileUpdateMsgBuilder.setType(DeviceProfileType.DEFAULT.name());
deviceProfileUpdateMsgBuilder.setProfileDataBytes(ByteString.copyFrom(dataDecodingEncodingService.encode(createProfileData())));
deviceProfileUpdateMsgBuilder.setDefaultRuleChainIdMSB(ruleChainId.getId().getMostSignificantBits());
deviceProfileUpdateMsgBuilder.setDefaultRuleChainIdLSB(ruleChainId.getId().getLeastSignificantBits());
deviceProfileUpdateMsgBuilder.setDefaultDashboardIdMSB(dashboardId.getId().getMostSignificantBits());
deviceProfileUpdateMsgBuilder.setDefaultDashboardIdLSB(dashboardId.getId().getLeastSignificantBits());
deviceProfileUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(deviceProfileUpdateMsgBuilder);
uplinkMsgBuilder.addDeviceProfileUpdateMsg(deviceProfileUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
Assert.assertTrue(latestResponseMsg.getSuccess());
AssetProfile assetProfile = doGet("/api/deviceProfile/" + uuid, AssetProfile.class);
Assert.assertNotNull(assetProfile);
Assert.assertEquals("Device Profile On Edge", assetProfile.getName());
// delete profile
edgeImitator.expectMessageAmount(1);
doDelete("/api/deviceProfile/" + assetProfile.getUuidId())
.andExpect(status().isOk());
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg);
DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType());
Assert.assertEquals(assetProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB());
Assert.assertEquals(assetProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB());
// cleanup
unAssignFromEdgeAndDeleteDashboard(dashboardId);
unAssignFromEdgeAndDeleteRuleChain(ruleChainId);
}
private DeviceProfileData createProfileData() {
DeviceProfileData deviceProfileData = new DeviceProfileData();
deviceProfileData.setConfiguration(new DefaultDeviceProfileConfiguration());
deviceProfileData.setTransportConfiguration(new DefaultDeviceProfileTransportConfiguration());
deviceProfileData.setProvisionConfiguration(new DisabledDeviceProfileProvisionConfiguration("Device Secret"));
return deviceProfileData;
}
private DeviceProfile createDeviceProfileAndDoBasicAssert(String deviceProfileName, DeviceProfileTransportConfiguration deviceProfileTransportConfiguration) throws Exception {
DeviceProfile deviceProfile = this.createDeviceProfile(deviceProfileName, deviceProfileTransportConfiguration);

154
application/src/test/java/org/thingsboard/server/edge/EntityViewEdgeTest.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.edge;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.protobuf.AbstractMessage;
import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.Assert;
@ -22,15 +24,24 @@ import org.junit.Test;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.EdgeEntityType;
import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg;
import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -43,11 +54,7 @@ public class EntityViewEdgeTest extends AbstractEdgeTest {
// create entity view and assign to edge
edgeImitator.expectMessageAmount(1);
Device device = findDeviceByName("Edge Device 1");
EntityView entityView = new EntityView();
entityView.setName("Edge EntityView 1");
entityView.setType("test");
entityView.setEntityId(device.getId());
EntityView savedEntityView = doPost("/api/entityView", entityView, EntityView.class);
EntityView savedEntityView = saveEntityView("Edge EntityView 1", device.getId());
doPost("/api/edge/" + edge.getUuidId()
+ "/entityView/" + savedEntityView.getUuidId(), EntityView.class);
Assert.assertTrue(edgeImitator.waitForMessages());
@ -102,11 +109,7 @@ public class EntityViewEdgeTest extends AbstractEdgeTest {
// create entity view #2 and assign to edge
edgeImitator.expectMessageAmount(1);
entityView = new EntityView();
entityView.setName("Edge EntityView 2");
entityView.setType("test");
entityView.setEntityId(device.getId());
savedEntityView = doPost("/api/entityView", entityView, EntityView.class);
savedEntityView = saveEntityView("Edge EntityView 2", device.getId());
doPost("/api/edge/" + edge.getUuidId()
+ "/entityView/" + savedEntityView.getUuidId(), EntityView.class);
Assert.assertTrue(edgeImitator.waitForMessages());
@ -158,6 +161,115 @@ public class EntityViewEdgeTest extends AbstractEdgeTest {
}
@Test
public void testSendEntityViewToCloud() throws Exception {
Device device = findDeviceByName("Edge Device 1");
UUID uuid = Uuids.timeBased();
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
EntityViewUpdateMsg.Builder entityViewUpdateMsgBuilder = EntityViewUpdateMsg.newBuilder();
entityViewUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
entityViewUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
entityViewUpdateMsgBuilder.setName("Edge EntityView 2");
entityViewUpdateMsgBuilder.setType("test");
entityViewUpdateMsgBuilder.setEntityType(EdgeEntityType.DEVICE);
entityViewUpdateMsgBuilder.setEntityIdMSB(device.getUuidId().getMostSignificantBits());
entityViewUpdateMsgBuilder.setEntityIdLSB(device.getUuidId().getLeastSignificantBits());
entityViewUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(entityViewUpdateMsgBuilder);
uplinkMsgBuilder.addEntityViewUpdateMsg(entityViewUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
Assert.assertTrue(latestResponseMsg.getSuccess());
EntityView entityView = doGet("/api/entityView/" + uuid, EntityView.class);
Assert.assertNotNull(entityView);
Assert.assertEquals("Edge EntityView 2", entityView.getName());
}
@Test
public void testSendEntityViewToCloudWithNameThatAlreadyExistsOnCloud() throws Exception {
Device device = findDeviceByName("Edge Device 1");
String entityViewOnCloudName = StringUtils.randomAlphanumeric(15);
EntityView entityViewOnCloud = saveEntityView(entityViewOnCloudName, device.getId());
UUID uuid = Uuids.timeBased();
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
EntityViewUpdateMsg.Builder entityViewUpdateMsgBuilder = EntityViewUpdateMsg.newBuilder();
entityViewUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
entityViewUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
entityViewUpdateMsgBuilder.setName(entityViewOnCloudName);
entityViewUpdateMsgBuilder.setType("test");
entityViewUpdateMsgBuilder.setEntityType(EdgeEntityType.DEVICE);
entityViewUpdateMsgBuilder.setEntityIdMSB(device.getUuidId().getMostSignificantBits());
entityViewUpdateMsgBuilder.setEntityIdLSB(device.getUuidId().getLeastSignificantBits());
entityViewUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(entityViewUpdateMsgBuilder);
uplinkMsgBuilder.addEntityViewUpdateMsg(entityViewUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<EntityViewUpdateMsg> entityViewUpdateMsgOpt = edgeImitator.findMessageByType(EntityViewUpdateMsg.class);
Assert.assertTrue(entityViewUpdateMsgOpt.isPresent());
EntityViewUpdateMsg latestEntityViewUpdateMsg = entityViewUpdateMsgOpt.get();
Assert.assertNotEquals(entityViewOnCloudName, latestEntityViewUpdateMsg.getName());
UUID newEntityViewId = new UUID(latestEntityViewUpdateMsg.getIdMSB(), latestEntityViewUpdateMsg.getIdLSB());
Assert.assertNotEquals(entityViewOnCloud.getId().getId(), newEntityViewId);
EntityView entityView = doGet("/api/entityView/" + newEntityViewId, EntityView.class);
Assert.assertNotNull(entityView);
Assert.assertNotEquals(entityViewOnCloudName, entityView.getName());
}
@Test
public void testSendDeleteEntityViewOnEdgeToCloud() throws Exception {
Device device = findDeviceByName("Edge Device 1");
EntityView savedEntityView = saveEntityViewOnCloudAndVerifyDeliveryToEdge(device);
UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder();
EntityViewUpdateMsg.Builder entityViewDeleteMsgBuilder = EntityViewUpdateMsg.newBuilder();
entityViewDeleteMsgBuilder.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE);
entityViewDeleteMsgBuilder.setIdMSB(savedEntityView.getUuidId().getMostSignificantBits());
entityViewDeleteMsgBuilder.setIdLSB(savedEntityView.getUuidId().getLeastSignificantBits());
testAutoGeneratedCodeByProtobuf(entityViewDeleteMsgBuilder);
upLinkMsgBuilder.addEntityViewUpdateMsg(entityViewDeleteMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(upLinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
EntityViewInfo entityViewInfo = doGet("/api/entityView/info/" + savedEntityView.getUuidId(), EntityViewInfo.class);
Assert.assertNotNull(entityViewInfo);
List<EntityViewInfo> edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/entityViews?",
new TypeReference<PageData<EntityViewInfo>>() {
}, new PageLink(100)).getData();
Assert.assertFalse(edgeAssets.contains(entityViewInfo));
}
private void verifyEntityViewUpdateMsg(EntityView entityView, Device device) throws InvalidProtocolBufferException {
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof EntityViewUpdateMsg);
@ -173,6 +285,28 @@ public class EntityViewEdgeTest extends AbstractEdgeTest {
testAutoGeneratedCodeByProtobuf(entityViewUpdateMsg);
}
private EntityView saveEntityViewOnCloudAndVerifyDeliveryToEdge(Device device) throws Exception {
// create entity view and assign to edge
EntityView savedEntityView = saveEntityView(StringUtils.randomAlphanumeric(15), device.getId());
edgeImitator.expectMessageAmount(1); // entity view message
doPost("/api/edge/" + edge.getUuidId()
+ "/entityView/" + savedEntityView.getUuidId(), EntityView.class);
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<EntityViewUpdateMsg> entityViewUpdateMsgOpt = edgeImitator.findMessageByType(EntityViewUpdateMsg.class);
Assert.assertTrue(entityViewUpdateMsgOpt.isPresent());
EntityViewUpdateMsg entityViewUpdateMsg = entityViewUpdateMsgOpt.get();
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType());
Assert.assertEquals(savedEntityView.getUuidId().getMostSignificantBits(), entityViewUpdateMsg.getIdMSB());
Assert.assertEquals(savedEntityView.getUuidId().getLeastSignificantBits(), entityViewUpdateMsg.getIdLSB());
return savedEntityView;
}
private EntityView saveEntityView(String name, DeviceId deviceId) {
EntityView entityView = new EntityView();
entityView.setName(name);
entityView.setType("test");
entityView.setEntityId(deviceId);
return doPost("/api/entityView", entityView, EntityView.class);
}
}

69
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.notification;
import com.google.common.util.concurrent.SettableFuture;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.data.Offset;
import org.java_websocket.client.WebSocketClient;
@ -27,6 +28,7 @@ import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
@ -40,6 +42,7 @@ import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter;
@ -484,6 +487,66 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).hasValue(1);
}
@Test
public void testUserNotificationSettings() throws Exception {
var entityActionNotificationPref = new UserNotificationSettings.NotificationPref();
entityActionNotificationPref.setEnabled(true);
entityActionNotificationPref.setEnabledDeliveryMethods(Map.of(
NotificationDeliveryMethod.WEB, true,
NotificationDeliveryMethod.SMS, false,
NotificationDeliveryMethod.EMAIL, false
));
var entitiesLimitNotificationPref = new UserNotificationSettings.NotificationPref();
entitiesLimitNotificationPref.setEnabled(true);
entitiesLimitNotificationPref.setEnabledDeliveryMethods(Map.of(
NotificationDeliveryMethod.SMS, true,
NotificationDeliveryMethod.WEB, false,
NotificationDeliveryMethod.EMAIL, false
));
var apiUsageLimitNotificationPref = new UserNotificationSettings.NotificationPref();
apiUsageLimitNotificationPref.setEnabled(false);
apiUsageLimitNotificationPref.setEnabledDeliveryMethods(Map.of(
NotificationDeliveryMethod.WEB, true,
NotificationDeliveryMethod.SMS, false,
NotificationDeliveryMethod.EMAIL, false
));
UserNotificationSettings settings = new UserNotificationSettings(Map.of(
NotificationType.ENTITY_ACTION, entityActionNotificationPref,
NotificationType.ENTITIES_LIMIT, entitiesLimitNotificationPref,
NotificationType.API_USAGE_LIMIT, apiUsageLimitNotificationPref
));
doPost("/api/notification/settings/user", settings, UserNotificationSettings.class);
var entityActionNotificationTemplate = createNotificationTemplate(NotificationType.ENTITY_ACTION, "Entity action", "Entity action", NotificationDeliveryMethod.WEB);
var entitiesLimitNotificationTemplate = createNotificationTemplate(NotificationType.ENTITIES_LIMIT, "Entities limit", "Entities limit", NotificationDeliveryMethod.WEB);
var apiUsageLimitNotificationTemplate = createNotificationTemplate(NotificationType.API_USAGE_LIMIT, "API usage limit", "API usage limit", NotificationDeliveryMethod.WEB);
NotificationTarget target = createNotificationTarget(tenantAdminUserId);
NotificationRequest notificationRequest = NotificationRequest.builder()
.tenantId(tenantId)
.templateId(entityActionNotificationTemplate.getId())
.originatorEntityId(tenantAdminUserId)
.targets(List.of(target.getUuidId()))
.ruleId(new NotificationRuleId(UUID.randomUUID())) // to trigger user settings check
.build();
NotificationRequestStats stats = submitNotificationRequestAndWait(notificationRequest);
assertThat(stats.getErrors()).isEmpty();
assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB).get()).isOne();
notificationRequest.setTemplateId(entitiesLimitNotificationTemplate.getId());
stats = submitNotificationRequestAndWait(notificationRequest);
assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).matches(n -> n == null || n.get() == 0);
assertThat(stats.getErrors().get(NotificationDeliveryMethod.WEB).values()).first().asString().contains("disabled");
notificationRequest.setTemplateId(apiUsageLimitNotificationTemplate.getId());
stats = submitNotificationRequestAndWait(notificationRequest);
assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).matches(n -> n == null || n.get() == 0);
assertThat(stats.getErrors().get(NotificationDeliveryMethod.WEB).values()).first().asString().contains("disabled");
}
@Test
public void testSlackNotifications() throws Exception {
NotificationSettings settings = new NotificationSettings();
@ -603,6 +666,12 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
assertThat(message.getPotentialAction().get(0).getTargets().get(0).getUri()).isEqualTo("https://" + expectedParams);
}
private NotificationRequestStats submitNotificationRequestAndWait(NotificationRequest notificationRequest) throws Exception {
SettableFuture<NotificationRequestStats> future = SettableFuture.create();
notificationCenter.processNotificationRequest(notificationRequest.getTenantId(), notificationRequest, future::set);
return future.get(30, TimeUnit.SECONDS);
}
private void checkFullNotificationsUpdate(UnreadNotificationsUpdate notificationsUpdate, String... expectedNotifications) {
assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getText).containsOnly(expectedNotifications);
assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getType).containsOnly(DEFAULT_NOTIFICATION_TYPE);

12
application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java → application/src/test/java/org/thingsboard/server/service/notification/TestNotificationSettingsService.java

@ -19,14 +19,20 @@ import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.notification.DefaultNotificationSettingsService;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.user.UserSettingsService;
@Service
@Primary
public class MockNotificationSettingsService extends DefaultNotificationSettingsService {
public class TestNotificationSettingsService extends DefaultNotificationSettingsService {
public MockNotificationSettingsService(AdminSettingsService adminSettingsService) {
super(adminSettingsService, null, null, null);
public TestNotificationSettingsService(AdminSettingsService adminSettingsService,
NotificationTargetService notificationTargetService,
NotificationTemplateService notificationTemplateService,
UserSettingsService userSettingsService) {
super(adminSettingsService, notificationTargetService, notificationTemplateService, null, userSettingsService);
}
@Override

215
application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java

@ -22,19 +22,30 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.sql.query.EntityQueryRepository;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@ -62,14 +73,32 @@ public class DefaultDeviceStateServiceTest {
PartitionService partitionService;
@Mock
DeviceStateData deviceStateDataMock;
@Mock
EntityQueryRepository entityQueryRepository;
TenantId tenantId = new TenantId(UUID.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112"));
DeviceId deviceId = DeviceId.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112");
TopicPartitionInfo tpi;
DefaultDeviceStateService service;
TelemetrySubscriptionService telemetrySubscriptionService;
@Before
public void setUp() {
service = spy(new DefaultDeviceStateService(deviceService, attributesService, tsService, clusterService, partitionService, null, null, null, mock(NotificationRuleProcessor.class)));
service = spy(new DefaultDeviceStateService(deviceService, attributesService, tsService, clusterService, partitionService, entityQueryRepository, null, null, mock(NotificationRuleProcessor.class)));
telemetrySubscriptionService = Mockito.mock(TelemetrySubscriptionService.class);
ReflectionTestUtils.setField(service, "tsSubService", telemetrySubscriptionService);
ReflectionTestUtils.setField(service, "defaultStateCheckIntervalInSec", 60);
ReflectionTestUtils.setField(service, "defaultActivityStatsIntervalInSec", 60);
ReflectionTestUtils.setField(service, "initFetchPackSize", 10);
tpi = TopicPartitionInfo.builder().myPartition(true).build();
Mockito.when(partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId)).thenReturn(tpi);
Mockito.when(entityQueryRepository.findEntityDataByQueryInternal(Mockito.any())).thenReturn(new PageData<>());
var deviceIdInfo = new DeviceIdInfo(tenantId.getId(), null, deviceId.getId());
Mockito.when(deviceService.findDeviceIdInfos(Mockito.any()))
.thenReturn(new PageData<>(List.of(deviceIdInfo), 0, 1, false));
}
@Test
@ -125,4 +154,188 @@ public class DefaultDeviceStateServiceTest {
Assert.assertEquals(5000L, deviceStateData.getState().getInactivityTimeout());
}
private void initStateService(long timeout) throws InterruptedException {
service.stop();
Mockito.reset(service, telemetrySubscriptionService);
ReflectionTestUtils.setField(service, "defaultInactivityTimeoutMs", timeout);
service.init();
PartitionChangeEvent event = new PartitionChangeEvent(this, new QueueKey(ServiceType.TB_CORE), Collections.singleton(tpi));
service.onApplicationEvent(event);
Thread.sleep(100);
}
@Test
public void increaseInactivityForInactiveDeviceTest() throws Exception {
final long defaultTimeout = 1;
initStateService(defaultTimeout);
DeviceState deviceState = DeviceState.builder().build();
DeviceStateData deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(deviceState)
.metaData(new TbMsgMetaData())
.build();
service.deviceStates.put(deviceId, deviceStateData);
service.getPartitionedEntities(tpi).add(deviceId);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Thread.sleep(defaultTimeout);
service.checkStates();
activityVerify(false);
Mockito.reset(telemetrySubscriptionService);
long increase = 100;
long newTimeout = System.currentTimeMillis() - deviceState.getLastActivityTime() + increase;
service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout);
activityVerify(true);
Thread.sleep(increase);
service.checkStates();
activityVerify(false);
Mockito.reset(telemetrySubscriptionService);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Thread.sleep(newTimeout + 5);
service.checkStates();
activityVerify(false);
}
@Test
public void increaseInactivityForActiveDeviceTest() throws Exception {
final long defaultTimeout = 1000;
initStateService(defaultTimeout);
DeviceState deviceState = DeviceState.builder().build();
DeviceStateData deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(deviceState)
.metaData(new TbMsgMetaData())
.build();
service.deviceStates.put(deviceId, deviceStateData);
service.getPartitionedEntities(tpi).add(deviceId);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Mockito.reset(telemetrySubscriptionService);
long increase = 100;
long newTimeout = System.currentTimeMillis() - deviceState.getLastActivityTime() + increase;
service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout);
Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any());
Thread.sleep(defaultTimeout + increase);
service.checkStates();
activityVerify(false);
Mockito.reset(telemetrySubscriptionService);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Thread.sleep(newTimeout);
service.checkStates();
activityVerify(false);
}
@Test
public void increaseSmallInactivityForInactiveDeviceTest() throws Exception {
final long defaultTimeout = 1;
initStateService(defaultTimeout);
DeviceState deviceState = DeviceState.builder().build();
DeviceStateData deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(deviceState)
.metaData(new TbMsgMetaData())
.build();
service.deviceStates.put(deviceId, deviceStateData);
service.getPartitionedEntities(tpi).add(deviceId);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Thread.sleep(defaultTimeout);
service.checkStates();
activityVerify(false);
Mockito.reset(telemetrySubscriptionService);
long newTimeout = 1;
Thread.sleep(newTimeout);
Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any());
}
@Test
public void decreaseInactivityForActiveDeviceTest() throws Exception {
final long defaultTimeout = 1000;
initStateService(defaultTimeout);
DeviceState deviceState = DeviceState.builder().build();
DeviceStateData deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(deviceState)
.metaData(new TbMsgMetaData())
.build();
service.deviceStates.put(deviceId, deviceStateData);
service.getPartitionedEntities(tpi).add(deviceId);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Mockito.reset(telemetrySubscriptionService);
Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any());
long newTimeout = 1;
service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout);
activityVerify(false);
Mockito.reset(telemetrySubscriptionService);
service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, defaultTimeout);
activityVerify(true);
Thread.sleep(defaultTimeout);
service.checkStates();
activityVerify(false);
}
@Test
public void decreaseInactivityForInactiveDeviceTest() throws Exception {
final long defaultTimeout = 1000;
initStateService(defaultTimeout);
DeviceState deviceState = DeviceState.builder().build();
DeviceStateData deviceStateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.state(deviceState)
.metaData(new TbMsgMetaData())
.build();
service.deviceStates.put(deviceId, deviceStateData);
service.getPartitionedEntities(tpi).add(deviceId);
service.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
activityVerify(true);
Thread.sleep(defaultTimeout);
service.checkStates();
activityVerify(false);
Mockito.reset(telemetrySubscriptionService);
long newTimeout = 1;
service.onDeviceInactivityTimeoutUpdate(tenantId, deviceId, newTimeout);
Mockito.verify(telemetrySubscriptionService, Mockito.never()).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.any(), Mockito.any());
}
private void activityVerify(boolean isActive) {
Mockito.verify(telemetrySubscriptionService, Mockito.times(1)).saveAttrAndNotify(Mockito.any(), Mockito.eq(deviceId), Mockito.any(), Mockito.eq("active"), Mockito.eq(isActive), Mockito.any());
}
}

2
application/src/test/resources/application-test.properties

@ -68,3 +68,5 @@ sql.ttl.audit_logs.ttl=2592000
sql.edge_events.partition_size=168
sql.ttl.edge_events.edge_event_ttl=2592000
server.log_controller_error_stack_trace=false

2
common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java

@ -31,6 +31,8 @@ public interface AssetProfileService extends EntityDaoService {
AssetProfileInfo findAssetProfileInfoById(TenantId tenantId, AssetProfileId assetProfileId);
AssetProfile saveAssetProfile(AssetProfile assetProfile, boolean doValidate);
AssetProfile saveAssetProfile(AssetProfile assetProfile);
void deleteAssetProfile(TenantId tenantId, AssetProfileId assetProfileId);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java

@ -41,6 +41,8 @@ public interface AssetService extends EntityDaoService {
Asset findAssetByTenantIdAndName(TenantId tenantId, String name);
Asset saveAsset(Asset asset, boolean doValidate);
Asset saveAsset(Asset asset);
Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, CustomerId customerId);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java

@ -40,6 +40,8 @@ public interface DashboardService extends EntityDaoService {
ListenableFuture<DashboardInfo> findDashboardInfoByIdAsync(TenantId tenantId, DashboardId dashboardId);
Dashboard saveDashboard(Dashboard dashboard, boolean doValidate);
Dashboard saveDashboard(Dashboard dashboard);
Dashboard assignDashboardToCustomer(TenantId tenantId, DashboardId dashboardId, CustomerId customerId);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java

@ -31,6 +31,8 @@ public interface DeviceProfileService extends EntityDaoService {
DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId);
DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile, boolean doValidate);
DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile);
void deleteDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java

@ -38,6 +38,8 @@ public interface EntityViewService extends EntityDaoService {
EntityView saveEntityView(EntityView entityView);
EntityView saveEntityView(EntityView entityView, boolean doValidate);
EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, CustomerId customerId);
EntityView unassignEntityViewFromCustomer(TenantId tenantId, EntityViewId entityViewId);

6
common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java

@ -16,7 +16,9 @@
package org.thingsboard.server.dao.notification;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
public interface NotificationSettingsService {
@ -24,6 +26,10 @@ public interface NotificationSettingsService {
NotificationSettings findNotificationSettings(TenantId tenantId);
UserNotificationSettings saveUserNotificationSettings(TenantId tenantId, UserId userId, UserNotificationSettings settings);
UserNotificationSettings getUserNotificationSettings(TenantId tenantId, UserId userId, boolean format);
void createDefaultNotificationConfigs(TenantId tenantId);
void updateDefaultNotificationConfigs(TenantId tenantId);

41
common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java

@ -16,11 +16,10 @@
package org.thingsboard.server.common.data.msg;
import lombok.Getter;
import org.thingsboard.server.common.data.StringUtils;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public enum TbMsgType {
@ -39,10 +38,10 @@ public enum TbMsgType {
ENTITY_UNASSIGNED("Entity Unassigned"),
ATTRIBUTES_UPDATED("Attributes Updated"),
ATTRIBUTES_DELETED("Attributes Deleted"),
ALARM(null),
ALARM,
ALARM_ACK("Alarm Acknowledged"),
ALARM_CLEAR("Alarm Cleared"),
ALARM_DELETE(null),
ALARM_DELETE,
ALARM_ASSIGNED("Alarm Assigned"),
ALARM_UNASSIGNED("Alarm Unassigned"),
COMMENT_CREATED("Comment Created"),
@ -50,8 +49,8 @@ public enum TbMsgType {
RPC_CALL_FROM_SERVER_TO_DEVICE("RPC Request to Device"),
ENTITY_ASSIGNED_FROM_TENANT("Entity Assigned From Tenant"),
ENTITY_ASSIGNED_TO_TENANT("Entity Assigned To Tenant"),
ENTITY_ASSIGNED_TO_EDGE(null),
ENTITY_UNASSIGNED_FROM_EDGE(null),
ENTITY_ASSIGNED_TO_EDGE,
ENTITY_UNASSIGNED_FROM_EDGE,
TIMESERIES_UPDATED("Timeseries Updated"),
TIMESERIES_DELETED("Timeseries Deleted"),
RPC_QUEUED("RPC Queued"),
@ -65,9 +64,9 @@ public enum TbMsgType {
RELATION_ADD_OR_UPDATE("Relation Added or Updated"),
RELATION_DELETED("Relation Deleted"),
RELATIONS_DELETED("All Relations Deleted"),
PROVISION_SUCCESS(null),
PROVISION_FAILURE(null),
SEND_EMAIL(null),
PROVISION_SUCCESS,
PROVISION_FAILURE,
SEND_EMAIL,
// tellSelfOnly types
GENERATOR_NODE_SELF_MSG(null, true),
@ -76,12 +75,15 @@ public enum TbMsgType {
DEVICE_UPDATE_SELF_MSG(null, true),
DEDUPLICATION_TIMEOUT_SELF_MSG(null, true),
DELAY_TIMEOUT_SELF_MSG(null, true),
MSG_COUNT_SELF_MSG(null, true);
MSG_COUNT_SELF_MSG(null, true),
// Custom or N/A type:
NA;
public static final List<String> NODE_CONNECTIONS = EnumSet.allOf(TbMsgType.class).stream()
.filter(tbMsgType -> !tbMsgType.isTellSelfOnly())
.map(TbMsgType::getRuleNodeConnection)
.filter(Objects::nonNull)
.filter(connection -> !TbNodeConnectionType.OTHER.equals(connection))
.collect(Collectors.toUnmodifiableList());
@Getter
@ -91,25 +93,16 @@ public enum TbMsgType {
private final boolean tellSelfOnly;
TbMsgType(String ruleNodeConnection, boolean tellSelfOnly) {
this.ruleNodeConnection = ruleNodeConnection;
this.ruleNodeConnection = StringUtils.isNotEmpty(ruleNodeConnection) ? ruleNodeConnection : TbNodeConnectionType.OTHER;
this.tellSelfOnly = tellSelfOnly;
}
TbMsgType(String ruleNodeConnection) {
this.ruleNodeConnection = ruleNodeConnection;
this.tellSelfOnly = false;
this(ruleNodeConnection, false);
}
public static String getRuleNodeConnectionOrElseOther(String msgType) {
if (msgType == null) {
return TbNodeConnectionType.OTHER;
} else {
return Arrays.stream(TbMsgType.values())
.filter(type -> type.name().equals(msgType))
.findFirst()
.map(TbMsgType::getRuleNodeConnection)
.orElse(TbNodeConnectionType.OTHER);
}
TbMsgType() {
this(null, false);
}
}

5
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java

@ -54,7 +54,6 @@ public class NotificationRequestStats {
public void reportSent(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient) {
sent.computeIfAbsent(deliveryMethod, k -> new AtomicInteger()).incrementAndGet();
processedRecipients.computeIfAbsent(deliveryMethod, k -> ConcurrentHashMap.newKeySet()).add(recipient.getId());
}
public void reportError(NotificationDeliveryMethod deliveryMethod, Throwable error, NotificationRecipient recipient) {
@ -68,6 +67,10 @@ public class NotificationRequestStats {
errors.computeIfAbsent(deliveryMethod, k -> new ConcurrentHashMap<>()).put(recipient.getTitle(), errorMessage);
}
public void reportProcessed(NotificationDeliveryMethod deliveryMethod, Object recipientId) {
processedRecipients.computeIfAbsent(deliveryMethod, k -> ConcurrentHashMap.newKeySet()).add(recipientId);
}
public boolean contains(NotificationDeliveryMethod deliveryMethod, Object recipientId) {
Set<Object> processedRecipients = this.processedRecipients.get(deliveryMethod);
return processedRecipients != null && processedRecipients.contains(recipientId);

82
common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/UserNotificationSettings.java

@ -0,0 +1,82 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.notification.settings;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.targets.NotificationTargetType;
import javax.validation.Valid;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotNull;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Data
public class UserNotificationSettings {
@NotNull
@Valid
private final Map<NotificationType, NotificationPref> prefs;
public static final UserNotificationSettings DEFAULT = new UserNotificationSettings(Collections.emptyMap());
public static final Set<NotificationDeliveryMethod> deliveryMethods = NotificationTargetType.PLATFORM_USERS.getSupportedDeliveryMethods();
@JsonCreator
public UserNotificationSettings(@JsonProperty("prefs") Map<NotificationType, NotificationPref> prefs) {
this.prefs = prefs;
}
public boolean isEnabled(NotificationType notificationType, NotificationDeliveryMethod deliveryMethod) {
NotificationPref pref = prefs.get(notificationType);
if (pref == null) {
return true;
}
if (!pref.isEnabled()) {
return false;
}
return pref.getEnabledDeliveryMethods().getOrDefault(deliveryMethod, true);
}
@Data
public static class NotificationPref {
private boolean enabled;
@NotNull
private Map<NotificationDeliveryMethod, Boolean> enabledDeliveryMethods;
public static NotificationPref createDefault() {
NotificationPref pref = new NotificationPref();
pref.setEnabled(true);
pref.setEnabledDeliveryMethods(deliveryMethods.stream().collect(Collectors.toMap(v -> v, v -> true)));
return pref;
}
@JsonIgnore
@AssertTrue(message = "Only email, Web and SMS delivery methods are allowed")
public boolean isValid() {
return enabledDeliveryMethods.entrySet().stream()
.allMatch(entry -> deliveryMethods.contains(entry.getKey()) && entry.getValue() != null);
}
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java

@ -36,4 +36,6 @@ public class SortOrder {
ASC, DESC
}
public static final SortOrder BY_CREATED_TIME_DESC = new SortOrder("createdTime", Direction.DESC);
}

2
common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java

@ -19,7 +19,7 @@ import lombok.Getter;
public enum UserSettingsType {
GENERAL, VISITED_DASHBOARDS(true), QUICK_LINKS, DOC_LINKS, DASHBOARDS, GETTING_STARTED;
GENERAL, VISITED_DASHBOARDS(true), QUICK_LINKS, DOC_LINKS, DASHBOARDS, GETTING_STARTED, NOTIFICATIONS;
@Getter
private final boolean reserved;

18
common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java

@ -22,6 +22,7 @@ import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM;
import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_DELETE;
import static org.thingsboard.server.common.data.msg.TbMsgType.NA;
import static org.thingsboard.server.common.data.msg.TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG;
import static org.thingsboard.server.common.data.msg.TbMsgType.DELAY_TIMEOUT_SELF_MSG;
import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_ASSIGNED_TO_EDGE;
@ -51,37 +52,36 @@ class TbMsgTypeTest {
DEVICE_UPDATE_SELF_MSG,
DEDUPLICATION_TIMEOUT_SELF_MSG,
DELAY_TIMEOUT_SELF_MSG,
MSG_COUNT_SELF_MSG
MSG_COUNT_SELF_MSG,
NA
);
// backward-compatibility tests
@Test
void getRuleNodeConnectionsTest() {
var tbMsgTypes = TbMsgType.values();
for (var type : tbMsgTypes) {
if (typesWithNullRuleNodeConnection.contains(type)) {
assertThat(type.getRuleNodeConnection()).isNull();
assertThat(type.getRuleNodeConnection()).isEqualTo(TbNodeConnectionType.OTHER);
} else {
assertThat(type.getRuleNodeConnection()).isNotNull();
assertThat(type.getRuleNodeConnection()).isNotEqualTo(TbNodeConnectionType.OTHER);
}
}
}
@Test
void getRuleNodeConnectionOrElseOtherTest() {
assertThat(TbMsgType.getRuleNodeConnectionOrElseOther(null))
.isEqualTo(TbNodeConnectionType.OTHER);
var tbMsgTypes = TbMsgType.values();
for (var type : tbMsgTypes) {
if (typesWithNullRuleNodeConnection.contains(type)) {
assertThat(TbMsgType.getRuleNodeConnectionOrElseOther(type.name()))
assertThat(type.getRuleNodeConnection())
.isEqualTo(TbNodeConnectionType.OTHER);
} else {
assertThat(TbMsgType.getRuleNodeConnectionOrElseOther(type.name())).isNotNull()
assertThat(type.getRuleNodeConnection()).isNotNull()
.isNotEqualTo(TbNodeConnectionType.OTHER);
}
}
}
}

10
common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java

@ -221,17 +221,11 @@ public class EdgeGrpcClient implements EdgeRpcClient {
}
@Override
public void sendSyncRequestMsg(boolean syncRequired) {
sendSyncRequestMsg(syncRequired, true);
}
@Override
public void sendSyncRequestMsg(boolean syncRequired, boolean fullSync) {
public void sendSyncRequestMsg(boolean fullSyncRequired) {
uplinkMsgLock.lock();
try {
SyncRequestMsg syncRequestMsg = SyncRequestMsg.newBuilder()
.setSyncRequired(syncRequired)
.setFullSync(fullSync)
.setFullSync(fullSyncRequired)
.build();
this.inputStream.onNext(RequestMsg.newBuilder()
.setMsgType(RequestMsgType.SYNC_REQUEST_RPC_MESSAGE)

4
common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java

@ -34,9 +34,7 @@ public interface EdgeRpcClient {
void disconnect(boolean onError) throws InterruptedException;
void sendSyncRequestMsg(boolean syncRequired);
void sendSyncRequestMsg(boolean syncRequired, boolean fullSync);
void sendSyncRequestMsg(boolean fullSyncRequired);
void sendUplinkMsg(UplinkMsg uplinkMsg);

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

@ -85,7 +85,7 @@ message ConnectResponseMsg {
}
message SyncRequestMsg {
bool syncRequired = 1;
bool syncRequired = 1; // deprecated
optional bool fullSync = 2;
}
@ -529,6 +529,11 @@ message UplinkMsg {
repeated DeviceProfileDevicesRequestMsg deviceProfileDevicesRequestMsg = 13; // deprecated
repeated WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg = 14;
repeated EntityViewsRequestMsg entityViewsRequestMsg = 15;
repeated AssetUpdateMsg assetUpdateMsg = 16;
repeated DashboardUpdateMsg dashboardUpdateMsg = 17;
repeated EntityViewUpdateMsg entityViewUpdateMsg = 18;
repeated AssetProfileUpdateMsg assetProfileUpdateMsg = 19;
repeated DeviceProfileUpdateMsg deviceProfileUpdateMsg = 20;
}
message UplinkResponseMsg {

73
common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java

@ -52,6 +52,7 @@ public final class TbMsg implements Serializable {
private final UUID id;
private final long ts;
private final String type;
private final TbMsgType internalType;
private final EntityId originator;
private final CustomerId customerId;
private final TbMsgMetaData metaData;
@ -97,7 +98,7 @@ public final class TbMsg implements Serializable {
*/
@Deprecated(since = "3.5.2")
public static TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId,
return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, customerId,
metaData.copy(), TbMsgDataType.JSON, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY);
}
@ -108,7 +109,7 @@ public final class TbMsg implements Serializable {
@Deprecated(since = "3.5.2", forRemoval = true)
public static TbMsg newMsg(String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) {
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId,
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, customerId,
metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY);
}
@ -117,7 +118,7 @@ public final class TbMsg implements Serializable {
}
public static TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, customerId,
return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId,
metaData.copy(), TbMsgDataType.JSON, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY);
}
@ -126,7 +127,7 @@ public final class TbMsg implements Serializable {
}
public static TbMsg newMsg(TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) {
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, customerId,
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId,
metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY);
}
@ -170,13 +171,13 @@ public final class TbMsg implements Serializable {
*/
@Deprecated(since = "3.5.2")
public static TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) {
return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId,
return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, customerId,
metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY);
}
@Deprecated(since = "3.5.2", forRemoval = true)
public static TbMsg newMsg(String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data) {
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId,
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, customerId,
metaData.copy(), dataType, data, null, null, null, TbMsgCallback.EMPTY);
}
@ -205,12 +206,12 @@ public final class TbMsg implements Serializable {
}
public static TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) {
return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, customerId,
return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId,
metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY);
}
public static TbMsg newMsg(TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data) {
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, customerId,
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId,
metaData.copy(), dataType, data, null, null, null, TbMsgCallback.EMPTY);
}
@ -222,13 +223,13 @@ public final class TbMsg implements Serializable {
@Deprecated(since = "3.5.2", forRemoval = true)
public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null,
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, null,
metaData.copy(), dataType, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY);
}
@Deprecated(since = "3.5.2", forRemoval = true)
public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data, TbMsgCallback callback) {
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null,
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), null, type, originator, null,
metaData.copy(), TbMsgDataType.JSON, data, null, null, null, callback);
}
@ -250,72 +251,77 @@ public final class TbMsg implements Serializable {
*/
@Deprecated(since = "3.5.2")
public static TbMsg transformMsg(TbMsg tbMsg, String type, EntityId originator, TbMsgMetaData metaData, String data) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType,
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, null, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType,
data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback);
}
public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, null,
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null,
metaData.copy(), dataType, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY);
}
public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data, TbMsgCallback callback) {
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, null,
return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null,
metaData.copy(), TbMsgDataType.JSON, data, null, null, null, callback);
}
public static TbMsg transformMsg(TbMsg tbMsg, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type.name(), originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType,
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType,
data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback);
}
public static TbMsg transformMsgOriginator(TbMsg tbMsg, EntityId originatorId) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, originatorId, tbMsg.getCustomerId(), tbMsg.metaData, tbMsg.dataType,
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, originatorId, tbMsg.getCustomerId(), tbMsg.metaData, tbMsg.dataType,
tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback());
}
public static TbMsg transformMsgData(TbMsg tbMsg, String data) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType,
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType,
data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback());
}
public static TbMsg transformMsgMetadata(TbMsg tbMsg, TbMsgMetaData metadata) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata.copy(), tbMsg.dataType,
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata.copy(), tbMsg.dataType,
tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback());
}
public static TbMsg transformMsg(TbMsg tbMsg, TbMsgMetaData metadata, String data) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata, tbMsg.dataType,
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata, tbMsg.dataType,
data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback());
}
public static TbMsg transformMsgCustomerId(TbMsg tbMsg, CustomerId customerId) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, customerId, tbMsg.metaData, tbMsg.dataType,
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, customerId, tbMsg.metaData, tbMsg.dataType,
tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback());
}
public static TbMsg transformMsgRuleChainId(TbMsg tbMsg, RuleChainId ruleChainId) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType,
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType,
tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback());
}
public static TbMsg transformMsgQueueName(TbMsg tbMsg, String queueName) {
return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType,
return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType,
tbMsg.data, tbMsg.getRuleChainId(), null, tbMsg.ctx.copy(), tbMsg.getCallback());
}
public static TbMsg transformMsg(TbMsg tbMsg, RuleChainId ruleChainId, String queueName) {
return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType,
return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType,
tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback());
}
//used for enqueueForTellNext
public static TbMsg newMsg(TbMsg tbMsg, String queueName, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
return new TbMsg(queueName, UUID.randomUUID(), tbMsg.getTs(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.customerId, tbMsg.getMetaData().copy(),
return new TbMsg(queueName, UUID.randomUUID(), tbMsg.getTs(), tbMsg.getInternalType(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.customerId, tbMsg.getMetaData().copy(),
tbMsg.getDataType(), tbMsg.getData(), ruleChainId, ruleNodeId, tbMsg.ctx.copy(), TbMsgCallback.EMPTY);
}
private TbMsg(String queueName, UUID id, long ts, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data,
private TbMsg(String queueName, UUID id, long ts, TbMsgType internalType, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data,
RuleChainId ruleChainId, RuleNodeId ruleNodeId, TbMsgProcessingCtx ctx, TbMsgCallback callback) {
this(queueName, id, ts, internalType, internalType.name(), originator, customerId, metaData, dataType, data, ruleChainId, ruleNodeId, ctx, callback);
}
private TbMsg(String queueName, UUID id, long ts, TbMsgType internalType, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data,
RuleChainId ruleChainId, RuleNodeId ruleNodeId, TbMsgProcessingCtx ctx, TbMsgCallback callback) {
this.id = id;
this.queueName = queueName;
@ -325,6 +331,7 @@ public final class TbMsg implements Serializable {
this.ts = System.currentTimeMillis();
}
this.type = type;
this.internalType = internalType != null ? internalType : getInternalType(type);
this.originator = originator;
if (customerId == null || customerId.isNullUid()) {
if (originator != null && originator.getEntityType() == EntityType.CUSTOMER) {
@ -410,7 +417,7 @@ public final class TbMsg implements Serializable {
}
TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()];
return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), proto.getType(), entityId, customerId,
return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), null, proto.getType(), entityId, customerId,
metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, ctx, callback);
} catch (InvalidProtocolBufferException e) {
throw new IllegalStateException("Could not parse protobuf for TbMsg", e);
@ -422,17 +429,17 @@ public final class TbMsg implements Serializable {
}
public TbMsg copyWithRuleChainId(RuleChainId ruleChainId, UUID msgId) {
return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.customerId,
return new TbMsg(this.queueName, msgId, this.ts, this.internalType, this.type, this.originator, this.customerId,
this.metaData, this.dataType, this.data, ruleChainId, null, this.ctx, callback);
}
public TbMsg copyWithRuleNodeId(RuleChainId ruleChainId, RuleNodeId ruleNodeId, UUID msgId) {
return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.customerId,
return new TbMsg(this.queueName, msgId, this.ts, this.internalType, this.type, this.originator, this.customerId,
this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx, callback);
}
public TbMsg copyWithNewCtx() {
return new TbMsg(this.queueName, this.id, this.ts, this.type, this.originator, this.customerId,
return new TbMsg(this.queueName, this.id, this.ts, this.internalType, this.type, this.originator, this.customerId,
this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx.copy(), TbMsgCallback.EMPTY);
}
@ -468,8 +475,16 @@ public final class TbMsg implements Serializable {
return ts;
}
private TbMsgType getInternalType(String type) {
try {
return TbMsgType.valueOf(type);
} catch (IllegalArgumentException e) {
return TbMsgType.NA;
}
}
public boolean isTypeOf(TbMsgType tbMsgType) {
return tbMsgType != null && tbMsgType.name().equals(this.type);
return internalType.equals(tbMsgType);
}
public boolean isTypeOneOf(TbMsgType... types) {

16
dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java

@ -111,10 +111,24 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService<AssetPr
return toAssetProfileInfo(findAssetProfileById(tenantId, assetProfileId));
}
@Override
public AssetProfile saveAssetProfile(AssetProfile assetProfile, boolean doValidate) {
return doSaveAssetProfile(assetProfile, doValidate);
}
@Override
public AssetProfile saveAssetProfile(AssetProfile assetProfile) {
return doSaveAssetProfile(assetProfile, true);
}
private AssetProfile doSaveAssetProfile(AssetProfile assetProfile, boolean doValidate) {
log.trace("Executing saveAssetProfile [{}]", assetProfile);
AssetProfile oldAssetProfile = assetProfileValidator.validate(assetProfile, AssetProfile::getTenantId);
AssetProfile oldAssetProfile = null;
if (doValidate) {
oldAssetProfile = assetProfileValidator.validate(assetProfile, AssetProfile::getTenantId);
} else if (assetProfile.getId() != null) {
oldAssetProfile = findAssetProfileById(assetProfile.getTenantId(), assetProfile.getId());
}
AssetProfile savedAssetProfile;
try {
savedAssetProfile = assetProfileDao.saveAndFlush(assetProfile.getTenantId(), assetProfile);

18
dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java

@ -132,12 +132,26 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
.orElse(null), true);
}
@Override
public Asset saveAsset(Asset asset, boolean doValidate) {
return doSaveAsset(asset, doValidate);
}
@Override
public Asset saveAsset(Asset asset) {
return doSaveAsset(asset, true);
}
private Asset doSaveAsset(Asset asset, boolean doValidate) {
log.trace("Executing saveAsset [{}]", asset);
Asset oldAsset = assetValidator.validate(asset, Asset::getTenantId);
Asset savedAsset;
Asset oldAsset = null;
if (doValidate) {
oldAsset = assetValidator.validate(asset, Asset::getTenantId);
} else if (asset.getId() != null) {
oldAsset = findAssetById(asset.getTenantId(), asset.getId());
}
AssetCacheEvictEvent evictEvent = new AssetCacheEvictEvent(asset.getTenantId(), asset.getName(), oldAsset != null ? oldAsset.getName() : null);
Asset savedAsset;
try {
AssetProfile assetProfile;
if (asset.getAssetProfileId() == null) {

13
dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java

@ -137,10 +137,21 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
return dashboardInfoDao.findByIdAsync(tenantId, dashboardId.getId());
}
@Override
public Dashboard saveDashboard(Dashboard dashboard, boolean doValidate) {
return doSaveDashboard(dashboard, doValidate);
}
@Override
public Dashboard saveDashboard(Dashboard dashboard) {
return doSaveDashboard(dashboard, true);
}
private Dashboard doSaveDashboard(Dashboard dashboard, boolean doValidate) {
log.trace("Executing saveDashboard [{}]", dashboard);
dashboardValidator.validate(dashboard, DashboardInfo::getTenantId);
if (doValidate) {
dashboardValidator.validate(dashboard, DashboardInfo::getTenantId);
}
try {
var saved = dashboardDao.save(dashboard.getTenantId(), dashboard);
publishEvictEvent(new DashboardTitleEvictEvent(saved.getId()));

16
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java

@ -142,8 +142,17 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
return toDeviceProfileInfo(findDeviceProfileById(tenantId, deviceProfileId));
}
@Override
public DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile, boolean doValidate) {
return doSaveDeviceProfile(deviceProfile, doValidate);
}
@Override
public DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) {
return doSaveDeviceProfile(deviceProfile, true);
}
private DeviceProfile doSaveDeviceProfile(DeviceProfile deviceProfile, boolean doValidate) {
log.trace("Executing saveDeviceProfile [{}]", deviceProfile);
if (deviceProfile.getProfileData() != null && deviceProfile.getProfileData().getProvisionConfiguration() instanceof X509CertificateChainProvisionConfiguration) {
X509CertificateChainProvisionConfiguration x509Configuration = (X509CertificateChainProvisionConfiguration) deviceProfile.getProfileData().getProvisionConfiguration();
@ -151,7 +160,12 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
formatDeviceProfileCertificate(deviceProfile, x509Configuration);
}
}
DeviceProfile oldDeviceProfile = deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId);
DeviceProfile oldDeviceProfile = null;
if (doValidate) {
oldDeviceProfile = deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId);
} else if (deviceProfile.getId() != null) {
oldDeviceProfile = findDeviceProfileById(deviceProfile.getTenantId(), deviceProfile.getId());
}
DeviceProfile savedDeviceProfile;
try {
savedDeviceProfile = deviceProfileDao.saveAndFlush(deviceProfile.getTenantId(), deviceProfile);

16
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java

@ -101,10 +101,24 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService<EntityVie
cache.evict(keys);
}
@Override
public EntityView saveEntityView(EntityView entityView, boolean doValidate) {
return doSaveEntityView(entityView, doValidate);
}
@Override
public EntityView saveEntityView(EntityView entityView) {
return doSaveEntityView(entityView, true);
}
private EntityView doSaveEntityView(EntityView entityView, boolean doValidate) {
log.trace("Executing save entity view [{}]", entityView);
EntityView old = entityViewValidator.validate(entityView, EntityView::getTenantId);
EntityView old = null;
if (doValidate) {
old = entityViewValidator.validate(entityView, EntityView::getTenantId);
} else if (entityView.getId() != null) {
old = findEntityViewById(entityView.getTenantId(), entityView.getId());
}
try {
EntityView saved = entityViewDao.save(entityView.getTenantId(), entityView);
publishEvictEvent(new EntityViewEvictEvent(saved.getTenantId(), saved.getId(), saved.getEntityId(), old != null ? old.getEntityId() : null, saved.getName(), old != null ? old.getName() : null));

64
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.notification;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@ -25,8 +26,11 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings.NotificationPref;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.AffectedTenantAdministratorsFilter;
import org.thingsboard.server.common.data.notification.targets.platform.AffectedUserFilter;
@ -38,20 +42,28 @@ import org.thingsboard.server.common.data.notification.targets.platform.TenantAd
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.user.UserSettingsService;
import java.util.Collections;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Slf4j
public class DefaultNotificationSettingsService implements NotificationSettingsService {
private final AdminSettingsService adminSettingsService;
private final NotificationTargetService notificationTargetService;
private final NotificationTemplateService notificationTemplateService;
private final DefaultNotifications defaultNotifications;
private final UserSettingsService userSettingsService;
private static final String SETTINGS_KEY = "notifications";
@ -81,6 +93,58 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
});
}
@Override
public UserNotificationSettings saveUserNotificationSettings(TenantId tenantId, UserId userId, UserNotificationSettings settings) {
UserSettings userSettings = new UserSettings();
userSettings.setUserId(userId);
userSettings.setType(UserSettingsType.NOTIFICATIONS);
userSettings.setSettings(JacksonUtil.valueToTree(settings));
userSettingsService.saveUserSettings(tenantId, userSettings);
return formatUserNotificationSettings(settings);
}
@Override
public UserNotificationSettings getUserNotificationSettings(TenantId tenantId, UserId userId, boolean format) {
UserSettings userSettings = userSettingsService.findUserSettings(tenantId, userId, UserSettingsType.NOTIFICATIONS);
UserNotificationSettings settings = null;
if (userSettings != null) {
try {
settings = JacksonUtil.treeToValue(userSettings.getSettings(), UserNotificationSettings.class);
} catch (Exception e) {
log.warn("Failed to parse notification settings for user {}", userId, e);
}
}
if (settings == null) {
settings = UserNotificationSettings.DEFAULT;
}
if (format) {
settings = formatUserNotificationSettings(settings);
}
return settings;
}
private UserNotificationSettings formatUserNotificationSettings(UserNotificationSettings settings) {
Map<NotificationType, NotificationPref> prefs = new EnumMap<>(NotificationType.class);
if (settings != null) {
prefs.putAll(settings.getPrefs());
}
NotificationPref defaultPref = NotificationPref.createDefault();
for (NotificationType notificationType : NotificationType.values()) {
NotificationPref pref = prefs.get(notificationType);
if (pref == null) {
prefs.put(notificationType, defaultPref);
} else {
var enabledDeliveryMethods = new LinkedHashMap<>(pref.getEnabledDeliveryMethods());
// in case a new delivery method was added to the platform
UserNotificationSettings.deliveryMethods.forEach(deliveryMethod -> {
enabledDeliveryMethods.putIfAbsent(deliveryMethod, true);
});
pref.setEnabledDeliveryMethods(enabledDeliveryMethods);
}
}
return new UserNotificationSettings(prefs);
}
@Transactional(propagation = Propagation.NOT_SUPPORTED) // so that parent transaction is not aborted on method failure
@Override
public void createDefaultNotificationConfigs(TenantId tenantId) {

3
dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java

@ -81,6 +81,9 @@ public class JpaNotificationDao extends JpaAbstractDao<NotificationEntity, Notif
return notificationRepository.updateStatusByIdAndRecipientId(notificationId.getId(), recipientId.getId(), status) != 0;
}
/**
* For this hot method, the partial index `idx_notification_recipient_id_unread` was introduced since 3.5.2
* */
@Override
public int countUnreadByRecipientId(TenantId tenantId, UserId recipientId) {
return notificationRepository.countByRecipientIdAndStatusNot(recipientId.getId(), NotificationStatus.READ);

2
dao/src/main/resources/sql/schema-entities-idx.sql

@ -113,3 +113,5 @@ CREATE INDEX IF NOT EXISTS idx_notification_request_status ON notification_reque
CREATE INDEX IF NOT EXISTS idx_notification_id ON notification(id);
CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_created_time ON notification(recipient_id, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java

@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
@ -50,7 +49,7 @@ public class TbMsgTypeSwitchNode implements TbNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
ctx.tellNext(msg, TbMsgType.getRuleNodeConnectionOrElseOther(msg.getType()));
ctx.tellNext(msg, msg.getInternalType().getRuleNodeConnection());
}
}

3
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNodeTest.java

@ -81,9 +81,10 @@ class TbMsgTypeSwitchNodeTest {
var msg = resultMsgs.get(i);
assertThat(msg).isNotNull();
assertThat(msg.getType()).isNotNull();
assertThat(msg.getType()).isEqualTo(msg.getInternalType().name());
assertThat(msg).isSameAs(tbMsgList.get(i));
assertThat(resultNodeConnections.get(i))
.isEqualTo(TbMsgType.getRuleNodeConnectionOrElseOther(msg.getType()));
.isEqualTo(msg.getInternalType().getRuleNodeConnection());
}
}

8
ui-ngx/src/app/core/auth/auth.service.ts

@ -385,10 +385,10 @@ export class AuthService {
} else if (authPayload.authUser) {
authPayload.authUser.authority = Authority.ANONYMOUS;
}
if (authPayload.authUser.isPublic) {
if (authPayload.authUser?.isPublic) {
authPayload.forceFullscreen = true;
}
if (authPayload.authUser.isPublic) {
if (authPayload.authUser?.isPublic) {
this.loadSystemParams().subscribe(
(sysParams) => {
authPayload = {...authPayload, ...sysParams};
@ -399,10 +399,10 @@ export class AuthService {
loadUserSubject.error(err);
}
);
} else if (authPayload.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) {
} else if (authPayload.authUser?.authority === Authority.PRE_VERIFICATION_TOKEN) {
loadUserSubject.next(authPayload);
loadUserSubject.complete();
} else if (authPayload.authUser.userId) {
} else if (authPayload.authUser?.userId) {
this.userService.getUser(authPayload.authUser.userId).subscribe(
(user) => {
authPayload.userDetails = user;

9
ui-ngx/src/app/core/http/notification.service.ts

@ -31,6 +31,7 @@ import {
NotificationTarget,
NotificationTemplate,
NotificationType,
NotificationUserSettings,
SlackChanelType,
SlackConversation
} from '@shared/models/notification.models';
@ -174,4 +175,12 @@ export class NotificationService {
}
return this.http.get<PageData<NotificationTemplate>>(url, defaultHttpOptionsFromConfig(config));
}
public getNotificationUserSettings(config?: RequestConfig): Observable<NotificationUserSettings> {
return this.http.get<NotificationUserSettings>(`/api/notification/settings/user`, defaultHttpOptionsFromConfig(config));
}
public saveNotificationUserSettings(settings: NotificationUserSettings, config?: RequestConfig): Observable<NotificationUserSettings> {
return this.http.post<NotificationUserSettings>('/api/notification/settings/user', settings, defaultHttpOptionsFromConfig(config));
}
}

12
ui-ngx/src/app/core/utils.ts

@ -130,7 +130,7 @@ export function isLiteralObject(value: any) {
return (!!value) && (value.constructor === Object);
}
export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined {
export const formatValue = (value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined => {
if (isDefinedAndNotNull(value) && isNumeric(value) &&
(isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) {
let formatted: string | number = Number(value);
@ -150,6 +150,16 @@ export function formatValue(value: any, dec?: number, units?: string, showZeroDe
}
}
export const formatNumberValue = (value: any, dec?: number): number | undefined => {
if (isDefinedAndNotNull(value) && isNumeric(value)) {
let formatted: string | number = Number(value);
if (isDefinedAndNotNull(dec)) {
formatted = formatted.toFixed(dec);
}
return Number(formatted);
}
}
export function objectValues(obj: any): any[] {
return Object.keys(obj).map(e => obj[e]);
}

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

@ -40,6 +40,15 @@ import {
import {
ValueCardBasicConfigComponent
} from '@home/components/widget/config/basic/cards/value-card-basic-config.component';
import {
AggregatedValueCardBasicConfigComponent
} from '@home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component';
import {
AggregatedDataKeyRowComponent
} from '@home/components/widget/config/basic/cards/aggregated-data-key-row.component';
import {
AggregatedDataKeysPanelComponent
} from '@home/components/widget/config/basic/cards/aggregated-data-keys-panel.component';
@NgModule({
declarations: [
@ -50,6 +59,9 @@ import {
FlotBasicConfigComponent,
AlarmsTableBasicConfigComponent,
ValueCardBasicConfigComponent,
AggregatedValueCardBasicConfigComponent,
AggregatedDataKeyRowComponent,
AggregatedDataKeysPanelComponent,
DataKeyRowComponent,
DataKeysPanelComponent
],
@ -66,6 +78,9 @@ import {
FlotBasicConfigComponent,
AlarmsTableBasicConfigComponent,
ValueCardBasicConfigComponent,
AggregatedValueCardBasicConfigComponent,
AggregatedDataKeyRowComponent,
AggregatedDataKeysPanelComponent,
DataKeyRowComponent,
DataKeysPanelComponent
]
@ -79,5 +94,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-timeseries-table-basic-config': TimeseriesTableBasicConfigComponent,
'tb-flot-basic-config': FlotBasicConfigComponent,
'tb-alarms-table-basic-config': AlarmsTableBasicConfigComponent,
'tb-value-card-basic-config': ValueCardBasicConfigComponent
'tb-value-card-basic-config': ValueCardBasicConfigComponent,
'tb-aggregated-value-card-basic-config': AggregatedValueCardBasicConfigComponent
};

94
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.html

@ -0,0 +1,94 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div [formGroup]="keyRowFormGroup" class="tb-form-table-row tb-aggregated-data-key-row">
<mat-form-field class="tb-inline-field tb-position-field" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="position">
<mat-option *ngFor="let position of aggregatedValueCardKeyPositions" [value]="position">
{{ aggregatedValueCardKeyPositionTranslationMap.get(position) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="tb-inline-field tb-aggregation-field" subscriptSizing="dynamic">
<mat-chip-grid #chipList>
<mat-chip-row class="tb-datakey-chip">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px" class="tb-attribute-chip">
<div class="tb-chip-labels">
<div class="tb-chip-label tb-chip-text">
<ng-container *ngTemplateOutlet="keyName"></ng-container>
</div>
</div>
<button type="button"
(click)="editKey()" mat-icon-button class="tb-mat-24">
<mat-icon class="tb-mat-18">edit</mat-icon>
</button>
</div>
</mat-chip-row>
</mat-chip-grid>
<input matInput
type="text"
fxHide
readonly
[matChipInputFor]="chipList"
/>
</mat-form-field>
<div class="tb-units-field">
<tb-unit-input formControlName="units">
</tb-unit-input>
</div>
<div class="tb-decimals-field">
<mat-form-field appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-font-field">
<tb-font-settings formControlName="font"
[previewText]="valuePreviewFn">
</tb-font-settings>
</div>
<div class="tb-color-field">
<tb-color-settings formControlName="color">
</tb-color-settings>
</div>
<div class="tb-form-table-row-cell-buttons">
<button type="button"
mat-icon-button
(click)="keyRemoved.emit()"
matTooltip="{{ 'widgets.aggregated-value-card.remove-value' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
<ng-template #keyName>
<ng-container *ngIf="dataKeyHasPostprocessing(); else keyName">
<strong><span>f(</span></strong><ng-container *ngTemplateOutlet="keyNameTemplate"></ng-container><strong><span>)</span></strong>
</ng-container>
<ng-template #keyName>
<ng-container *ngTemplateOutlet="keyNameTemplate"></ng-container>
</ng-template>
</ng-template>
<ng-template #keyNameTemplate>
<ng-container>
<strong><span class="tb-agg-func">{{ (modelValue?.aggregationType || aggregationTypes.NONE) }}</span></strong>
<span *ngIf="!modelValue?.aggregationType || modelValue?.aggregationType === aggregationTypes.NONE; else aggValue">({{ 'datakey.latest-value' | translate }})</span>
</ng-container>
<ng-template #aggValue>
<span *ngIf="modelValue?.comparisonEnabled && modelValue?.comparisonResultType !== comparisonResultTypes.PREVIOUS_VALUE">({{ 'datakey.delta' | translate }}:{{ (modelValue?.comparisonResultType === comparisonResultTypes.DELTA_PERCENT ? 'datakey.percent' : 'datakey.absolute') | translate }})</span>
<span *ngIf="modelValue?.comparisonEnabled && modelValue?.comparisonResultType === comparisonResultTypes.PREVIOUS_VALUE">({{ 'datakey.delta-calculation-result-previous-value' | translate }})</span>
</ng-template>
</ng-template>

88
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.scss

@ -0,0 +1,88 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../../../scss/constants';
.tb-aggregated-data-key-row {
.mat-mdc-form-field.tb-inline-field.tb-aggregation-field {
.mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) {
padding-left: 8px;
padding-right: 0;
.mat-mdc-form-field-infix {
padding-top: 0;
padding-bottom: 6px;
.mdc-evolution-chip-set .mdc-evolution-chip {
margin: 0;
}
input.mat-mdc-chip-input {
height: 32px;
margin-left: 0;
}
}
}
.mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip {
.tb-attribute-chip {
.tb-chip-labels {
background: transparent;
}
}
}
}
.tb-position-field {
width: 132px;
min-width: 132px;
}
.tb-aggregation-field {
flex: 1;
min-width: 150px;
}
.tb-units-field, .tb-decimals-field, .tb-font-field, .tb-color-field {
display: flex;
flex-direction: row;
place-content: center;
align-items: center;
}
.tb-units-field {
width: 80px;
min-width: 80px;
}
.tb-decimals-field {
width: 60px;
min-width: 60px;
}
.tb-font-field {
width: 40px;
min-width: 40px;
}
.tb-color-field {
width: 40px;
min-width: 40px;
}
.tb-units-field, .tb-decimals-field {
display: none;
@media #{$mat-gt-sm} {
display: block;
}
}
}

233
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts

@ -0,0 +1,233 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectorRef,
Component,
EventEmitter,
forwardRef,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import {
ComparisonResultType,
DataKey,
DataKeyConfigMode,
DatasourceType, Widget,
widgetType
} from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { AggregationType } from '@shared/models/time/time.models';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { TranslateService } from '@ngx-translate/core';
import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import {
DataKeyConfigDialogComponent,
DataKeyConfigDialogData
} from '@home/components/widget/config/data-key-config-dialog.component';
import { deepClone, formatValue } from '@core/utils';
import {
AggregatedValueCardKeyPosition,
aggregatedValueCardKeyPositionTranslations,
AggregatedValueCardKeySettings
} from '@home/components/widget/lib/cards/aggregated-value-card.models';
@Component({
selector: 'tb-aggregated-data-key-row',
templateUrl: './aggregated-data-key-row.component.html',
styleUrls: ['./aggregated-data-key-row.component.scss', '../../data-keys.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AggregatedDataKeyRowComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class AggregatedDataKeyRowComponent implements ControlValueAccessor, OnInit, OnChanges {
aggregatedValueCardKeyPositions: AggregatedValueCardKeyPosition[] =
Object.keys(AggregatedValueCardKeyPosition).map(value => AggregatedValueCardKeyPosition[value]);
aggregatedValueCardKeyPositionTranslationMap = aggregatedValueCardKeyPositionTranslations;
dataKeyTypes = DataKeyType;
aggregationTypes = AggregationType;
comparisonResultTypes = ComparisonResultType;
@Input()
disabled: boolean;
@Input()
datasourceType: DatasourceType;
@Input()
keyName: string;
@Output()
keyRemoved = new EventEmitter();
keyRowFormGroup: UntypedFormGroup;
modelValue: DataKey;
valuePreviewFn = this._valuePreviewFn.bind(this);
get callbacks(): DataKeysCallbacks {
return this.widgetConfigComponent.widgetConfigCallbacks;
}
get widget(): Widget {
return this.widgetConfigComponent.widget;
}
get isEntityDatasource(): boolean {
return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType);
}
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private dialog: MatDialog,
private cd: ChangeDetectorRef,
public translate: TranslateService,
public truncate: TruncatePipe,
private widgetConfigComponent: WidgetConfigComponent) {
}
ngOnInit() {
this.keyRowFormGroup = this.fb.group({
position: [null, []],
units: [null, []],
decimals: [null, []],
font: [null, []],
color: [null, []]
});
this.keyRowFormGroup.valueChanges.subscribe(
() => this.updateModel()
);
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (['keyName'].includes(propName)) {
if (change.currentValue) {
this.modelValue.name = change.currentValue;
setTimeout(() => {
this.updateModel();
}, 0);
}
}
}
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.keyRowFormGroup.disable({emitEvent: false});
} else {
this.keyRowFormGroup.enable({emitEvent: false});
}
}
writeValue(value: DataKey): void {
this.modelValue = value || {} as DataKey;
const settings: AggregatedValueCardKeySettings = (this.modelValue.settings || {});
this.keyRowFormGroup.patchValue(
{
position: settings.position || AggregatedValueCardKeyPosition.center,
units: value?.units,
decimals: value?.decimals,
font: settings.font,
color: settings.color
}, {emitEvent: false}
);
this.cd.markForCheck();
}
dataKeyHasPostprocessing(): boolean {
return !!this.modelValue?.postFuncBody;
}
editKey() {
this.dialog.open<DataKeyConfigDialogComponent, DataKeyConfigDialogData, DataKey>(DataKeyConfigDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dataKey: deepClone(this.modelValue),
dataKeyConfigMode: DataKeyConfigMode.general,
dataKeySettingsSchema: null,
dataKeySettingsDirective: null,
dashboard: null,
aliasController: null,
widget: this.widget,
widgetType: widgetType.latest,
deviceId: null,
entityAliasId: null,
showPostProcessing: true,
callbacks: this.callbacks,
hideDataKeyName: true,
hideDataKeyLabel: true,
hideDataKeyColor: true
}
}).afterClosed().subscribe((updatedDataKey) => {
if (updatedDataKey) {
this.modelValue = updatedDataKey;
this.keyRowFormGroup.get('units').patchValue(this.modelValue.units, {emitEvent: false});
this.keyRowFormGroup.get('decimals').patchValue(this.modelValue.decimals, {emitEvent: false});
this.updateModel();
}
});
}
private updateModel() {
const value = this.keyRowFormGroup.value;
this.modelValue.settings = this.modelValue.settings || {};
this.modelValue.settings.position = value.position;
this.modelValue.settings.font = value.font;
this.modelValue.settings.color = value.color;
this.modelValue.units = value.units;
this.modelValue.decimals = value.decimals;
this.propagateChange(this.modelValue);
}
private _valuePreviewFn(): string {
const units: string = this.keyRowFormGroup.get('units').value;
const decimals: number = this.keyRowFormGroup.get('decimals').value;
return formatValue(22, decimals, units, true);
}
}

50
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html

@ -0,0 +1,50 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-form-panel tb-aggregated-data-keys-panel">
<div class="tb-form-panel-title">{{ 'widgets.aggregated-value-card.values' | translate }}</div>
<div class="tb-form-table">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell tb-position-header" translate>widgets.aggregated-value-card.position</div>
<div class="tb-form-table-header-cell tb-aggregation-header" translate>widgets.aggregated-value-card.aggregation</div>
<div class="tb-form-table-header-cell tb-units-header" translate>widget-config.units-short</div>
<div class="tb-form-table-header-cell tb-decimals-header" translate>widget-config.decimals-short</div>
<div class="tb-form-table-header-cell tb-font-header" translate>widgets.aggregated-value-card.font</div>
<div class="tb-form-table-header-cell tb-color-header" translate>widgets.aggregated-value-card.color</div>
<div class="tb-form-table-header-cell tb-actions-header"></div>
</div>
<div *ngIf="keysFormArray().controls.length; else noKeys" class="tb-form-table-body">
<div *ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; let $index = index;">
<tb-aggregated-data-key-row fxFlex
[formControl]="keyControl"
[datasourceType]="datasourceType"
[keyName]="keyName"
(keyRemoved)="removeKey($index)">
</tb-aggregated-data-key-row>
</div>
</div>
</div>
<div>
<button type="button" mat-stroked-button color="primary" (click)="addKey()">
{{ 'widgets.aggregated-value-card.add-value' | translate }}
</button>
</div>
</div>
<ng-template #noKeys>
<span fxLayoutAlign="center center"
class="tb-prompt">{{ 'widgets.aggregated-value-card.no-values' | translate }}</span>
</ng-template>

69
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.scss

@ -0,0 +1,69 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../../../scss/constants';
.tb-aggregated-data-keys-panel {
.tb-form-table-header-cell {
&.tb-position-header {
width: 132px;
min-width: 132px;
}
&.tb-aggregation-header {
flex: 1;
min-width: 150px;
}
&.tb-units-header {
width: 80px;
min-width: 80px;
}
&.tb-decimals-header {
width: 60px;
min-width: 60px;
}
&.tb-font-header {
width: 40px;
min-width: 40px;
}
&.tb-color-header {
width: 40px;
min-width: 40px;
}
&.tb-actions-header {
width: 40px;
min-width: 40px;
}
&.tb-units-header, &.tb-decimals-header {
display: none;
@media #{$mat-gt-sm} {
display: block;
}
}
}
.tb-form-table-body {
tb-aggregated-data-key-row {
overflow: hidden;
}
}
}

173
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts

@ -0,0 +1,173 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectorRef,
Component,
forwardRef,
Input,
OnChanges,
OnInit,
SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { aggregatedValueCardDefaultKeySettings } from '@home/components/widget/lib/cards/aggregated-value-card.models';
@Component({
selector: 'tb-aggregated-data-keys-panel',
templateUrl: './aggregated-data-keys-panel.component.html',
styleUrls: ['./aggregated-data-keys-panel.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AggregatedDataKeysPanelComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class AggregatedDataKeysPanelComponent implements ControlValueAccessor, OnInit, OnChanges {
@Input()
disabled: boolean;
@Input()
datasourceType: DatasourceType;
@Input()
keyName: string;
dataKeyType: DataKeyType;
keysListFormGroup: UntypedFormGroup;
get widgetType(): widgetType {
return this.widgetConfigComponent.widgetType;
}
get callbacks(): DataKeysCallbacks {
return this.widgetConfigComponent.widgetConfigCallbacks;
}
get noKeys(): boolean {
const keys: DataKey[] = this.keysListFormGroup.get('keys').value;
return keys.length === 0;
}
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private dialog: MatDialog,
private cd: ChangeDetectorRef,
private utils: UtilsService,
private widgetConfigComponent: WidgetConfigComponent) {
}
ngOnInit() {
this.keysListFormGroup = this.fb.group({
keys: [this.fb.array([]), []]
});
this.keysListFormGroup.valueChanges.subscribe(
(val) => this.propagateChange(this.keysListFormGroup.get('keys').value)
);
this.updateParams();
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (['datasourceType'].includes(propName)) {
this.updateParams();
}
}
}
}
private updateParams() {
if (this.datasourceType === DatasourceType.function) {
this.dataKeyType = DataKeyType.function;
} else {
this.dataKeyType = DataKeyType.timeseries;
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.keysListFormGroup.disable({emitEvent: false});
} else {
this.keysListFormGroup.enable({emitEvent: false});
}
}
writeValue(value: DataKey[] | undefined): void {
this.keysListFormGroup.setControl('keys', this.prepareKeysFormArray(value), {emitEvent: false});
}
keysFormArray(): UntypedFormArray {
return this.keysListFormGroup.get('keys') as UntypedFormArray;
}
trackByKey(index: number, keyControl: AbstractControl): any {
return keyControl;
}
removeKey(index: number) {
(this.keysListFormGroup.get('keys') as UntypedFormArray).removeAt(index);
}
addKey() {
const dataKey = this.callbacks.generateDataKey(this.keyName, this.dataKeyType, null);
dataKey.decimals = 0;
dataKey.settings = {...aggregatedValueCardDefaultKeySettings};
const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray;
const keyControl = this.fb.control(dataKey, []);
keysArray.push(keyControl);
}
private prepareKeysFormArray(keys: DataKey[] | undefined): UntypedFormArray {
const keysControls: Array<AbstractControl> = [];
if (keys) {
keys.forEach((key) => {
keysControls.push(this.fb.control(key, []));
});
}
return this.fb.array(keysControls);
}
}

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

@ -0,0 +1,136 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="aggregatedValueCardWidgetConfigForm">
<tb-timewindow-config-panel formControlName="timewindowConfig">
</tb-timewindow-config-panel>
<tb-datasources
[configMode]="basicMode"
hideDataKeyLabel
hideDataKeyColor
hideDataKeyUnits
hideDataKeyDecimals
hideLatestDataKeys
formControlName="datasources">
</tb-datasources>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle">
{{ 'widget-config.title' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="titleFont"
clearButton
[previewText]="aggregatedValueCardWidgetConfigForm.get('title').value"
[initialPreviewStyle]="widgetConfig.config.titleStyle">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="titleColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.value-card.icon' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
[color]="aggregatedValueCardWidgetConfigForm.get('iconColor').value"
formControlName="icon">
</tb-material-icon-select>
<tb-color-input asBoxInput
colorClearButton
formControlName="iconColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showSubtitle">
{{ 'widgets.aggregated-value-card.subtitle' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="subtitle" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="subtitleFont"
clearButton
[previewText]="aggregatedValueCardWidgetConfigForm.get('subtitle').value">
</tb-font-settings>
<tb-color-settings formControlName="subtitleColor">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showDate">
{{ 'widgets.value-card.date' | translate }}
</mat-slide-toggle>
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-date-format-select fxFlex formControlName="dateFormat"></tb-date-format-select>
<tb-font-settings formControlName="dateFont"
[previewText]="datePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="dateColor">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showChart">
{{ 'widgets.aggregated-value-card.chart' | translate }}
</mat-slide-toggle>
<tb-color-settings formControlName="chartColor">
</tb-color-settings>
</div>
</div>
<tb-aggregated-data-keys-panel
[datasourceType]="datasource?.type"
[keyName]="keyName"
formControlName="values">
</tb-aggregated-data-keys-panel>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
<div class="tb-form-row space-between column-lt-md">
<div translate>widget-config.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons">
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-border-radius' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

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

@ -0,0 +1,293 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectorRef, Component, Injector } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { DataKey, Datasource, WidgetConfig, } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import {
getTimewindowConfig,
setTimewindowConfig
} from '@home/components/widget/config/timewindow-config-panel.component';
import { isUndefined } from '@core/utils';
import {
cssSizeToStrSize,
DateFormatProcessor,
DateFormatSettings, getDataKey,
resolveCssSize
} from '@shared/models/widget-settings.models';
import {
aggregatedValueCardDefaultSettings,
AggregatedValueCardWidgetSettings,
createDefaultAggregatedValueLatestDataKeys
} from '@home/components/widget/lib/cards/aggregated-value-card.models';
import {
AggregationType,
HistoryWindowType,
HOUR,
QuickTimeInterval,
TimewindowType
} from '@shared/models/time/time.models';
@Component({
selector: 'tb-aggregated-value-card-basic-config',
templateUrl: './aggregated-value-card-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class AggregatedValueCardBasicConfigComponent extends BasicWidgetConfigComponent {
public get datasource(): Datasource {
const datasources: Datasource[] = this.aggregatedValueCardWidgetConfigForm.get('datasources').value;
if (datasources && datasources.length) {
return datasources[0];
} else {
return null;
}
}
public get keyName(): string {
const dataKey = getDataKey(this.aggregatedValueCardWidgetConfigForm.get('datasources').value);
if (dataKey) {
return dataKey.name;
} else {
return null;
}
}
aggregatedValueCardWidgetConfigForm: UntypedFormGroup;
datePreviewFn = this._datePreviewFn.bind(this);
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private cd: ChangeDetectorRef,
private $injector: Injector,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.aggregatedValueCardWidgetConfigForm;
}
protected setupDefaults(configData: WidgetConfigComponentData) {
this.setupDefaultDatasource(configData, [
{ name: 'watermeter', label: 'Watermeter', type: DataKeyType.timeseries }
],
createDefaultAggregatedValueLatestDataKeys('watermeter', 'm³')
);
configData.config.useDashboardTimewindow = false;
configData.config.displayTimewindow = true;
configData.config.timewindow = {
selectedTab: TimewindowType.HISTORY,
history: {
historyType: HistoryWindowType.INTERVAL,
quickInterval: QuickTimeInterval.CURRENT_MONTH_SO_FAR,
},
aggregation: {
type: AggregationType.AVG,
interval: 12 * HOUR,
limit: 5000
}
};
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: AggregatedValueCardWidgetSettings = {...aggregatedValueCardDefaultSettings, ...(configData.config.settings || {})};
const dataKey = getDataKey(configData.config.datasources);
const keyName = dataKey?.name;
const iconSize = resolveCssSize(configData.config.iconSize);
this.aggregatedValueCardWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
showTitle: [configData.config.showTitle, []],
title: [configData.config.title, []],
titleFont: [configData.config.titleFont, []],
titleColor: [configData.config.titleColor, []],
showIcon: [configData.config.showTitleIcon, []],
iconSize: [iconSize[0], [Validators.min(0)]],
iconSizeUnit: [iconSize[1], []],
icon: [configData.config.titleIcon, []],
iconColor: [configData.config.iconColor, []],
showSubtitle: [settings.showSubtitle, []],
subtitle: [settings.subtitle, []],
subtitleFont: [settings.subtitleFont, []],
subtitleColor: [settings.subtitleColor, []],
showDate: [settings.showDate, []],
dateFormat: [settings.dateFormat, []],
dateFont: [settings.dateFont, []],
dateColor: [settings.dateColor, []],
showChart: [settings.showChart, []],
chartColor: [settings.chartColor, []],
values: [this.getValues(configData.config.datasources, keyName), []],
background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.datasources = config.datasources;
this.widgetConfig.config.showTitle = config.showTitle;
this.widgetConfig.config.title = config.title;
this.widgetConfig.config.titleFont = config.titleFont;
this.widgetConfig.config.titleColor = config.titleColor;
this.widgetConfig.config.showTitleIcon = config.showIcon;
this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit);
this.widgetConfig.config.titleIcon = config.icon;
this.widgetConfig.config.iconColor = config.iconColor;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.showSubtitle = config.showSubtitle;
this.widgetConfig.config.settings.subtitle = config.subtitle;
this.widgetConfig.config.settings.subtitleFont = config.subtitleFont;
this.widgetConfig.config.settings.subtitleColor = config.subtitleColor;
this.widgetConfig.config.settings.showDate = config.showDate;
this.widgetConfig.config.settings.dateFormat = config.dateFormat;
this.widgetConfig.config.settings.dateFont = config.dateFont;
this.widgetConfig.config.settings.dateColor = config.dateColor;
this.widgetConfig.config.settings.showChart = config.showChart;
this.widgetConfig.config.settings.chartColor = config.chartColor;
this.setValues(config.values, this.widgetConfig.config.datasources);
this.widgetConfig.config.settings.background = config.background;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
protected validatorTriggers(): string[] {
return ['showTitle', 'showIcon', 'showSubtitle', 'showDate', 'showChart'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showTitle: boolean = this.aggregatedValueCardWidgetConfigForm.get('showTitle').value;
const showIcon: boolean = this.aggregatedValueCardWidgetConfigForm.get('showIcon').value;
const showSubtitle: boolean = this.aggregatedValueCardWidgetConfigForm.get('showSubtitle').value;
const showDate: boolean = this.aggregatedValueCardWidgetConfigForm.get('showDate').value;
const showChart: boolean = this.aggregatedValueCardWidgetConfigForm.get('showChart').value;
if (showTitle) {
this.aggregatedValueCardWidgetConfigForm.get('title').enable();
this.aggregatedValueCardWidgetConfigForm.get('titleFont').enable();
this.aggregatedValueCardWidgetConfigForm.get('titleColor').enable();
this.aggregatedValueCardWidgetConfigForm.get('showIcon').enable({emitEvent: false});
if (showIcon) {
this.aggregatedValueCardWidgetConfigForm.get('iconSize').enable();
this.aggregatedValueCardWidgetConfigForm.get('iconSizeUnit').enable();
this.aggregatedValueCardWidgetConfigForm.get('icon').enable();
this.aggregatedValueCardWidgetConfigForm.get('iconColor').enable();
} else {
this.aggregatedValueCardWidgetConfigForm.get('iconSize').disable();
this.aggregatedValueCardWidgetConfigForm.get('iconSizeUnit').disable();
this.aggregatedValueCardWidgetConfigForm.get('icon').disable();
this.aggregatedValueCardWidgetConfigForm.get('iconColor').disable();
}
} else {
this.aggregatedValueCardWidgetConfigForm.get('title').disable();
this.aggregatedValueCardWidgetConfigForm.get('titleFont').disable();
this.aggregatedValueCardWidgetConfigForm.get('titleColor').disable();
this.aggregatedValueCardWidgetConfigForm.get('showIcon').disable({emitEvent: false});
this.aggregatedValueCardWidgetConfigForm.get('iconSize').disable();
this.aggregatedValueCardWidgetConfigForm.get('iconSizeUnit').disable();
this.aggregatedValueCardWidgetConfigForm.get('icon').disable();
this.aggregatedValueCardWidgetConfigForm.get('iconColor').disable();
}
if (showSubtitle) {
this.aggregatedValueCardWidgetConfigForm.get('subtitle').enable();
this.aggregatedValueCardWidgetConfigForm.get('subtitleFont').enable();
this.aggregatedValueCardWidgetConfigForm.get('subtitleColor').enable();
} else {
this.aggregatedValueCardWidgetConfigForm.get('subtitle').disable();
this.aggregatedValueCardWidgetConfigForm.get('subtitleFont').disable();
this.aggregatedValueCardWidgetConfigForm.get('subtitleColor').disable();
}
if (showDate) {
this.aggregatedValueCardWidgetConfigForm.get('dateFormat').enable();
this.aggregatedValueCardWidgetConfigForm.get('dateFont').enable();
this.aggregatedValueCardWidgetConfigForm.get('dateColor').enable();
} else {
this.aggregatedValueCardWidgetConfigForm.get('dateFormat').disable();
this.aggregatedValueCardWidgetConfigForm.get('dateFont').disable();
this.aggregatedValueCardWidgetConfigForm.get('dateColor').disable();
}
if (showChart) {
this.aggregatedValueCardWidgetConfigForm.get('chartColor').enable();
} else {
this.aggregatedValueCardWidgetConfigForm.get('chartColor').disable();
}
}
private getValues(datasources: Datasource[], keyName: string): DataKey[] {
if (datasources && datasources.length) {
return (datasources[0].latestDataKeys || []).filter(k => k.name === keyName);
}
return [];
}
private setValues(values: DataKey[], datasources?: Datasource[]) {
if (datasources && datasources.length) {
datasources[0].latestDataKeys = values;
}
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.enableFullscreen = buttons.includes('fullscreen');
}
private _datePreviewFn(): string {
const dateFormat: DateFormatSettings = this.aggregatedValueCardWidgetConfigForm.get('dateFormat').value;
const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat);
processor.update(Date.now());
return processor.formatted;
}
}

2
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div [formGroup]="keyRowFormGroup" class="tb-form-table-row tb-data-key-row no-padding-right">
<div [formGroup]="keyRowFormGroup" class="tb-form-table-row tb-data-key-row">
<mat-form-field *ngIf="hasAdditionalLatestDataKeys" class="tb-inline-field tb-source-field" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="latest">
<mat-option [value]="false">{{ 'datakey.timeseries' | translate }}</mat-option>

2
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html

@ -32,7 +32,7 @@
[cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="keyDrop($event)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled"
class="tb-form-table-row tb-draggable no-padding-right"
class="tb-draggable-form-table-row"
*ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; let $index = index;">
<tb-data-key-row fxFlex
[formControl]="keyControl"

1
ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html

@ -45,6 +45,7 @@
[widgetType]="data.widgetType"
[showPostProcessing]="data.showPostProcessing"
[callbacks]="data.callbacks"
[hideDataKeyName]="data.hideDataKeyName"
[hideDataKeyLabel]="data.hideDataKeyLabel"
[hideDataKeyColor]="data.hideDataKeyColor"
[hideDataKeyUnits]="data.hideDataKeyUnits"

9
ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts

@ -50,10 +50,11 @@ export interface DataKeyConfigDialogData {
entityAliasId?: string;
showPostProcessing?: boolean;
callbacks?: DataKeysCallbacks;
hideDataKeyLabel: boolean;
hideDataKeyColor: boolean;
hideDataKeyUnits: boolean;
hideDataKeyDecimals: boolean;
hideDataKeyName?: boolean;
hideDataKeyLabel?: boolean;
hideDataKeyColor?: boolean;
hideDataKeyUnits?: boolean;
hideDataKeyDecimals?: boolean;
}
@Component({

2
ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html

@ -20,7 +20,7 @@
<div class="mat-padding" fxLayout="column" style="gap: 16px;">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>datakey.general</div>
<mat-form-field *ngIf="modelValue.type !== dataKeyTypes.function" subscriptSizing="dynamic">
<mat-form-field *ngIf="modelValue.type !== dataKeyTypes.function && !hideDataKeyName" subscriptSizing="dynamic">
<mat-label>{{ 'entity.key' | translate }}</mat-label>
<input matInput type="text" placeholder="{{ 'entity.key-name' | translate }}"
#keyInput

13
ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts

@ -126,6 +126,10 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
@Input()
showPostProcessing = true;
@Input()
@coerceBoolean()
hideDataKeyName = false;
@Input()
@coerceBoolean()
hideDataKeyLabel = false;
@ -144,8 +148,8 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
@ViewChild('keyInput') keyInput: ElementRef;
@ViewChild('funcBodyEdit') funcBodyEdit: JsFuncComponent;
@ViewChild('postFuncBodyEdit') postFuncBodyEdit: JsFuncComponent;
@ViewChild('funcBodyEdit', {static: false}) funcBodyEdit: JsFuncComponent;
@ViewChild('postFuncBodyEdit', {static: false}) postFuncBodyEdit: JsFuncComponent;
hasAdvanced = false;
@ -438,10 +442,11 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
}
public validateOnSubmit() {
if (this.modelValue.type === DataKeyType.function) {
if (this.modelValue.type === DataKeyType.function && this.funcBodyEdit) {
this.funcBodyEdit.validateOnSubmit();
} else if ((this.modelValue.type === DataKeyType.timeseries ||
this.modelValue.type === DataKeyType.attribute) && this.dataKeyFormGroup.get('usePostProcessing').value) {
this.modelValue.type === DataKeyType.attribute) && this.dataKeyFormGroup.get('usePostProcessing').value &&
this.postFuncBodyEdit) {
this.postFuncBodyEdit.validateOnSubmit();
}
}

6
ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts

@ -88,7 +88,7 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
public get hasAdditionalLatestDataKeys(): boolean {
return this.widgetConfigComponent.widgetType === widgetType.timeseries &&
this.widgetConfigComponent.modelValue?.typeParameters?.hasAdditionalLatestDataKeys;
this.widgetConfigComponent.modelValue?.typeParameters?.hasAdditionalLatestDataKeys && !this.hideLatestDataKeys;
}
public get dataKeysOptional(): boolean {
@ -143,6 +143,10 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
return this.datasourcesComponent?.hideDataKeys;
}
public get hideLatestDataKeys(): boolean {
return this.datasourcesComponent?.hideLatestDataKeys;
}
@Input()
disabled: boolean;

4
ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts

@ -113,6 +113,10 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
@coerceBoolean()
hideDataKeys = false;
@Input()
@coerceBoolean()
hideLatestDataKeys = false;
@Input()
@coerceBoolean()
forceSingleDatasource = false;

5
ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.html

@ -65,6 +65,11 @@
asBoxInput>
</tb-color-input>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="displayTypePrefix">
{{ 'timewindow.displayTypePrefix' | translate }}
</mat-slide-toggle>
</div>
<mat-divider></mat-divider>
<div class="tb-form-row no-border no-padding timewindow-preview">
<div class="fixed-title-width" translate>timewindow.preview</div>

3
ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts

@ -63,7 +63,8 @@ export class TimewindowStylePanelComponent extends PageComponent implements OnIn
icon: [computedTimewindowStyle.icon, []],
iconPosition: [computedTimewindowStyle.iconPosition, []],
font: [computedTimewindowStyle.font, []],
color: [computedTimewindowStyle.color, []]
color: [computedTimewindowStyle.color, []],
displayTypePrefix: [computedTimewindowStyle.displayTypePrefix, []]
}
);
this.updatePreviewStyle(this.timewindowStyle);

59
ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts

@ -119,7 +119,7 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
return this.configForm().valid;
}
protected setupDefaultDatasource(configData: WidgetConfigComponentData, keys?: DataKey[]) {
protected setupDefaultDatasource(configData: WidgetConfigComponentData, keys?: DataKey[], latestKeys?: DataKey[]) {
let datasources = configData.config.datasources;
if (!datasources || !datasources.length) {
datasources = [
@ -135,23 +135,58 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
dataKeys = [];
datasources[0].dataKeys = dataKeys;
}
let latestDataKeys = datasources[0].latestDataKeys;
if (!latestDataKeys) {
latestDataKeys = [];
datasources[0].latestDataKeys = latestDataKeys;
}
if (keys && keys.length) {
dataKeys.length = 0;
keys.forEach(key => {
const dataKey =
this.widgetConfigComponent.widgetConfigCallbacks.generateDataKey(key.name, key.type, configData.dataKeySettingsSchema);
if (key.label) {
dataKey.label = key.label;
}
if (key.units) {
dataKey.units = key.units;
}
if (isDefinedAndNotNull(key.decimals)) {
dataKey.decimals = key.decimals;
}
const dataKey = this.constructDataKey(configData, key);
dataKeys.push(dataKey);
});
}
if (latestKeys && latestKeys.length) {
latestDataKeys.length = 0;
latestKeys.forEach(key => {
const dataKey = this.constructDataKey(configData, key);
latestDataKeys.push(dataKey);
});
}
}
protected constructDataKey(configData: WidgetConfigComponentData, key: DataKey): DataKey {
const dataKey =
this.widgetConfigComponent.widgetConfigCallbacks.generateDataKey(key.name, key.type, configData.dataKeySettingsSchema);
if (key.label) {
dataKey.label = key.label;
}
if (key.units) {
dataKey.units = key.units;
}
if (isDefinedAndNotNull(key.decimals)) {
dataKey.decimals = key.decimals;
}
if (isDefinedAndNotNull(key.settings)) {
dataKey.settings = key.settings;
}
if (isDefinedAndNotNull(key.aggregationType)) {
dataKey.aggregationType = key.aggregationType;
}
if (isDefinedAndNotNull(key.comparisonEnabled)) {
dataKey.comparisonEnabled = key.comparisonEnabled;
}
if (isDefinedAndNotNull(key.timeForComparison)) {
dataKey.timeForComparison = key.timeForComparison;
}
if (isDefinedAndNotNull(key.comparisonCustomIntervalValue)) {
dataKey.comparisonCustomIntervalValue = key.comparisonCustomIntervalValue;
}
if (isDefinedAndNotNull(key.comparisonResultType)) {
dataKey.comparisonResultType = key.comparisonResultType;
}
return dataKey;
}
protected abstract configForm(): UntypedFormGroup;

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

@ -0,0 +1,90 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-aggregated-value-card-panel" [style]="backgroundStyle">
<div class="tb-aggregated-value-card-overlay" [style]="overlayStyle"></div>
<div class="tb-aggregated-value-card-title-panel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<ng-container *ngTemplateOutlet="subtitleTpl"></ng-container>
</div>
<ng-container *ngTemplateOutlet="valuesTpl"></ng-container>
<ng-container *ngTemplateOutlet="chartTpl"></ng-container>
<ng-container *ngTemplateOutlet="dateTpl"></ng-container>
</div>
<ng-template #subtitleTpl>
<div *ngIf="showSubtitle" class="tb-aggregated-value-card-subtitle"
[style]="subtitleStyle" [style.color]="subtitleColor.color">{{ subtitle$ | async }}</div>
</ng-template>
<ng-template #valuesTpl>
<div *ngIf="showValues" class="tb-aggregated-value-card-values">
<div class="tb-aggregated-value-card-values-container">
<div class="tb-aggregated-value-card-values-section left">
<ng-container *ngIf="values[aggregatedValueCardKeyPosition.leftTop]">
<ng-container *ngTemplateOutlet="valueTpl; context:{value: values[aggregatedValueCardKeyPosition.leftTop]}"></ng-container>
</ng-container>
<ng-container *ngIf="values[aggregatedValueCardKeyPosition.leftBottom]">
<ng-container *ngTemplateOutlet="valueTpl; context:{value: values[aggregatedValueCardKeyPosition.leftBottom]}"></ng-container>
</ng-container>
</div>
<div class="tb-aggregated-value-card-values-section center">
<ng-container *ngIf="values[aggregatedValueCardKeyPosition.center]">
<ng-container *ngTemplateOutlet="valueTpl; context:{value: values[aggregatedValueCardKeyPosition.center]}"></ng-container>
</ng-container>
</div>
<div class="tb-aggregated-value-card-values-section right">
<ng-container *ngIf="values[aggregatedValueCardKeyPosition.rightTop]">
<ng-container *ngTemplateOutlet="valueTpl; context:{value: values[aggregatedValueCardKeyPosition.rightTop]}"></ng-container>
</ng-container>
<ng-container *ngIf="values[aggregatedValueCardKeyPosition.rightBottom]">
<ng-container *ngTemplateOutlet="valueTpl; context:{value: values[aggregatedValueCardKeyPosition.rightBottom]}"></ng-container>
</ng-container>
</div>
</div>
</div>
</ng-template>
<ng-template #chartTpl>
<div *ngIf="showChart" class="tb-aggregated-value-card-chart">
<div class="tb-aggregated-value-card-chart-ticks">
<div>{{tickMax$ | async}}</div>
<div>{{tickMin$ | async}}</div>
</div>
<div class="tb-aggregated-value-card-chart-container">
<div class="tb-aggregated-value-card-chart-element" #chartElement></div>
<div class="tb-aggregated-value-card-chart-boundary top left"></div>
<div class="tb-aggregated-value-card-chart-boundary top right"></div>
<div class="tb-aggregated-value-card-chart-boundary bottom left"></div>
<div class="tb-aggregated-value-card-chart-boundary bottom right"></div>
</div>
</div>
</ng-template>
<ng-template #dateTpl>
<div *ngIf="showDate" [style]="dateStyle" [style.color]="dateColor.color" [innerHTML]="dateFormat.formatted"></div>
</ng-template>
<ng-template #valueTpl let-value="value">
<div class="tb-aggregated-value-card-value" [style]="value.style" [style.color]="value.color.color">
<div *ngIf="value.showArrow" class="value-arrow-container">
<span *ngIf="!value.upArrow && !value.downArrow" class="value-arrow"></span>
<tb-icon *ngIf="value.upArrow" class="value-arrow">arrow_upward</tb-icon>
<tb-icon *ngIf="value.downArrow" class="value-arrow">arrow_downward</tb-icon>
</div>
<div class="value-text">
<span>{{ value.value }}</span>
<span class="units" [class]="{'small': value.center}">{{ value.units }}</span>
</div>
</div>
</ng-template>

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

@ -0,0 +1,156 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-aggregated-value-card-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 8px;
padding: 20px 24px 24px;
> div:not(.tb-value-card-overlay) {
z-index: 1;
}
.tb-aggregated-value-card-overlay {
position: absolute;
top: 12px;
left: 12px;
bottom: 12px;
right: 12px;
}
> div.tb-aggregated-value-card-title-panel {
display: flex;
flex-direction: column;
.tb-aggregated-value-card-subtitle {
margin-left: 28px;
}
}
.tb-aggregated-value-card-values, .tb-aggregated-value-card-chart {
flex: 1;
min-height: 0;
overflow: hidden;
}
.tb-aggregated-value-card-values-container {
width: 100%;
height: 100%;
padding: 8px 0;
display: grid;
grid-template-columns: minmax(0, 1fr) fit-content(100%) minmax(0, 1fr);
.tb-aggregated-value-card-values-section {
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
&.left {
align-items: flex-start;
}
&.center {
align-items: center;
}
&.right {
align-items: flex-end;
}
}
}
.tb-aggregated-value-card-chart {
display: flex;
gap: 8px;
flex-direction: row;
.tb-aggregated-value-card-chart-ticks {
height: 100%;
display: flex;
flex-direction: column;
place-content: flex-end space-between;
align-items: flex-end;
font-size: 11px;
line-height: 16px;
font-weight: 400;
color: rgba(0, 0, 0, 0.38);
}
.tb-aggregated-value-card-chart-container {
position: relative;
flex: 1;
margin-top: 8px;
margin-bottom: 8px;
.tb-aggregated-value-card-chart-element {
width: 100%;
height: 100%;
}
.tb-aggregated-value-card-chart-boundary {
position: absolute;
width: 6px;
height: 6px;
&.top {
top: 0;
border-top: 2px solid rgba(0,0,0,0.38);
}
&.left {
left: 0;
border-left: 2px solid rgba(0,0,0,0.38);
}
&.right {
right: 0;
border-right: 2px solid rgba(0,0,0,0.38);
}
&.bottom {
bottom: 0;
border-bottom: 2px solid rgba(0,0,0,0.38);
}
}
}
}
.tb-aggregated-value-card-value {
white-space: nowrap;
min-height: 0;
display: flex;
flex-direction: row;
place-content: center;
align-items: center;
.value-arrow-container {
display: flex;
}
.value-text {
line-height: 1;
}
.value-arrow {
font-size: 1.1em;
height: 1.1em;
line-height: 1.1em;
min-width: 1em;
width: 1em;
}
.units {
font-size: 85%;
padding-left: 0.2em;
&.small {
font-size: 50%;
}
}
}
}
}
:host ::ng-deep {
.tb-aggregated-value-card-panel {
> div.tb-aggregated-value-card-title-panel {
.tb-widget-title {
padding: 0;
}
}
}
}

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

@ -0,0 +1,261 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnInit,
TemplateRef,
ViewChild
} from '@angular/core';
import {
aggregatedValueCardDefaultSettings,
AggregatedValueCardKeyPosition,
AggregatedValueCardValue,
AggregatedValueCardWidgetSettings,
computeAggregatedCardValue,
getTsValueByLatestDataKey
} from '@home/components/widget/lib/cards/aggregated-value-card.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { Observable } from 'rxjs';
import {
backgroundStyle,
ColorProcessor,
ComponentStyle,
DateFormatProcessor, getDataKey,
getLatestSingleTsValue,
overlayStyle,
textStyle
} from '@shared/models/widget-settings.models';
import { DatePipe } from '@angular/common';
import { TbFlot } from '@home/components/widget/lib/flot-widget';
import { TbFlotKeySettings, TbFlotSettings } from '@home/components/widget/lib/flot-widget.models';
import { DataKey } from '@shared/models/widget.models';
import { formatNumberValue, formatValue, isDefined, isNumeric } from '@core/utils';
import { map } from 'rxjs/operators';
@Component({
selector: 'tb-aggregated-value-card-widget',
templateUrl: './aggregated-value-card-widget.component.html',
styleUrls: ['./aggregated-value-card-widget.component.scss']
})
export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit {
@ViewChild('chartElement', {static: false}) chartElement: ElementRef;
aggregatedValueCardKeyPosition = AggregatedValueCardKeyPosition;
settings: AggregatedValueCardWidgetSettings;
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
showSubtitle = true;
subtitle$: Observable<string>;
subtitleStyle: ComponentStyle = {};
subtitleColor: ColorProcessor;
showValues = false;
values: {[key: string]: AggregatedValueCardValue} = {};
showChart = true;
chartColor: ColorProcessor;
showDate = true;
dateFormat: DateFormatProcessor;
dateStyle: ComponentStyle = {};
dateColor: ColorProcessor;
backgroundStyle: ComponentStyle = {};
overlayStyle: ComponentStyle = {};
private flot: TbFlot;
private flotDataKey: DataKey;
private lastUpdateTs: number;
tickMin$: Observable<string>;
tickMax$: Observable<string>;
constructor(private date: DatePipe,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
this.ctx.$scope.aggregatedValueCardWidget = this;
this.settings = {...aggregatedValueCardDefaultSettings, ...this.ctx.settings};
this.showSubtitle = this.settings.showSubtitle;
const subtitle = this.settings.subtitle;
this.subtitle$ = this.ctx.registerLabelPattern(subtitle, this.subtitle$);
this.subtitleStyle = textStyle(this.settings.subtitleFont, '0.25px');
this.subtitleColor = ColorProcessor.fromSettings(this.settings.subtitleColor);
const dataKey = getDataKey(this.ctx.defaultSubscription.datasources);
if (dataKey?.name && this.ctx.defaultSubscription.firstDatasource?.latestDataKeys?.length) {
const dataKeys = this.ctx.defaultSubscription.firstDatasource?.latestDataKeys;
for (const position of Object.keys(AggregatedValueCardKeyPosition)) {
const value = computeAggregatedCardValue(dataKeys, dataKey?.name, AggregatedValueCardKeyPosition[position]);
if (value) {
this.values[position] = value;
}
}
this.showValues = !!Object.keys(this.values).length;
}
this.showChart = this.settings.showChart;
this.chartColor = ColorProcessor.fromSettings(this.settings.chartColor);
if (this.showChart) {
if (this.ctx.defaultSubscription.firstDatasource?.dataKeys?.length) {
this.flotDataKey = this.ctx.defaultSubscription.firstDatasource?.dataKeys[0];
this.flotDataKey.settings = {
fillLines: false,
showLines: true,
lineWidth: 2
} as TbFlotKeySettings;
this.flotDataKey.color = this.chartColor.color;
}
}
this.showDate = this.settings.showDate;
this.dateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.dateFormat);
this.dateStyle = textStyle(this.settings.dateFont, '0.25px');
this.dateColor = ColorProcessor.fromSettings(this.settings.dateColor);
this.backgroundStyle = backgroundStyle(this.settings.background);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
}
ngAfterViewInit(): void {
if (this.showChart) {
const settings = {
shadowSize: 0,
enableSelection: false,
smoothLines: false,
grid: {
tickColor: 'rgba(0,0,0,0.12)',
horizontalLines: true,
verticalLines: false,
outlineWidth: 0,
minBorderMargin: 0,
margin: 0
},
yaxis: {
showLabels: false,
tickGenerator: 'return [(axis.max + axis.min) / 2];'
},
xaxis: {
showLabels: false
}
} as TbFlotSettings;
this.flot = new TbFlot(this.ctx, 'line', $(this.chartElement.nativeElement), settings);
this.tickMin$ = this.flot.yMin$.pipe(
map((value) => formatValue(value, (this.flotDataKey?.decimals || this.ctx.decimals),
(this.flotDataKey?.units || this.ctx.units))
));
this.tickMax$ = this.flot.yMax$.pipe(
map((value) => formatValue(value, (this.flotDataKey?.decimals || this.ctx.decimals),
(this.flotDataKey?.units || this.ctx.units))
));
}
}
public onInit() {
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
this.cd.detectChanges();
}
public onDataUpdated() {
const tsValue = getLatestSingleTsValue(this.ctx.data);
let ts;
let value;
if (tsValue) {
ts = tsValue[0];
value = tsValue[1];
}
this.subtitleColor.update(value);
this.dateColor.update(value);
if (this.showChart) {
this.chartColor.update(value);
this.flot.updateSeriesColor(this.chartColor.color);
this.flot.update();
}
this.updateLastUpdateTs(ts);
this.cd.detectChanges();
}
public onLatestDataUpdated() {
if (this.showValues) {
for (const aggValue of Object.values(this.values)) {
const tsValue = getTsValueByLatestDataKey(this.ctx.latestData, aggValue.key);
let ts;
let value;
if (tsValue) {
ts = tsValue[0];
value = tsValue[1];
aggValue.value = formatValue(value, (aggValue.key.decimals || this.ctx.decimals), null, false);
} else {
aggValue.value = 'N/A';
}
const numeric = formatNumberValue(value, (aggValue.key.decimals || this.ctx.decimals));
aggValue.color.update(numeric);
if (aggValue.showArrow && isDefined(numeric)) {
aggValue.upArrow = numeric > 0;
aggValue.downArrow = numeric < 0;
} else {
aggValue.upArrow = aggValue.downArrow = false;
}
this.updateLastUpdateTs(ts);
}
this.cd.detectChanges();
}
}
public onResize() {
if (this.showChart) {
this.flot.resize();
}
}
public onEditModeChanged() {
if (this.showChart) {
this.flot.checkMouseEvents();
}
}
public onDestroy() {
if (this.showChart) {
this.flot.destroy();
}
}
private updateLastUpdateTs(ts: number) {
if (ts && (!this.lastUpdateTs || ts > this.lastUpdateTs)) {
this.lastUpdateTs = ts;
this.dateFormat.update(ts);
}
}
}

232
ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card.models.ts

@ -0,0 +1,232 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
BackgroundSettings,
BackgroundType,
ColorProcessor,
ColorSettings,
ColorType,
ComponentStyle,
constantColor,
DateFormatSettings,
Font,
iconStyle,
lastUpdateAgoDateFormat,
textStyle
} from '@shared/models/widget-settings.models';
import { ComparisonResultType, DataKey, DatasourceData } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { AggregationType } from '@shared/models/time/time.models';
export interface AggregatedValueCardWidgetSettings {
showSubtitle: boolean;
subtitle: string;
subtitleFont: Font;
subtitleColor: ColorSettings;
showDate: boolean;
dateFormat: DateFormatSettings;
dateFont: Font;
dateColor: ColorSettings;
showChart: boolean;
chartColor: ColorSettings;
background: BackgroundSettings;
}
export enum AggregatedValueCardKeyPosition {
center = 'center',
rightTop = 'rightTop',
rightBottom = 'rightBottom',
leftTop = 'leftTop',
leftBottom = 'leftBottom'
}
export const aggregatedValueCardKeyPositionTranslations = new Map<AggregatedValueCardKeyPosition, string>(
[
[AggregatedValueCardKeyPosition.center, 'widgets.aggregated-value-card.position-center'],
[AggregatedValueCardKeyPosition.rightTop, 'widgets.aggregated-value-card.position-right-top'],
[AggregatedValueCardKeyPosition.rightBottom, 'widgets.aggregated-value-card.position-right-bottom'],
[AggregatedValueCardKeyPosition.leftTop, 'widgets.aggregated-value-card.position-left-top'],
[AggregatedValueCardKeyPosition.leftBottom, 'widgets.aggregated-value-card.position-left-bottom']
]
);
export interface AggregatedValueCardKeySettings {
position: AggregatedValueCardKeyPosition;
font: Font;
color: ColorSettings;
showArrow: boolean;
}
export interface AggregatedValueCardValue {
key: DataKey;
value: string;
units: string;
style: ComponentStyle;
color: ColorProcessor;
center: boolean;
showArrow: boolean;
upArrow: boolean;
downArrow: boolean;
}
export const computeAggregatedCardValue = (dataKeys: DataKey[], keyName: string, position: AggregatedValueCardKeyPosition): AggregatedValueCardValue => {
const key = dataKeys.find(dataKey => ( dataKey.name === keyName && (dataKey.settings?.position === position ||
(!dataKey.settings?.position && position === AggregatedValueCardKeyPosition.center)) ));
if (key) {
const settings: AggregatedValueCardKeySettings = key.settings;
return {
key,
value: '',
units: key.units,
style: textStyle(settings.font, '0.25px'),
color: ColorProcessor.fromSettings(settings.color),
center: position === AggregatedValueCardKeyPosition.center,
showArrow: settings.showArrow,
upArrow: false,
downArrow: false
};
}
};
export const getTsValueByLatestDataKey = (latestData: Array<DatasourceData>, dataKey: DataKey): [number, any] => {
if (latestData?.length) {
const dsData = latestData.find(data => data.dataKey === dataKey);
if (dsData?.data?.length) {
return dsData.data[0];
}
}
return null;
};
export const aggregatedValueCardDefaultSettings: AggregatedValueCardWidgetSettings = {
showSubtitle: true,
subtitle: '${entityName}',
subtitleFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: '400',
lineHeight: '16px'
},
subtitleColor: constantColor('rgba(0, 0, 0, 0.38)'),
showDate: true,
dateFormat: lastUpdateAgoDateFormat(),
dateFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: '400',
lineHeight: '16px'
},
dateColor: constantColor('rgba(0, 0, 0, 0.38)'),
showChart: true,
chartColor: constantColor('rgba(0, 0, 0, 0.87)'),
background: {
type: BackgroundType.color,
color: '#fff',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
}
};
export const aggregatedValueCardDefaultKeySettings: AggregatedValueCardKeySettings = {
position: AggregatedValueCardKeyPosition.center,
font: {
family: 'Roboto',
size: 14,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '1'
},
color: constantColor('rgba(0, 0, 0, 0.87)'),
showArrow: false
};
export const createDefaultAggregatedValueLatestDataKeys = (keyName: string, units): DataKey[] => [
{
name: keyName, label: keyName, type: DataKeyType.timeseries, units, decimals: 0,
aggregationType: AggregationType.NONE,
settings: {
position: AggregatedValueCardKeyPosition.center,
font: {
family: 'Roboto',
size: 52,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '1'
},
color: constantColor('rgba(0, 0, 0, 0.87)'),
showArrow: false
} as AggregatedValueCardKeySettings
},
{
name: keyName, label: 'Delta percent ' + keyName, type: DataKeyType.timeseries, units: '%', decimals: 0,
aggregationType: AggregationType.AVG,
comparisonEnabled: true,
timeForComparison: 'previousInterval',
comparisonResultType: ComparisonResultType.DELTA_PERCENT,
settings: {
position: AggregatedValueCardKeyPosition.rightTop,
font: {
family: 'Roboto',
size: 14,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '1'
},
color: {
color: 'rgba(0, 0, 0, 0.87)',
type: ColorType.range,
rangeList: [
{to: 0, color: '#198038'},
{from: 0, to: 0, color: 'rgba(0, 0, 0, 0.87)'},
{from: 0, color: '#D12730'}
],
colorFunction: ''
},
showArrow: true
} as AggregatedValueCardKeySettings
},
{
name: keyName, label: 'Delta absolute ' + keyName, type: DataKeyType.timeseries, units, decimals: 1,
aggregationType: AggregationType.AVG,
comparisonEnabled: true,
timeForComparison: 'previousInterval',
comparisonResultType: ComparisonResultType.DELTA_ABSOLUTE,
settings: {
position: AggregatedValueCardKeyPosition.rightBottom,
font: {
family: 'Roboto',
size: 11,
sizeUnit: 'px',
style: 'normal',
weight: '400',
lineHeight: '1'
},
color: constantColor('rgba(0, 0, 0, 0.38)'),
showArrow: false
} as AggregatedValueCardKeySettings
}
];

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

@ -66,7 +66,7 @@
<div *ngIf="showLabel" [style]="labelStyle" [style.color]="labelColor.color">{{ label$ | async }}</div>
</ng-template>
<ng-template #dateTpl>
<div *ngIf="showDate" [style]="dateStyle" [style.color]="dateColor.color">{{ dateFormat.formatted }}</div>
<div *ngIf="showDate" [style]="dateStyle" [style.color]="dateColor.color" [innerHTML]="dateFormat.formatted"></div>
</ng-template>
<ng-template #valueTpl>
<div class="tb-value-card-value" [style]="valueStyle" [style.color]="valueColor.color">{{ valueText }}</div>

2
ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts

@ -83,7 +83,7 @@ export function loadNodeCtxFunction<F extends (...args: any[]) => any>(functionB
}
export function materialIconHtml(materialIcon: string): string {
return '<mat-icon class="node-icon material-icons" role="img" aria-hidden="false">' + materialIcon + '</mat-icon>';
return '<mat-icon class="mat-icon node-icon material-icons" role="img" aria-hidden="false">' + materialIcon + '</mat-icon>';
}
export function iconUrlHtml(iconUrl: string): string {

1
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts

@ -132,6 +132,7 @@ export interface TbFlotYAxisSettings {
ticksFormatter: string;
tickDecimals: number;
tickSize: number;
tickGenerator: string;
}
export interface TbFlotBaseSettings {

46
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts

@ -18,7 +18,8 @@
import { WidgetContext } from '@home/models/widget-component.models';
import {
createLabelFromDatasource,
deepClone, formattedDataFormDatasourceData,
deepClone,
formattedDataFormDatasourceData,
insertVariable,
isDefined,
isDefinedAndNotNull,
@ -59,6 +60,7 @@ import { AggregationType } from '@shared/models/time/time.models';
import { CancelAnimationFrame } from '@core/services/raf.service';
import { UtilsService } from '@core/services/utils.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { BehaviorSubject } from 'rxjs';
import Timeout = NodeJS.Timeout;
const moment = moment_;
@ -130,9 +132,15 @@ export class TbFlot {
private pieAnimationLastTime: number;
private pieAnimationCaf: CancelAnimationFrame;
constructor(private ctx: WidgetContext, private readonly chartType: ChartType, private $flotElement?: JQuery<any>) {
private yMinSubject = new BehaviorSubject(-1);
private yMaxSubject = new BehaviorSubject(1);
yMin$ = this.yMinSubject.asObservable();
yMax$ = this.yMaxSubject.asObservable();
constructor(private ctx: WidgetContext, private readonly chartType: ChartType, private $flotElement?: JQuery<any>, settings?: TbFlotSettings) {
this.chartType = this.chartType || 'line';
this.settings = ctx.settings as TbFlotSettings;
this.settings = settings || (ctx.settings as TbFlotSettings);
this.utils = this.ctx.$injector.get(UtilsService);
this.enableSelection = isDefined(this.settings.enableSelection) ? this.settings.enableSelection : true;
this.selectionMode = this.enableSelection ? 'x' : null;
@ -209,6 +217,12 @@ export class TbFlot {
} else {
this.yaxis.tickSize = null;
}
if (this.settings.yaxis.tickGenerator?.length) {
try {
this.yaxis.ticks = new Function('axis',
this.settings.yaxis.tickGenerator);
} catch (e) {}
}
if (isNumber(this.settings.yaxis.tickDecimals)) {
this.yaxis.tickDecimals = this.settings.yaxis.tickDecimals;
} else {
@ -717,6 +731,15 @@ export class TbFlot {
}
}
public updateSeriesColor(color: string) {
if (this.subscription?.data?.length) {
const series = this.subscription.data[0] as TbFlotSeries;
series.dataKey.color = color;
series.color = color;
series.highlightColor = tinycolor(color).setAlpha(.75).toRgbString();
}
}
private latestDataByDataIndex(index: number): FormattedData {
if (this.latestData[index]) {
return this.latestData[index];
@ -802,6 +825,8 @@ export class TbFlot {
clearTimeout(this.resizeTimeoutHandle);
this.resizeTimeoutHandle = null;
}
this.yMinSubject.complete();
this.yMaxSubject.complete();
}
private createPlot() {
@ -818,6 +843,7 @@ export class TbFlot {
} else {
this.plot = $.plot(this.$element, this.subscription.data, this.options) as JQueryPlot;
}
this.updateYMinMax();
} else {
this.createPlotTimeoutHandle = setTimeout(this.createPlot.bind(this), 30);
}
@ -830,6 +856,20 @@ export class TbFlot {
this.plot.setupGrid();
}
this.plot.draw();
this.updateYMinMax();
}
private updateYMinMax() {
if (this.plot?.getYAxes().length) {
const min = this.plot?.getYAxes()[0].min;
const max = this.plot?.getYAxes()[0].max;
if (this.yMinSubject.value !== min) {
this.yMinSubject.next(min);
}
if (this.yMaxSubject.value !== max) {
this.yMaxSubject.next(max);
}
}
}
private redrawPlot() {

48
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html

@ -0,0 +1,48 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="aggregatedValueCardKeySettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.aggregated-value-card.value-appearance</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.aggregated-value-card.position' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="position">
<mat-option *ngFor="let position of aggregatedValueCardKeyPositions" [value]="position">
{{ aggregatedValueCardKeyPositionTranslationMap.get(position) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.aggregated-value-card.font' | translate }}</div>
<tb-font-settings formControlName="font"
previewText="22 °C">
</tb-font-settings>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.aggregated-value-card.color' | translate }}</div>
<tb-color-settings formControlName="color">
</tb-color-settings>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="showArrow">
{{ 'widgets.aggregated-value-card.display-up-down-arrow' | translate }}
</mat-slide-toggle>
</div>
</div>
</ng-container>

64
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.ts

@ -0,0 +1,64 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
aggregatedValueCardDefaultKeySettings,
AggregatedValueCardKeyPosition,
aggregatedValueCardKeyPositionTranslations
} from '@home/components/widget/lib/cards/aggregated-value-card.models';
import { constantColor } from '@shared/models/widget-settings.models';
@Component({
selector: 'tb-aggregated-value-card-key-settings',
templateUrl: './aggregated-value-card-key-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class AggregatedValueCardKeySettingsComponent extends WidgetSettingsComponent {
aggregatedValueCardKeyPositions: AggregatedValueCardKeyPosition[] =
Object.keys(AggregatedValueCardKeyPosition).map(value => AggregatedValueCardKeyPosition[value]);
aggregatedValueCardKeyPositionTranslationMap = aggregatedValueCardKeyPositionTranslations;
aggregatedValueCardKeySettingsForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.aggregatedValueCardKeySettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {...aggregatedValueCardDefaultKeySettings};
}
protected onSettingsSet(settings: WidgetSettings) {
this.aggregatedValueCardKeySettingsForm = this.fb.group({
position: [settings.position, []],
font: [settings.font, []],
color: [settings.color, []],
showArrow: [settings.showArrow, []]
});
}
}

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts

@ -74,7 +74,8 @@ export const flotDefaultSettings = (chartType: ChartType): Partial<TbFlotSetting
color: null,
tickSize: null,
tickDecimals: 0,
ticksFormatter: ''
ticksFormatter: '',
tickGenerator: ''
}
};
if (chartType === 'graph') {

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.html

@ -21,7 +21,7 @@
[disabled]="disabled"
#matButton
(click)="openColorSettingsPopup($event, matButton)">
<tb-icon matButtonIcon *ngIf="modelValue.type === colorType.function; else colorPreview">mdi:function-variant</tb-icon>
<tb-icon matButtonIcon *ngIf="modelValue?.type === colorType.function; else colorPreview">mdi:function-variant</tb-icon>
</button>
<ng-template #colorPreview>
<div class="tb-color-preview box" [ngClass]="{'disabled': disabled}">

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts

@ -99,7 +99,7 @@ export class ColorSettingsComponent implements OnInit, ControlValueAccessor {
}
private updateColorStyle() {
if (!this.disabled) {
if (!this.disabled && this.modelValue) {
let colors: string[] = [this.modelValue.color];
if (this.modelValue.type === ColorType.range && this.modelValue.rangeList?.length) {
const rangeColors = this.modelValue.rangeList.slice(0, Math.min(2, this.modelValue.rangeList.length)).map(r => r.color);

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -267,6 +267,9 @@ import {
ValueCardWidgetSettingsComponent
} from '@home/components/widget/lib/settings/cards/value-card-widget-settings.component';
import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings/common/widget-settings-common.module';
import {
AggregatedValueCardKeySettingsComponent
} from '@home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component';
@NgModule({
declarations: [
@ -366,7 +369,8 @@ import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings
TripAnimationWidgetSettingsComponent,
DocLinksWidgetSettingsComponent,
QuickLinksWidgetSettingsComponent,
ValueCardWidgetSettingsComponent
ValueCardWidgetSettingsComponent,
AggregatedValueCardKeySettingsComponent
],
imports: [
CommonModule,
@ -471,7 +475,8 @@ import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings
TripAnimationWidgetSettingsComponent,
DocLinksWidgetSettingsComponent,
QuickLinksWidgetSettingsComponent,
ValueCardWidgetSettingsComponent
ValueCardWidgetSettingsComponent,
AggregatedValueCardKeySettingsComponent
]
})
export class WidgetSettingsModule {
@ -541,5 +546,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-trip-animation-widget-settings': TripAnimationWidgetSettingsComponent,
'tb-doc-links-widget-settings': DocLinksWidgetSettingsComponent,
'tb-quick-links-widget-settings': QuickLinksWidgetSettingsComponent,
'tb-value-card-widget-settings': ValueCardWidgetSettingsComponent
'tb-value-card-widget-settings': ValueCardWidgetSettingsComponent,
'tb-aggregated-value-card-key-settings': AggregatedValueCardKeySettingsComponent,
};

9
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -44,6 +44,9 @@ import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
import { FlotWidgetComponent } from '@home/components/widget/lib/flot-widget.component';
import { LegendComponent } from '@home/components/widget/lib/legend.component';
import { ValueCardWidgetComponent } from '@home/components/widget/lib/cards/value-card-widget.component';
import {
AggregatedValueCardWidgetComponent
} from '@home/components/widget/lib/cards/aggregated-value-card-widget.component';
@NgModule({
declarations:
@ -68,7 +71,8 @@ import { ValueCardWidgetComponent } from '@home/components/widget/lib/cards/valu
SelectEntityDialogComponent,
LegendComponent,
FlotWidgetComponent,
ValueCardWidgetComponent
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent
],
imports: [
CommonModule,
@ -97,7 +101,8 @@ import { ValueCardWidgetComponent } from '@home/components/widget/lib/cards/valu
MarkdownWidgetComponent,
LegendComponent,
FlotWidgetComponent,
ValueCardWidgetComponent
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }

6
ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts

@ -23,6 +23,9 @@ import { profileRoutes } from '@home/pages/profile/profile-routing.module';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
notificationUserSettingsRoutes
} from '@home/pages/notification/settings/notification-settings-routing.modules';
const routes: Routes = [
{
@ -49,7 +52,8 @@ const routes: Routes = [
}
},
...profileRoutes,
...securityRoutes
...securityRoutes,
...notificationUserSettingsRoutes
]
}
];

8
ui-ngx/src/app/modules/home/pages/notification/notification.module.ts

@ -35,6 +35,10 @@ import { EscalationFormComponent } from '@home/pages/notification/rule/escalatio
import { EscalationsComponent } from '@home/pages/notification/rule/escalations.component';
import { RuleNotificationDialogComponent } from '@home/pages/notification/rule/rule-notification-dialog.component';
import { RuleTableHeaderComponent } from '@home/pages/notification/rule/rule-table-header.component';
import { NotificationSettingsComponent } from '@home/pages/notification/settings/notification-settings.component';
import {
NotificationSettingFormComponent
} from '@home/pages/notification/settings/notification-setting-form.component';
@NgModule({
declarations: [
@ -49,7 +53,9 @@ import { RuleTableHeaderComponent } from '@home/pages/notification/rule/rule-tab
EscalationFormComponent,
EscalationsComponent,
RuleNotificationDialogComponent,
RuleTableHeaderComponent
RuleTableHeaderComponent,
NotificationSettingsComponent,
NotificationSettingFormComponent
],
imports: [
CommonModule,

40
ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html

@ -0,0 +1,40 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="notificationSettingsFormGroup" fxLayout="column">
<div fxLayout="row" style="height: 48px;">
<div fxFlex="50" fxLayoutAlign="start center">
<mat-checkbox color="primary"
[checked]="notificationSettingsFormGroup.get('enabled').value"
(click)="toggleEnabled()">
<span class="notification-type"
[ngClass]="{'notification-type-disabled': !notificationSettingsFormGroup.get('enabled').value}">
{{notificationTemplateTypeTranslateMap.get(notificationSettingsFormGroup.get('name').value)?.name | translate}}
</span>
</mat-checkbox>
</div>
<div fxFlex fxLayout="row" *ngFor="let deliveryMethods of deliveryMethods">
<div fxFlex fxLayoutAlign="start center">
<mat-checkbox color="primary"
[disabled]="!allowDeliveryMethods?.includes(deliveryMethods) || !notificationSettingsFormGroup.get('enabled').value"
[checked]="getChecked(deliveryMethods)"
(click)="toggleDeliviryMethod(deliveryMethods)"
></mat-checkbox>
</div>
</div>
</div>
</form>

24
ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.scss

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.notification-type {
font-size: 14px;
&-disabled {
color: rgba(0, 0, 0, 0.38)
}
}
}

126
ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.ts

@ -0,0 +1,126 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { UtilsService } from '@core/services/utils.service';
import { isDefinedAndNotNull } from '@core/utils';
import { Subscription } from 'rxjs';
import {
NotificationDeliveryMethod,
NotificationTemplateTypeTranslateMap,
NotificationUserSetting
} from '@shared/models/notification.models';
@Component({
selector: 'tb-notification-setting-form',
templateUrl: './notification-setting-form.component.html',
styleUrls: ['./notification-setting-form.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NotificationSettingFormComponent),
multi: true
}
]
})
export class NotificationSettingFormComponent implements ControlValueAccessor, OnInit, OnDestroy {
@Input()
disabled: boolean;
@Input()
deliveryMethods: NotificationDeliveryMethod[] = [];
@Input()
allowDeliveryMethods: NotificationDeliveryMethod[] = [];
notificationSettingsFormGroup: UntypedFormGroup;
notificationTemplateTypeTranslateMap = NotificationTemplateTypeTranslateMap;
private propagateChange = null;
private valueChange$: Subscription = null;
constructor(private utils: UtilsService,
private fb: UntypedFormBuilder) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
const deliveryMethod = {};
this.deliveryMethods.forEach(value => {
deliveryMethod[value] = true;
});
this.notificationSettingsFormGroup = this.fb.group(
{
name: [''],
enabled: [true],
enabledDeliveryMethods: this.fb.group({
...deliveryMethod
})
});
this.valueChange$ = this.notificationSettingsFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
ngOnDestroy() {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
this.valueChange$ = null;
}
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.notificationSettingsFormGroup.disable({emitEvent: false});
} else {
this.notificationSettingsFormGroup.enable({emitEvent: false});
}
}
toggleEnabled() {
this.notificationSettingsFormGroup.get('enabled').patchValue(!this.notificationSettingsFormGroup.get('enabled').value);
}
getChecked(deliveryMethod: NotificationDeliveryMethod): boolean {
return this.notificationSettingsFormGroup.get('enabledDeliveryMethods').get(deliveryMethod).value;
}
toggleDeliviryMethod(deliveryMethod: NotificationDeliveryMethod) {
this.notificationSettingsFormGroup.get('enabledDeliveryMethods').get(deliveryMethod)
.patchValue(!this.notificationSettingsFormGroup.get('enabledDeliveryMethods').get(deliveryMethod).value);
}
writeValue(value: NotificationUserSetting): void {
if (isDefinedAndNotNull(value)) {
this.notificationSettingsFormGroup.patchValue(value, {emitEvent: false});
}
}
private updateModel() {
this.propagateChange(this.notificationSettingsFormGroup.value);
}
}

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

Loading…
Cancel
Save