Browse Source

Merge remote-tracking branch 'upstream/master' into improvement/mqtt-reason-codes

pull/10734/head
imbeacon 2 years ago
parent
commit
ff43e1e97d
  1. 2
      application/src/main/data/json/system/widget_types/alarms_table.json
  2. 2
      application/src/main/data/json/system/widget_types/asset_admin_table.json
  3. 2
      application/src/main/data/json/system/widget_types/device_admin_table.json
  4. 2
      application/src/main/data/json/system/widget_types/entities_table.json
  5. 17
      application/src/main/data/json/tenant/dashboards/gateways.json
  6. 5
      application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
  7. 2
      application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java
  8. 2
      application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java
  9. 415
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  10. 42
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  11. 322
      application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java
  12. 133
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  13. 8
      application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerManagerTask.java
  14. 7
      application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java
  15. 25
      application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineConsumerContext.java
  16. 290
      application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java
  17. 2
      application/src/main/resources/thingsboard.yml
  18. 60
      application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java
  19. 1
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java
  20. 2
      common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java
  21. 24
      common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueConfig.java
  22. 2
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  23. 14
      common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java
  24. 5
      common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java
  25. 2
      common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java
  26. 5
      common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java
  27. 6
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java
  28. 8
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java
  29. 101
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
  30. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java
  31. 14
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java
  32. 10
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java
  33. 28
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java
  34. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java
  35. 81
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java
  36. 70
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  37. 86
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java
  38. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java
  39. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
  40. 42
      dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java
  41. 9
      monitoring/src/main/resources/lwm2m/device_profile.json
  42. 4
      monitoring/src/main/resources/lwm2m/models/test-model.xml
  43. 2
      monitoring/src/main/resources/lwm2m/resource.json
  44. 11
      ui-ngx/src/app/core/api/data-aggregator.ts
  45. 58
      ui-ngx/src/app/core/services/dynamic-component-factory.service.ts
  46. 30
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html
  47. 14
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts
  48. 16
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts
  49. 2
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts
  50. 21
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html
  51. 58
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
  52. 6
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html
  53. 29
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts
  54. 12
      ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html
  55. 35
      ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts
  56. 86
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.html
  57. 20
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.scss
  58. 165
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts
  59. 97
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html
  60. 57
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss
  61. 179
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts
  62. 128
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts
  63. 195
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html
  64. 60
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss
  65. 173
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts
  66. 123
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html
  67. 101
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss
  68. 339
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts
  69. 100
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html
  70. 22
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss
  71. 122
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts
  72. 634
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html
  73. 76
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss
  74. 360
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts
  75. 38
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html
  76. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss
  77. 275
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  78. 45
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss
  79. 341
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  80. 7
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts
  81. 247
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  82. 21
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  83. 28
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  84. 4
      ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss
  85. 4
      ui-ngx/src/app/shared/models/time/time.models.ts
  86. 4
      ui-ngx/src/app/shared/models/widget.models.ts
  87. 96
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/expressions_fn.md
  88. 15
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/topic-filter_fn.md
  89. 3
      ui-ngx/src/assets/locale/locale.constant-ar_AE.json
  90. 203
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  91. 58
      ui-ngx/src/assets/metadata/connector-default-configs/bacnet.json
  92. 73
      ui-ngx/src/assets/metadata/connector-default-configs/ble.json
  93. 89
      ui-ngx/src/assets/metadata/connector-default-configs/can.json
  94. 65
      ui-ngx/src/assets/metadata/connector-default-configs/ftp.json
  95. 248
      ui-ngx/src/assets/metadata/connector-default-configs/modbus.json
  96. 218
      ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json
  97. 66
      ui-ngx/src/assets/metadata/connector-default-configs/ocpp.json
  98. 58
      ui-ngx/src/assets/metadata/connector-default-configs/odbc.json
  99. 52
      ui-ngx/src/assets/metadata/connector-default-configs/opcua.json
  100. 52
      ui-ngx/src/assets/metadata/connector-default-configs/opcua_asyncio.json

2
application/src/main/data/json/system/widget_types/alarms_table.json

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-alarms-table-widget \n [ctx]=\"ctx\">\n</tb-alarms-table-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.alarmsTableWidget.onEditModeChanged();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.alarmsTableWidget.onEditModeChanged();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-alarms-table-widget-settings",

2
application/src/main/data/json/system/widget_types/asset_admin_table.json

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-entities-table-widget \n [ctx]=\"ctx\">\n</tb-entities-table-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-entities-table-widget-settings",

2
application/src/main/data/json/system/widget_types/device_admin_table.json

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-entities-table-widget \n [ctx]=\"ctx\">\n</tb-entities-table-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-entities-table-widget-settings",

2
application/src/main/data/json/system/widget_types/entities_table.json

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-entities-table-widget \n [ctx]=\"ctx\">\n</tb-entities-table-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-entities-table-widget-settings",

17
application/src/main/data/json/tenant/dashboards/gateways.json

File diff suppressed because one or more lines are too long

5
application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java

@ -33,7 +33,6 @@ import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredential;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode;
import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration;
@ -68,6 +67,8 @@ import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static org.eclipse.leshan.core.LwM2m.Version.V1_0;
@Service
@TbCoreComponent
@RequiredArgsConstructor
@ -256,7 +257,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration();
transportConfiguration.setBootstrap(Collections.emptyList());
transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(1, 1, 1, PowerMode.DRX, null, null, null, null, null));
transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(1, 1, 1, PowerMode.DRX, null, null, null, null, null, V1_0.toString()));
transportConfiguration.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap()));
DeviceProfileData deviceProfileData = new DeviceProfileData();

2
application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java

@ -27,11 +27,11 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperService
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.consumer.QueueConsumerManager;
import org.thingsboard.server.queue.housekeeper.HousekeeperConfig;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.queue.consumer.QueueConsumerManager;
import javax.annotation.PreDestroy;
import java.util.LinkedHashSet;

2
application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java

@ -27,13 +27,13 @@ import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.consumer.QueueConsumerManager;
import org.thingsboard.server.queue.housekeeper.HousekeeperConfig;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.housekeeper.processor.HousekeeperTaskProcessor;
import org.thingsboard.server.service.housekeeper.stats.HousekeeperStatsService;
import org.thingsboard.server.service.queue.consumer.QueueConsumerManager;
import javax.annotation.PreDestroy;
import java.util.List;

415
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -20,12 +20,12 @@ import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.data.queue.QueueConfig;
import org.thingsboard.server.common.data.rpc.RpcError;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.TbActorMsg;
@ -78,10 +79,11 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceM
import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.consumer.QueueConsumerManager;
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.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.edge.EdgeNotificationService;
@ -89,6 +91,7 @@ import org.thingsboard.server.service.notification.NotificationSchedulerService;
import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.consumer.MainQueueConsumerManager;
import org.thingsboard.server.service.queue.processing.AbstractConsumerService;
import org.thingsboard.server.service.queue.processing.IdMsgPair;
import org.thingsboard.server.service.resource.TbImageService;
@ -109,7 +112,6 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@ -122,9 +124,11 @@ import java.util.stream.Collectors;
public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCoreNotificationMsg> implements TbCoreConsumerService {
@Value("${queue.core.poll-interval}")
private long pollDuration;
private long pollInterval;
@Value("${queue.core.pack-processing-timeout}")
private long packProcessingTimeout;
@Value("${queue.core.consumer-per-partition:true}")
private boolean consumerPerPartition;
@Value("${queue.core.stats.enabled:false}")
private boolean statsEnabled;
@ -133,7 +137,6 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
@Value("${queue.core.ota.pack-size:100}")
private int firmwarePackSize;
private final TbQueueConsumer<TbProtoQueueMsg<ToCoreMsg>> mainConsumer;
private final DeviceStateService stateService;
private final TbApiUsageStateService statsService;
private final TbLocalSubscriptionService localSubscriptionService;
@ -144,14 +147,14 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
private final GitVersionControlQueueService vcQueueService;
private final NotificationSchedulerService notificationSchedulerService;
private final NotificationRuleProcessor notificationRuleProcessor;
private final TbCoreConsumerStats stats;
protected final TbQueueConsumer<TbProtoQueueMsg<ToUsageStatsServiceMsg>> usageStatsConsumer;
private final TbQueueConsumer<TbProtoQueueMsg<ToOtaPackageStateServiceMsg>> firmwareStatesConsumer;
private final TbCoreQueueFactory queueFactory;
private final TbImageService imageService;
private final TbCoreConsumerStats stats;
private MainQueueConsumerManager<TbProtoQueueMsg<ToCoreMsg>, CoreQueueConfig> mainConsumer;
private QueueConsumerManager<TbProtoQueueMsg<ToUsageStatsServiceMsg>> usageStatsConsumer;
private QueueConsumerManager<TbProtoQueueMsg<ToOtaPackageStateServiceMsg>> firmwareStatesConsumer;
protected volatile ExecutorService consumersExecutor;
protected volatile ExecutorService usageStatsExecutor;
private volatile ExecutorService firmwareStatesExecutor;
private volatile ListeningExecutorService deviceActivityEventsExecutor;
public DefaultTbCoreConsumerService(TbCoreQueueFactory tbCoreQueueFactory,
@ -176,10 +179,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
NotificationRuleProcessor notificationRuleProcessor,
TbImageService imageService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService,
eventPublisher, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService);
this.mainConsumer = tbCoreQueueFactory.createToCoreMsgConsumer();
this.usageStatsConsumer = tbCoreQueueFactory.createToUsageStatsServiceMsgConsumer();
this.firmwareStatesConsumer = tbCoreQueueFactory.createToOtaPackageStateServiceMsgConsumer();
eventPublisher, jwtSettingsService);
this.stateService = stateService;
this.localSubscriptionService = localSubscriptionService;
this.subscriptionManagerService = subscriptionManagerService;
@ -192,152 +192,146 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
this.notificationSchedulerService = notificationSchedulerService;
this.notificationRuleProcessor = notificationRuleProcessor;
this.imageService = imageService;
this.queueFactory = tbCoreQueueFactory;
}
@PostConstruct
public void init() {
super.init("tb-core-notifications-consumer");
this.consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("tb-core-consumer"));
this.usageStatsExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-core-usage-stats-consumer"));
this.firmwareStatesExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-core-firmware-notifications-consumer"));
super.init("tb-core");
this.deviceActivityEventsExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-core-device-activity-events-executor")));
this.mainConsumer = MainQueueConsumerManager.<TbProtoQueueMsg<ToCoreMsg>, CoreQueueConfig>builder()
.queueKey(new QueueKey(ServiceType.TB_CORE))
.config(CoreQueueConfig.of(consumerPerPartition, (int) pollInterval))
.msgPackProcessor(this::processMsgs)
.consumerCreator(config -> queueFactory.createToCoreMsgConsumer())
.consumerExecutor(consumersExecutor)
.scheduler(scheduler)
.taskExecutor(mgmtExecutor)
.build();
this.usageStatsConsumer = QueueConsumerManager.<TbProtoQueueMsg<ToUsageStatsServiceMsg>>builder()
.name("TB Usage Stats")
.msgPackProcessor(this::processUsageStatsMsg)
.pollInterval(pollInterval)
.consumerCreator(queueFactory::createToUsageStatsServiceMsgConsumer)
.consumerExecutor(consumersExecutor)
.threadPrefix("usage-stats")
.build();
this.firmwareStatesConsumer = QueueConsumerManager.<TbProtoQueueMsg<ToOtaPackageStateServiceMsg>>builder()
.name("TB Ota Package States")
.msgPackProcessor(this::processFirmwareMsgs)
.pollInterval(pollInterval)
.consumerCreator(queueFactory::createToOtaPackageStateServiceMsgConsumer)
.consumerExecutor(consumersExecutor)
.threadPrefix("firmware")
.build();
}
@PreDestroy
public void destroy() {
super.destroy();
if (consumersExecutor != null) {
consumersExecutor.shutdownNow();
}
if (usageStatsExecutor != null) {
usageStatsExecutor.shutdownNow();
}
if (firmwareStatesExecutor != null) {
firmwareStatesExecutor.shutdownNow();
}
if (deviceActivityEventsExecutor != null) {
deviceActivityEventsExecutor.shutdownNow();
}
}
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
public void onApplicationEvent(ApplicationReadyEvent event) {
super.onApplicationEvent(event);
launchUsageStatsConsumer();
launchOtaPackageUpdateNotificationConsumer();
@Override
protected void startConsumers() {
super.startConsumers();
firmwareStatesConsumer.subscribe();
firmwareStatesConsumer.launch();
usageStatsConsumer.launch();
}
@Override
protected void onTbApplicationEvent(PartitionChangeEvent event) {
log.info("Subscribing to partitions: {}", event.getPartitions());
this.mainConsumer.subscribe(event.getPartitions());
this.usageStatsConsumer.subscribe(
event
.getPartitions()
.stream()
.map(tpi -> tpi.newByTopic(usageStatsConsumer.getTopic()))
.collect(Collectors.toSet()));
this.firmwareStatesConsumer.subscribe();
}
@Override
protected void launchMainConsumers() {
consumersExecutor.submit(() -> {
while (!stopped) {
mainConsumer.update(event.getPartitions());
usageStatsConsumer.subscribe(event.getPartitions()
.stream()
.map(tpi -> tpi.newByTopic(usageStatsConsumer.getConsumer().getTopic()))
.collect(Collectors.toSet()));
}
private void processMsgs(List<TbProtoQueueMsg<ToCoreMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToCoreMsg>> consumer, CoreQueueConfig config) throws Exception {
List<IdMsgPair<ToCoreMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList());
ConcurrentMap<UUID, TbProtoQueueMsg<ToCoreMsg>> pendingMap = orderedMsgList.stream().collect(
Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg));
CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
TbPackProcessingContext<TbProtoQueueMsg<ToCoreMsg>> ctx = new TbPackProcessingContext<>(
processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
PendingMsgHolder pendingMsgHolder = new PendingMsgHolder();
Future<?> packSubmitFuture = consumersExecutor.submit(() -> {
orderedMsgList.forEach((element) -> {
UUID id = element.getUuid();
TbProtoQueueMsg<ToCoreMsg> msg = element.getMsg();
log.trace("[{}] Creating main callback for message: {}", id, msg.getValue());
TbCallback callback = new TbPackCallback<>(id, ctx);
try {
List<TbProtoQueueMsg<ToCoreMsg>> msgs = mainConsumer.poll(pollDuration);
if (msgs.isEmpty()) {
continue;
}
List<IdMsgPair<ToCoreMsg>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList());
ConcurrentMap<UUID, TbProtoQueueMsg<ToCoreMsg>> pendingMap = orderedMsgList.stream().collect(
Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg));
CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
TbPackProcessingContext<TbProtoQueueMsg<ToCoreMsg>> ctx = new TbPackProcessingContext<>(
processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
PendingMsgHolder pendingMsgHolder = new PendingMsgHolder();
Future<?> packSubmitFuture = consumersExecutor.submit(() -> {
orderedMsgList.forEach((element) -> {
UUID id = element.getUuid();
TbProtoQueueMsg<ToCoreMsg> msg = element.getMsg();
log.trace("[{}] Creating main callback for message: {}", id, msg.getValue());
TbCallback callback = new TbPackCallback<>(id, ctx);
try {
ToCoreMsg toCoreMsg = msg.getValue();
pendingMsgHolder.setToCoreMsg(toCoreMsg);
if (toCoreMsg.hasToSubscriptionMgrMsg()) {
log.trace("[{}] Forwarding message to subscription manager service {}", id, toCoreMsg.getToSubscriptionMgrMsg());
forwardToSubMgrService(toCoreMsg.getToSubscriptionMgrMsg(), callback);
} else if (toCoreMsg.hasToDeviceActorMsg()) {
log.trace("[{}] Forwarding message to device actor {}", id, toCoreMsg.getToDeviceActorMsg());
forwardToDeviceActor(toCoreMsg.getToDeviceActorMsg(), callback);
} else if (toCoreMsg.hasDeviceStateServiceMsg()) {
log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceStateServiceMsg());
forwardToStateService(toCoreMsg.getDeviceStateServiceMsg(), callback);
} else if (toCoreMsg.hasEdgeNotificationMsg()) {
log.trace("[{}] Forwarding message to edge service {}", id, toCoreMsg.getEdgeNotificationMsg());
forwardToEdgeNotificationService(toCoreMsg.getEdgeNotificationMsg(), callback);
} else if (toCoreMsg.hasDeviceConnectMsg()) {
log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceConnectMsg());
forwardToStateService(toCoreMsg.getDeviceConnectMsg(), callback);
} else if (toCoreMsg.hasDeviceActivityMsg()) {
log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceActivityMsg());
forwardToStateService(toCoreMsg.getDeviceActivityMsg(), callback);
} else if (toCoreMsg.hasDeviceDisconnectMsg()) {
log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceDisconnectMsg());
forwardToStateService(toCoreMsg.getDeviceDisconnectMsg(), callback);
} else if (toCoreMsg.hasDeviceInactivityMsg()) {
log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceInactivityMsg());
forwardToStateService(toCoreMsg.getDeviceInactivityMsg(), callback);
} else if (toCoreMsg.hasToDeviceActorNotification()) {
TbActorMsg actorMsg = ProtoUtils.fromProto(toCoreMsg.getToDeviceActorNotification());
if (actorMsg != null) {
if (actorMsg.getMsgType().equals(MsgType.DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG)) {
tbCoreDeviceRpcService.forwardRpcRequestToDeviceActor((ToDeviceRpcRequestActorMsg) actorMsg);
} else {
log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg);
actorContext.tell(actorMsg);
}
}
callback.onSuccess();
} else if (toCoreMsg.hasNotificationSchedulerServiceMsg()) {
TransportProtos.NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = toCoreMsg.getNotificationSchedulerServiceMsg();
log.trace("[{}] Forwarding message to notification scheduler service {}", id, toCoreMsg.getNotificationSchedulerServiceMsg());
forwardToNotificationSchedulerService(notificationSchedulerServiceMsg, callback);
} else if (toCoreMsg.hasErrorEventMsg()) {
forwardToEventService(toCoreMsg.getErrorEventMsg(), callback);
} else if (toCoreMsg.hasLifecycleEventMsg()) {
forwardToEventService(toCoreMsg.getLifecycleEventMsg(), callback);
}
} catch (Throwable e) {
log.warn("[{}] Failed to process message: {}", id, msg, e);
callback.onFailure(e);
ToCoreMsg toCoreMsg = msg.getValue();
pendingMsgHolder.setToCoreMsg(toCoreMsg);
if (toCoreMsg.hasToSubscriptionMgrMsg()) {
log.trace("[{}] Forwarding message to subscription manager service {}", id, toCoreMsg.getToSubscriptionMgrMsg());
forwardToSubMgrService(toCoreMsg.getToSubscriptionMgrMsg(), callback);
} else if (toCoreMsg.hasToDeviceActorMsg()) {
log.trace("[{}] Forwarding message to device actor {}", id, toCoreMsg.getToDeviceActorMsg());
forwardToDeviceActor(toCoreMsg.getToDeviceActorMsg(), callback);
} else if (toCoreMsg.hasDeviceStateServiceMsg()) {
log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceStateServiceMsg());
forwardToStateService(toCoreMsg.getDeviceStateServiceMsg(), callback);
} else if (toCoreMsg.hasEdgeNotificationMsg()) {
log.trace("[{}] Forwarding message to edge service {}", id, toCoreMsg.getEdgeNotificationMsg());
forwardToEdgeNotificationService(toCoreMsg.getEdgeNotificationMsg(), callback);
} else if (toCoreMsg.hasDeviceConnectMsg()) {
log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceConnectMsg());
forwardToStateService(toCoreMsg.getDeviceConnectMsg(), callback);
} else if (toCoreMsg.hasDeviceActivityMsg()) {
log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceActivityMsg());
forwardToStateService(toCoreMsg.getDeviceActivityMsg(), callback);
} else if (toCoreMsg.hasDeviceDisconnectMsg()) {
log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceDisconnectMsg());
forwardToStateService(toCoreMsg.getDeviceDisconnectMsg(), callback);
} else if (toCoreMsg.hasDeviceInactivityMsg()) {
log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceInactivityMsg());
forwardToStateService(toCoreMsg.getDeviceInactivityMsg(), callback);
} else if (toCoreMsg.hasToDeviceActorNotification()) {
TbActorMsg actorMsg = ProtoUtils.fromProto(toCoreMsg.getToDeviceActorNotification());
if (actorMsg != null) {
if (actorMsg.getMsgType().equals(MsgType.DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG)) {
tbCoreDeviceRpcService.forwardRpcRequestToDeviceActor((ToDeviceRpcRequestActorMsg) actorMsg);
} else {
log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg);
actorContext.tell(actorMsg);
}
});
});
if (!processingTimeoutLatch.await(packProcessingTimeout, TimeUnit.MILLISECONDS)) {
if (!packSubmitFuture.isDone()) {
packSubmitFuture.cancel(true);
ToCoreMsg lastSubmitMsg = pendingMsgHolder.getToCoreMsg();
log.info("Timeout to process message: {}", lastSubmitMsg);
}
ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue()));
ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue()));
}
mainConsumer.commit();
} catch (Exception e) {
if (!stopped) {
log.warn("Failed to obtain messages from queue.", e);
try {
Thread.sleep(pollDuration);
} catch (InterruptedException e2) {
log.trace("Failed to wait until the server has capacity to handle new requests", e2);
}
callback.onSuccess();
} else if (toCoreMsg.hasNotificationSchedulerServiceMsg()) {
TransportProtos.NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = toCoreMsg.getNotificationSchedulerServiceMsg();
log.trace("[{}] Forwarding message to notification scheduler service {}", id, toCoreMsg.getNotificationSchedulerServiceMsg());
forwardToNotificationSchedulerService(notificationSchedulerServiceMsg, callback);
} else if (toCoreMsg.hasErrorEventMsg()) {
forwardToEventService(toCoreMsg.getErrorEventMsg(), callback);
} else if (toCoreMsg.hasLifecycleEventMsg()) {
forwardToEventService(toCoreMsg.getLifecycleEventMsg(), callback);
}
} catch (Throwable e) {
log.warn("[{}] Failed to process message: {}", id, msg, e);
callback.onFailure(e);
}
}
log.info("TB Core Consumer stopped.");
});
});
if (!processingTimeoutLatch.await(packProcessingTimeout, TimeUnit.MILLISECONDS)) {
if (!packSubmitFuture.isDone()) {
packSubmitFuture.cancel(true);
ToCoreMsg lastSubmitMsg = pendingMsgHolder.getToCoreMsg();
log.info("Timeout to process message: {}", lastSubmitMsg);
}
if (log.isDebugEnabled()) {
ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue()));
}
ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue()));
}
consumer.commit();
}
private static class PendingMsgHolder {
@ -353,7 +347,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
@Override
protected long getNotificationPollDuration() {
return pollDuration;
return pollInterval;
}
@Override
@ -361,6 +355,16 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
return packProcessingTimeout;
}
@Override
protected int getMgmtThreadPoolSize() {
return Math.max(Runtime.getRuntime().availableProcessors(), 4);
}
@Override
protected TbQueueConsumer<TbProtoQueueMsg<ToCoreNotificationMsg>> createNotificationsConsumer() {
return queueFactory.createToCoreNotificationsMsgConsumer();
}
@Override
protected void handleNotification(UUID id, TbProtoQueueMsg<ToCoreNotificationMsg> msg, TbCallback callback) {
ToCoreNotificationMsg toCoreNotification = msg.getValue();
@ -409,92 +413,55 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
}
}
private void launchUsageStatsConsumer() {
usageStatsExecutor.submit(() -> {
while (!stopped) {
try {
List<TbProtoQueueMsg<ToUsageStatsServiceMsg>> msgs = usageStatsConsumer.poll(getNotificationPollDuration());
if (msgs.isEmpty()) {
continue;
}
ConcurrentMap<UUID, TbProtoQueueMsg<ToUsageStatsServiceMsg>> pendingMap = msgs.stream().collect(
Collectors.toConcurrentMap(s -> UUID.randomUUID(), Function.identity()));
CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
TbPackProcessingContext<TbProtoQueueMsg<ToUsageStatsServiceMsg>> ctx = new TbPackProcessingContext<>(
processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
pendingMap.forEach((id, msg) -> {
log.trace("[{}] Creating usage stats callback for message: {}", id, msg.getValue());
TbCallback callback = new TbPackCallback<>(id, ctx);
try {
handleUsageStats(msg, callback);
} catch (Throwable e) {
log.warn("[{}] Failed to process usage stats: {}", id, msg, e);
callback.onFailure(e);
}
});
if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) {
ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process usage stats: {}", id, msg.getValue()));
ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process usage stats: {}", id, msg.getValue()));
}
usageStatsConsumer.commit();
} catch (Exception e) {
if (!stopped) {
log.warn("Failed to obtain usage stats from queue.", e);
try {
Thread.sleep(getNotificationPollDuration());
} catch (InterruptedException e2) {
log.trace("Failed to wait until the server has capacity to handle new usage stats", e2);
}
}
}
private void processUsageStatsMsg(List<TbProtoQueueMsg<ToUsageStatsServiceMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToUsageStatsServiceMsg>> consumer) throws Exception {
ConcurrentMap<UUID, TbProtoQueueMsg<ToUsageStatsServiceMsg>> pendingMap = msgs.stream().collect(
Collectors.toConcurrentMap(s -> UUID.randomUUID(), Function.identity()));
CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
TbPackProcessingContext<TbProtoQueueMsg<ToUsageStatsServiceMsg>> ctx = new TbPackProcessingContext<>(
processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
pendingMap.forEach((id, msg) -> {
log.trace("[{}] Creating usage stats callback for message: {}", id, msg.getValue());
TbCallback callback = new TbPackCallback<>(id, ctx);
try {
handleUsageStats(msg, callback);
} catch (Throwable e) {
log.warn("[{}] Failed to process usage stats: {}", id, msg, e);
callback.onFailure(e);
}
log.info("TB Usage Stats Consumer stopped.");
});
if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) {
ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process usage stats: {}", id, msg.getValue()));
ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process usage stats: {}", id, msg.getValue()));
}
consumer.commit();
}
private void launchOtaPackageUpdateNotificationConsumer() {
private void processFirmwareMsgs(List<TbProtoQueueMsg<ToOtaPackageStateServiceMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToOtaPackageStateServiceMsg>> consumer) {
long maxProcessingTimeoutPerRecord = firmwarePackInterval / firmwarePackSize;
firmwareStatesExecutor.submit(() -> {
while (!stopped) {
try {
List<TbProtoQueueMsg<ToOtaPackageStateServiceMsg>> msgs = firmwareStatesConsumer.poll(getNotificationPollDuration());
if (msgs.isEmpty()) {
continue;
}
long timeToSleep = maxProcessingTimeoutPerRecord;
for (TbProtoQueueMsg<ToOtaPackageStateServiceMsg> msg : msgs) {
try {
long startTime = System.currentTimeMillis();
boolean isSuccessUpdate = handleOtaPackageUpdates(msg);
long endTime = System.currentTimeMillis();
long spentTime = endTime - startTime;
timeToSleep = timeToSleep - spentTime;
if (isSuccessUpdate) {
if (timeToSleep > 0) {
log.debug("Spent time per record is: [{}]!", spentTime);
Thread.sleep(timeToSleep);
timeToSleep = 0;
}
timeToSleep += maxProcessingTimeoutPerRecord;
}
} catch (Throwable e) {
log.warn("Failed to process firmware update msg: {}", msg, e);
}
}
firmwareStatesConsumer.commit();
} catch (Exception e) {
if (!stopped) {
log.warn("Failed to obtain usage stats from queue.", e);
try {
Thread.sleep(getNotificationPollDuration());
} catch (InterruptedException e2) {
log.trace("Failed to wait until the server has capacity to handle new firmware updates", e2);
}
long timeToSleep = maxProcessingTimeoutPerRecord;
for (TbProtoQueueMsg<ToOtaPackageStateServiceMsg> msg : msgs) {
try {
long startTime = System.currentTimeMillis();
boolean isSuccessUpdate = handleOtaPackageUpdates(msg);
long endTime = System.currentTimeMillis();
long spentTime = endTime - startTime;
timeToSleep = timeToSleep - spentTime;
if (isSuccessUpdate) {
if (timeToSleep > 0) {
log.debug("Spent time per record is: [{}]!", spentTime);
Thread.sleep(timeToSleep);
timeToSleep = 0;
}
timeToSleep += maxProcessingTimeoutPerRecord;
}
} catch (InterruptedException e) {
return;
} catch (Throwable e) {
log.warn("Failed to process firmware update msg: {}", msg, e);
}
log.info("TB Ota Package States Consumer stopped.");
});
}
consumer.commit();
}
private void handleUsageStats(TbProtoQueueMsg<ToUsageStatsServiceMsg> msg, TbCallback callback) {
@ -782,15 +749,17 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
@Override
protected void stopConsumers() {
if (mainConsumer != null) {
mainConsumer.unsubscribe();
}
if (usageStatsConsumer != null) {
usageStatsConsumer.unsubscribe();
}
if (firmwareStatesConsumer != null) {
firmwareStatesConsumer.unsubscribe();
}
super.stopConsumers();
mainConsumer.stop();
mainConsumer.awaitStop();
usageStatsConsumer.stop();
firmwareStatesConsumer.stop();
}
@Data(staticConstructor = "of")
public static class CoreQueueConfig implements QueueConfig {
private final boolean consumerPerPartition;
private final int pollInterval;
}
}

42
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -15,8 +15,8 @@
*/
package org.thingsboard.server.service.queue;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Scheduled;
@ -39,12 +39,11 @@ import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.QueueDeleteMsg;
import org.thingsboard.server.gen.transport.TransportProtos.QueueUpdateMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
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.queue.provider.TbRuleEngineQueueFactory;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
@ -55,8 +54,6 @@ import org.thingsboard.server.service.queue.ruleengine.TbRuleEngineQueueConsumer
import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import jakarta.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@ -77,7 +74,6 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
private final ConcurrentMap<QueueKey, TbRuleEngineQueueConsumerManager> consumers = new ConcurrentHashMap<>();
public DefaultTbRuleEngineConsumerService(TbRuleEngineConsumerContext ctx,
TbRuleEngineQueueFactory tbRuleEngineQueueFactory,
ActorSystemContext actorContext,
TbRuleEngineDeviceRpcService tbDeviceRpcService,
QueueService queueService,
@ -88,8 +84,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService,
eventPublisher, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer(), jwtSettingsService);
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
this.ctx = ctx;
this.tbDeviceRpcService = tbDeviceRpcService;
this.queueService = queueService;
@ -97,7 +92,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
@PostConstruct
public void init() {
super.init("tb-rule-engine-notifications-consumer");
super.init("tb-rule-engine");
List<Queue> queues = queueService.findAllQueues();
for (Queue configuration : queues) {
if (partitionService.isManagedByCurrentService(configuration.getTenantId())) {
@ -137,20 +132,11 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
});
}
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
public void onApplicationEvent(ApplicationReadyEvent event) {
super.onApplicationEvent(event);
ctx.setReady(true);
}
@Override
protected void launchMainConsumers() {}
@Override
protected void stopConsumers() {
super.stopConsumers();
consumers.values().forEach(TbRuleEngineQueueConsumerManager::stop);
consumers.values().forEach(TbRuleEngineQueueConsumerManager::awaitStop);
ctx.stop();
}
@Override
@ -168,6 +154,16 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
return ctx.getPackProcessingTimeout();
}
@Override
protected int getMgmtThreadPoolSize() {
return ctx.getMgmtThreadPoolSize();
}
@Override
protected TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineNotificationMsg>> createNotificationsConsumer() {
return ctx.getQueueFactory().createToRuleEngineNotificationsMsgConsumer();
}
@Override
protected void handleNotification(UUID id, TbProtoQueueMsg<ToRuleEngineNotificationMsg> msg, TbCallback callback) throws Exception {
ToRuleEngineNotificationMsg nfMsg = msg.getValue();
@ -244,7 +240,13 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
}
private TbRuleEngineQueueConsumerManager createConsumer(QueueKey queueKey, Queue queue) {
var consumer = new TbRuleEngineQueueConsumerManager(ctx, queueKey);
var consumer = TbRuleEngineQueueConsumerManager.create()
.ctx(ctx)
.queueKey(queueKey)
.consumerExecutor(consumersExecutor)
.scheduler(scheduler)
.taskExecutor(mgmtExecutor)
.build();
consumers.put(queueKey, consumer);
consumer.init(queue);
return consumer;

322
application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java

@ -0,0 +1,322 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.queue.consumer;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.queue.QueueConfig;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.service.queue.ruleengine.QueueEvent;
import org.thingsboard.server.service.queue.ruleengine.TbQueueConsumerManagerTask;
import org.thingsboard.server.service.queue.ruleengine.TbQueueConsumerTask;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.stream.Collectors;
@Slf4j
public class MainQueueConsumerManager<M extends TbQueueMsg, C extends QueueConfig> {
protected final QueueKey queueKey;
@Getter
protected C config;
protected final MsgPackProcessor<M, C> msgPackProcessor;
protected final Function<C, TbQueueConsumer<M>> consumerCreator;
protected final ExecutorService consumerExecutor;
protected final ScheduledExecutorService scheduler;
protected final ExecutorService taskExecutor;
private final java.util.Queue<TbQueueConsumerManagerTask> tasks = new ConcurrentLinkedQueue<>();
private final ReentrantLock lock = new ReentrantLock();
@Getter
private volatile Set<TopicPartitionInfo> partitions;
protected volatile ConsumerWrapper<M> consumerWrapper;
protected volatile boolean stopped;
@Builder
public MainQueueConsumerManager(QueueKey queueKey, C config,
MsgPackProcessor<M, C> msgPackProcessor,
Function<C, TbQueueConsumer<M>> consumerCreator,
ExecutorService consumerExecutor,
ScheduledExecutorService scheduler,
ExecutorService taskExecutor) {
this.queueKey = queueKey;
this.config = config;
this.msgPackProcessor = msgPackProcessor;
this.consumerCreator = consumerCreator;
this.consumerExecutor = consumerExecutor;
this.scheduler = scheduler;
this.taskExecutor = taskExecutor;
if (config != null) {
init(config);
}
}
public void init(C config) {
this.config = config;
if (config.isConsumerPerPartition()) {
this.consumerWrapper = new ConsumerPerPartitionWrapper();
} else {
this.consumerWrapper = new SingleConsumerWrapper();
}
log.debug("[{}] Initialized consumer for queue: {}", queueKey, config);
}
public void update(C config) {
addTask(TbQueueConsumerManagerTask.configUpdate(config));
}
public void update(Set<TopicPartitionInfo> partitions) {
addTask(TbQueueConsumerManagerTask.partitionChange(partitions));
}
protected void addTask(TbQueueConsumerManagerTask todo) {
if (stopped) {
return;
}
tasks.add(todo);
log.trace("[{}] Added task: {}", queueKey, todo);
tryProcessTasks();
}
private void tryProcessTasks() {
taskExecutor.submit(() -> {
if (lock.tryLock()) {
try {
C newConfig = null;
Set<TopicPartitionInfo> newPartitions = null;
while (!stopped) {
TbQueueConsumerManagerTask task = tasks.poll();
if (task == null) {
break;
}
log.trace("[{}] Processing task: {}", queueKey, task);
if (task.getEvent() == QueueEvent.PARTITION_CHANGE) {
newPartitions = task.getPartitions();
} else if (task.getEvent() == QueueEvent.CONFIG_UPDATE) {
newConfig = (C) task.getConfig();
} else {
processTask(task);
}
}
if (stopped) {
return;
}
if (newConfig != null) {
doUpdate(newConfig);
}
if (newPartitions != null) {
doUpdate(newPartitions);
}
} catch (Exception e) {
log.error("[{}] Failed to process tasks", queueKey, e);
} finally {
lock.unlock();
}
} else {
log.trace("[{}] Failed to acquire lock", queueKey);
scheduler.schedule(this::tryProcessTasks, 1, TimeUnit.SECONDS);
}
});
}
protected void processTask(TbQueueConsumerManagerTask task) {
}
private void doUpdate(C newConfig) {
log.info("[{}] Processing queue update: {}", queueKey, newConfig);
var oldConfig = this.config;
this.config = newConfig;
if (log.isTraceEnabled()) {
log.trace("[{}] Old queue configuration: {}", queueKey, oldConfig);
log.trace("[{}] New queue configuration: {}", queueKey, newConfig);
}
if (oldConfig == null) {
init(config);
} else if (newConfig.isConsumerPerPartition() != oldConfig.isConsumerPerPartition()) {
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop);
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion);
init(config);
if (partitions != null) {
doUpdate(partitions); // even if partitions number was changed, there can be no partition change event
}
} else {
log.trace("[{}] Silently applied new config, because consumer-per-partition not changed", queueKey);
// do nothing, because partitions change (if they changed) will be handled on PartitionChangeEvent,
// and changes to other config values will be picked up by consumer on the fly,
// and queue topic and name are immutable
}
}
private void doUpdate(Set<TopicPartitionInfo> partitions) {
this.partitions = partitions;
consumerWrapper.updatePartitions(partitions);
}
private void launchConsumer(TbQueueConsumerTask<M> consumerTask) {
log.info("[{}] Launching consumer", consumerTask.getKey());
Future<?> consumerLoop = consumerExecutor.submit(() -> {
ThingsBoardThreadFactory.updateCurrentThreadName(consumerTask.getKey().toString());
try {
consumerLoop(consumerTask.getConsumer());
} catch (Throwable e) {
log.error("Failure in consumer loop", e);
}
log.info("[{}] Consumer stopped", consumerTask.getKey());
});
consumerTask.setTask(consumerLoop);
}
private void consumerLoop(TbQueueConsumer<M> consumer) {
while (!stopped && !consumer.isStopped()) {
try {
List<M> msgs = consumer.poll(config.getPollInterval());
if (msgs.isEmpty()) {
continue;
}
processMsgs(msgs, consumer, config);
} catch (Exception e) {
if (!consumer.isStopped()) {
log.warn("Failed to process messages from queue", e);
try {
Thread.sleep(config.getPollInterval());
} catch (InterruptedException e2) {
log.trace("Failed to wait until the server has capacity to handle new requests", e2);
}
}
}
}
if (consumer.isStopped()) {
consumer.unsubscribe();
}
}
protected void processMsgs(List<M> msgs, TbQueueConsumer<M> consumer, C config) throws Exception {
msgPackProcessor.process(msgs, consumer, config);
}
public void stop() {
log.debug("[{}] Stopping consumers", queueKey);
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop);
stopped = true;
}
public void awaitStop() {
log.debug("[{}] Waiting for consumers to stop", queueKey);
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion);
log.debug("[{}] Unsubscribed and stopped consumers", queueKey);
}
private static String partitionsToString(Collection<TopicPartitionInfo> partitions) {
return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.joining(", ", "[", "]"));
}
public interface MsgPackProcessor<M extends TbQueueMsg, C extends QueueConfig> {
void process(List<M> msgs, TbQueueConsumer<M> consumer, C config) throws Exception;
}
public interface ConsumerWrapper<M extends TbQueueMsg> {
void updatePartitions(Set<TopicPartitionInfo> partitions);
Collection<TbQueueConsumerTask<M>> getConsumers();
}
class ConsumerPerPartitionWrapper implements ConsumerWrapper<M> {
private final Map<TopicPartitionInfo, TbQueueConsumerTask<M>> consumers = new HashMap<>();
@Override
public void updatePartitions(Set<TopicPartitionInfo> partitions) {
Set<TopicPartitionInfo> addedPartitions = new HashSet<>(partitions);
addedPartitions.removeAll(consumers.keySet());
Set<TopicPartitionInfo> removedPartitions = new HashSet<>(consumers.keySet());
removedPartitions.removeAll(partitions);
log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, partitionsToString(addedPartitions), partitionsToString(removedPartitions));
removedPartitions.forEach((tpi) -> consumers.get(tpi).initiateStop());
removedPartitions.forEach((tpi) -> consumers.remove(tpi).awaitCompletion());
addedPartitions.forEach((tpi) -> {
String key = queueKey + "-" + tpi.getPartition().orElse(-1);
TbQueueConsumerTask<M> consumer = new TbQueueConsumerTask<>(key, consumerCreator.apply(config));
consumers.put(tpi, consumer);
consumer.subscribe(Set.of(tpi));
launchConsumer(consumer);
});
}
@Override
public Collection<TbQueueConsumerTask<M>> getConsumers() {
return consumers.values();
}
}
class SingleConsumerWrapper implements ConsumerWrapper<M> {
private TbQueueConsumerTask<M> consumer;
@Override
public void updatePartitions(Set<TopicPartitionInfo> partitions) {
log.info("[{}] New partitions: {}", queueKey, partitionsToString(partitions));
if (partitions.isEmpty()) {
if (consumer != null && consumer.isRunning()) {
consumer.initiateStop();
consumer.awaitCompletion();
}
consumer = null;
return;
}
if (consumer == null) {
consumer = new TbQueueConsumerTask<>(queueKey, consumerCreator.apply(config));
}
consumer.subscribe(partitions);
if (!consumer.isRunning()) {
launchConsumer(consumer);
}
}
@Override
public Collection<TbQueueConsumerTask<M>> getConsumers() {
if (consumer == null) {
return Collections.emptyList();
}
return List.of(consumer);
}
}
}

133
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java

@ -15,10 +15,11 @@
*/
package org.thingsboard.server.service.queue.processing;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.EntityType;
@ -36,6 +37,7 @@ import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.consumer.QueueConsumerManager;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
@ -47,9 +49,6 @@ import org.thingsboard.server.service.queue.TbPackCallback;
import org.thingsboard.server.service.queue.TbPackProcessingContext;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import jakarta.annotation.PreDestroy;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@ -57,6 +56,7 @@ import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -64,9 +64,6 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public abstract class AbstractConsumerService<N extends com.google.protobuf.GeneratedMessageV3> extends TbApplicationEventListener<PartitionChangeEvent> {
protected volatile ExecutorService notificationsConsumerExecutor;
protected volatile boolean stopped = false;
protected volatile boolean isReady = false;
protected final ActorSystemContext actorContext;
protected final TbTenantProfileCache tenantProfileCache;
protected final TbDeviceProfileCache deviceProfileCache;
@ -74,21 +71,37 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected final TbApiUsageStateService apiUsageStateService;
protected final PartitionService partitionService;
protected final ApplicationEventPublisher eventPublisher;
protected final TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer;
protected final JwtSettingsService jwtSettingsService;
public void init(String nfConsumerThreadName) {
this.notificationsConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(nfConsumerThreadName));
protected QueueConsumerManager<TbProtoQueueMsg<N>> nfConsumer;
protected ExecutorService consumersExecutor;
protected ExecutorService mgmtExecutor;
protected ScheduledExecutorService scheduler;
public void init(String prefix) {
this.consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(prefix + "-consumer"));
this.mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(getMgmtThreadPoolSize(), prefix + "-mgmt");
this.scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName(prefix + "-consumer-scheduler"));
this.nfConsumer = QueueConsumerManager.<TbProtoQueueMsg<N>>builder()
.name(getServiceType().getLabel() + " Notifications")
.msgPackProcessor(this::processNotifications)
.pollInterval(getNotificationPollDuration())
.consumerCreator(this::createNotificationsConsumer)
.consumerExecutor(consumersExecutor)
.threadPrefix("notifications")
.build();
}
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
public void onApplicationEvent(ApplicationReadyEvent event) {
log.info("Subscribing to notifications: {}", nfConsumer.getTopic());
this.nfConsumer.subscribe();
this.isReady = true;
launchNotificationsConsumer();
launchMainConsumers();
public void afterStartUp() {
startConsumers();
}
protected void startConsumers() {
nfConsumer.subscribe();
nfConsumer.launch();
}
@Override
@ -98,58 +111,42 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected abstract ServiceType getServiceType();
protected abstract void launchMainConsumers();
protected abstract void stopConsumers();
protected void stopConsumers() {
nfConsumer.stop();
}
protected abstract long getNotificationPollDuration();
protected abstract long getNotificationPackProcessingTimeout();
protected void launchNotificationsConsumer() {
notificationsConsumerExecutor.submit(() -> {
while (!stopped) {
try {
List<TbProtoQueueMsg<N>> msgs = nfConsumer.poll(getNotificationPollDuration());
if (msgs.isEmpty()) {
continue;
}
List<IdMsgPair<N>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList());
ConcurrentMap<UUID, TbProtoQueueMsg<N>> pendingMap = orderedMsgList.stream().collect(
Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg));
CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
TbPackProcessingContext<TbProtoQueueMsg<N>> ctx = new TbPackProcessingContext<>(
processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
orderedMsgList.forEach(element -> {
UUID id = element.getUuid();
TbProtoQueueMsg<N> msg = element.getMsg();
log.trace("[{}] Creating notification callback for message: {}", id, msg.getValue());
TbCallback callback = new TbPackCallback<>(id, ctx);
try {
handleNotification(id, msg, callback);
} catch (Throwable e) {
log.warn("[{}] Failed to process notification: {}", id, msg, e);
callback.onFailure(e);
}
});
if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) {
ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process notification: {}", id, msg.getValue()));
ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process notification: {}", id, msg.getValue()));
}
nfConsumer.commit();
} catch (Exception e) {
if (!stopped) {
log.warn("Failed to obtain notifications from queue.", e);
try {
Thread.sleep(getNotificationPollDuration());
} catch (InterruptedException e2) {
log.trace("Failed to wait until the server has capacity to handle new notifications", e2);
}
}
}
protected abstract int getMgmtThreadPoolSize();
protected abstract TbQueueConsumer<TbProtoQueueMsg<N>> createNotificationsConsumer();
protected void processNotifications(List<TbProtoQueueMsg<N>> msgs, TbQueueConsumer<TbProtoQueueMsg<N>> consumer) throws Exception {
List<IdMsgPair<N>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList());
ConcurrentMap<UUID, TbProtoQueueMsg<N>> pendingMap = orderedMsgList.stream().collect(
Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg));
CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
TbPackProcessingContext<TbProtoQueueMsg<N>> ctx = new TbPackProcessingContext<>(
processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
orderedMsgList.forEach(element -> {
UUID id = element.getUuid();
TbProtoQueueMsg<N> msg = element.getMsg();
log.trace("[{}] Creating notification callback for message: {}", id, msg.getValue());
TbCallback callback = new TbPackCallback<>(id, ctx);
try {
handleNotification(id, msg, callback);
} catch (Throwable e) {
log.warn("[{}] Failed to process notification: {}", id, msg, e);
callback.onFailure(e);
}
log.info("TB Notifications Consumer stopped.");
});
if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) {
ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process notification: {}", id, msg.getValue()));
ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process notification: {}", id, msg.getValue()));
}
consumer.commit();
}
protected final void handleComponentLifecycleMsg(UUID id, ComponentLifecycleMsg componentLifecycleMsg) {
@ -203,13 +200,15 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
@PreDestroy
public void destroy() {
stopped = true;
stopConsumers();
if (nfConsumer != null) {
nfConsumer.unsubscribe();
if (consumersExecutor != null) {
consumersExecutor.shutdownNow();
}
if (mgmtExecutor != null) {
mgmtExecutor.shutdownNow();
}
if (notificationsConsumerExecutor != null) {
notificationsConsumerExecutor.shutdownNow();
if (scheduler != null) {
scheduler.shutdownNow();
}
}
}

8
application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerManagerTask.java

@ -18,7 +18,7 @@ package org.thingsboard.server.service.queue.ruleengine;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.queue.QueueConfig;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import java.util.Set;
@ -29,7 +29,7 @@ import java.util.Set;
public class TbQueueConsumerManagerTask {
private final QueueEvent event;
private Queue queue;
private QueueConfig config;
private Set<TopicPartitionInfo> partitions;
private boolean drainQueue;
@ -37,8 +37,8 @@ public class TbQueueConsumerManagerTask {
return new TbQueueConsumerManagerTask(QueueEvent.DELETE, null, null, drainQueue);
}
public static TbQueueConsumerManagerTask configUpdate(Queue queue) {
return new TbQueueConsumerManagerTask(QueueEvent.CONFIG_UPDATE, queue, null, false);
public static TbQueueConsumerManagerTask configUpdate(QueueConfig config) {
return new TbQueueConsumerManagerTask(QueueEvent.CONFIG_UPDATE, config, null, false);
}
public static TbQueueConsumerManagerTask partitionChange(Set<TopicPartitionInfo> partitions) {

7
application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java

@ -20,9 +20,8 @@ import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.TbQueueMsg;
import java.util.Set;
import java.util.concurrent.Future;
@ -30,12 +29,12 @@ import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
@Slf4j
public class TbQueueConsumerTask {
public class TbQueueConsumerTask<M extends TbQueueMsg> {
@Getter
private final Object key;
@Getter
private final TbQueueConsumer<TbProtoQueueMsg<TransportProtos.ToRuleEngineMsg>> consumer;
private final TbQueueConsumer<M> consumer;
@Setter
private Future<?> task;

25
application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineConsumerContext.java

@ -19,8 +19,6 @@ import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.queue.TbQueueAdmin;
@ -33,11 +31,6 @@ import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStr
import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory;
import org.thingsboard.server.service.stats.RuleEngineStatisticsService;
import javax.annotation.PostConstruct;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@Component
@TbRuleEngineComponent
@Slf4j
@ -68,22 +61,4 @@ public class TbRuleEngineConsumerContext {
private final TbQueueProducerProvider producerProvider;
private final TbQueueAdmin queueAdmin;
private ExecutorService consumersExecutor;
private ExecutorService mgmtExecutor;
private ScheduledExecutorService scheduler;
private volatile boolean isReady = false;
@PostConstruct
void init() {
this.consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer"));
this.mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(mgmtThreadPoolSize, "tb-rule-engine-mgmt");
this.scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer-scheduler"));
}
public void stop() {
scheduler.shutdownNow();
consumersExecutor.shutdownNow();
mgmtExecutor.shutdownNow();
}
}

290
application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java

@ -16,9 +16,8 @@
package org.thingsboard.server.service.queue.ruleengine;
import com.google.protobuf.ProtocolStringList;
import lombok.Getter;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
@ -38,173 +37,54 @@ import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.service.queue.TbMsgPackCallback;
import org.thingsboard.server.service.queue.TbMsgPackProcessingContext;
import org.thingsboard.server.service.queue.TbRuleEngineConsumerStats;
import org.thingsboard.server.service.queue.consumer.MainQueueConsumerManager;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingDecision;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategy;
import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
@Slf4j
public class TbRuleEngineQueueConsumerManager {
public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager<TbProtoQueueMsg<ToRuleEngineMsg>, Queue> {
public static final String SUCCESSFUL_STATUS = "successful";
public static final String FAILED_STATUS = "failed";
private final TbRuleEngineConsumerContext ctx;
private final QueueKey queueKey;
private final TbRuleEngineConsumerStats stats;
private final ReentrantLock lock = new ReentrantLock(); //NonfairSync
@Getter
private volatile Queue queue;
@Getter
private volatile Set<TopicPartitionInfo> partitions;
private volatile ConsumerWrapper consumerWrapper;
private volatile boolean stopped;
private final java.util.Queue<TbQueueConsumerManagerTask> tasks = new ConcurrentLinkedQueue<>();
public TbRuleEngineQueueConsumerManager(TbRuleEngineConsumerContext ctx, QueueKey queueKey) {
@Builder(builderMethodName = "create") // not to conflict with super.builder()
public TbRuleEngineQueueConsumerManager(TbRuleEngineConsumerContext ctx,
QueueKey queueKey,
ExecutorService consumerExecutor,
ScheduledExecutorService scheduler,
ExecutorService taskExecutor) {
super(queueKey, null, null, ctx.getQueueFactory()::createToRuleEngineMsgConsumer, consumerExecutor, scheduler, taskExecutor);
this.ctx = ctx;
this.queueKey = queueKey;
this.stats = new TbRuleEngineConsumerStats(queueKey, ctx.getStatsFactory());
}
public void init(Queue queue) {
this.queue = queue;
if (queue.isConsumerPerPartition()) {
this.consumerWrapper = new ConsumerPerPartitionWrapper();
} else {
this.consumerWrapper = new SingleConsumerWrapper();
}
log.debug("[{}] Initialized consumer for queue: {}", queueKey, queue);
}
public void update(Queue queue) {
addTask(TbQueueConsumerManagerTask.configUpdate(queue));
}
public void update(Set<TopicPartitionInfo> partitions) {
addTask(TbQueueConsumerManagerTask.partitionChange(partitions));
}
public void delete(boolean drainQueue) {
addTask(TbQueueConsumerManagerTask.delete(drainQueue));
}
private void addTask(TbQueueConsumerManagerTask todo) {
if (stopped) {
return;
}
tasks.add(todo);
log.trace("[{}] Added task: {}", queueKey, todo);
tryProcessTasks();
}
private void tryProcessTasks() {
if (!ctx.isReady()) {
log.debug("[{}] TbRuleEngineConsumerContext is not ready yet, will process tasks later", queueKey);
ctx.getScheduler().schedule(this::tryProcessTasks, 1, TimeUnit.SECONDS);
return;
}
ctx.getMgmtExecutor().submit(() -> {
if (lock.tryLock()) {
try {
Queue newConfiguration = null;
Set<TopicPartitionInfo> newPartitions = null;
while (!stopped) {
TbQueueConsumerManagerTask task = tasks.poll();
if (task == null) {
break;
}
log.trace("[{}] Processing task: {}", queueKey, task);
if (task.getEvent() == QueueEvent.PARTITION_CHANGE) {
newPartitions = task.getPartitions();
} else if (task.getEvent() == QueueEvent.CONFIG_UPDATE) {
newConfiguration = task.getQueue();
} else if (task.getEvent() == QueueEvent.DELETE) {
doDelete(task.isDrainQueue());
return;
}
}
if (stopped) {
return;
}
if (newConfiguration != null) {
doUpdate(newConfiguration);
}
if (newPartitions != null) {
doUpdate(newPartitions);
}
} catch (Exception e) {
log.error("[{}] Failed to process tasks", queueKey, e);
} finally {
lock.unlock();
}
} else {
log.trace("[{}] Failed to acquire lock", queueKey);
ctx.getScheduler().schedule(this::tryProcessTasks, 1, TimeUnit.SECONDS);
}
});
}
private void doUpdate(Queue newQueue) {
log.info("[{}] Processing queue update: {}", queueKey, newQueue);
var oldQueue = this.queue;
this.queue = newQueue;
if (log.isTraceEnabled()) {
log.trace("[{}] Old queue configuration: {}", queueKey, oldQueue);
log.trace("[{}] New queue configuration: {}", queueKey, newQueue);
}
if (oldQueue == null) {
init(queue);
} else if (newQueue.isConsumerPerPartition() != oldQueue.isConsumerPerPartition()) {
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop);
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion);
init(queue);
if (partitions != null) {
doUpdate(partitions); // even if partitions number was changed, there can be no partition change event
}
} else {
// do nothing, because partitions change (if they changed) will be handled on PartitionChangeEvent,
// and changes to pollInterval/packProcessingTimeout/submitStrategy/processingStrategy will be picked up by consumer on the fly,
// and queue topic and name are immutable
@Override
protected void processTask(TbQueueConsumerManagerTask task) {
if (task.getEvent() == QueueEvent.DELETE) {
doDelete(task.isDrainQueue());
}
}
private void doUpdate(Set<TopicPartitionInfo> partitions) {
this.partitions = partitions;
consumerWrapper.updatePartitions(partitions);
}
public void stop() {
log.debug("[{}] Stopping consumers", queueKey);
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop);
stopped = true;
}
public void awaitStop() {
consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion);
log.debug("[{}] Unsubscribed and stopped consumers", queueKey);
}
private void doDelete(boolean drainQueue) {
stopped = true;
log.info("[{}] Handling queue deletion", queueKey);
@ -212,7 +92,7 @@ public class TbRuleEngineQueueConsumerManager {
List<TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>>> queueConsumers = consumerWrapper.getConsumers().stream()
.map(TbQueueConsumerTask::getConsumer).collect(Collectors.toList());
ctx.getConsumersExecutor().submit(() -> {
consumerExecutor.submit(() -> {
if (drainQueue) {
drainQueue(queueConsumers);
}
@ -235,47 +115,10 @@ public class TbRuleEngineQueueConsumerManager {
});
}
private void launchConsumer(TbQueueConsumerTask consumerTask) {
log.info("[{}] Launching consumer", consumerTask.getKey());
Future<?> consumerLoop = ctx.getConsumersExecutor().submit(() -> {
ThingsBoardThreadFactory.updateCurrentThreadName(consumerTask.getKey().toString());
try {
consumerLoop(consumerTask.getConsumer());
} catch (Throwable e) {
log.error("Failure in consumer loop", e);
}
});
consumerTask.setTask(consumerLoop);
}
private void consumerLoop(TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer) {
while (!stopped && !consumer.isStopped()) {
try {
List<TbProtoQueueMsg<ToRuleEngineMsg>> msgs = consumer.poll(queue.getPollInterval());
if (msgs.isEmpty()) {
continue;
}
processMsgs(msgs, consumer, queue);
} catch (Exception e) {
if (!consumer.isStopped()) {
log.warn("Failed to process messages from queue", e);
try {
Thread.sleep(ctx.getPollDuration());
} catch (InterruptedException e2) {
log.trace("Failed to wait until the server has capacity to handle new requests", e2);
}
}
}
}
if (consumer.isStopped()) {
consumer.unsubscribe();
}
log.info("Rule Engine consumer stopped");
}
private void processMsgs(List<TbProtoQueueMsg<ToRuleEngineMsg>> msgs,
TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer,
Queue queue) throws InterruptedException {
@Override
protected void processMsgs(List<TbProtoQueueMsg<ToRuleEngineMsg>> msgs,
TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer,
Queue queue) throws Exception {
TbRuleEngineSubmitStrategy submitStrategy = getSubmitStrategy(queue);
TbRuleEngineProcessingStrategy ackStrategy = getProcessingStrategy(queue);
submitStrategy.init(msgs);
@ -320,7 +163,7 @@ public class TbRuleEngineQueueConsumerManager {
}
private void submitMessage(TbMsgPackProcessingContext packCtx, UUID id, TbProtoQueueMsg<ToRuleEngineMsg> msg) {
log.trace("[{}] Creating callback for topic {} message: {}", id, queue.getName(), msg.getValue());
log.trace("[{}] Creating callback for topic {} message: {}", id, config.getName(), msg.getValue());
ToRuleEngineMsg toRuleEngineMsg = msg.getValue();
TenantId tenantId = TenantId.fromUUID(new UUID(toRuleEngineMsg.getTenantIdMSB(), toRuleEngineMsg.getTenantIdLSB()));
TbMsgCallback callback = ctx.isPrometheusStatsEnabled() ?
@ -328,7 +171,7 @@ public class TbRuleEngineQueueConsumerManager {
new TbMsgPackCallback(id, tenantId, packCtx);
try {
if (!toRuleEngineMsg.getTbMsg().isEmpty()) {
forwardToRuleEngineActor(queue.getName(), tenantId, toRuleEngineMsg, callback);
forwardToRuleEngineActor(config.getName(), tenantId, toRuleEngineMsg, callback);
} else {
callback.onSuccess();
}
@ -356,7 +199,7 @@ public class TbRuleEngineQueueConsumerManager {
log.info("[{}] {} to process [{}] messages", queueKey, prefix, map.size());
for (Map.Entry<UUID, TbProtoQueueMsg<ToRuleEngineMsg>> pending : map.entrySet()) {
ToRuleEngineMsg tmp = pending.getValue().getValue();
TbMsg tmpMsg = TbMsg.fromBytes(queue.getName(), tmp.getTbMsg().toByteArray(), TbMsgCallback.EMPTY);
TbMsg tmpMsg = TbMsg.fromBytes(config.getName(), tmp.getTbMsg().toByteArray(), TbMsgCallback.EMPTY);
RuleNodeInfo ruleNodeInfo = ctx.getLastVisitedRuleNode(pending.getKey());
if (printAll) {
log.trace("[{}][{}] {} to process message: {}, Last Rule Node: {}", queueKey, TenantId.fromUUID(new UUID(tmp.getTenantIdMSB(), tmp.getTenantIdLSB())), prefix, tmpMsg, ruleNodeInfo);
@ -379,7 +222,7 @@ public class TbRuleEngineQueueConsumerManager {
int n = 0;
while (System.currentTimeMillis() <= finishTs) {
for (TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer : consumers) {
List<TbProtoQueueMsg<ToRuleEngineMsg>> msgs = consumer.poll(queue.getPollInterval());
List<TbProtoQueueMsg<ToRuleEngineMsg>> msgs = consumer.poll(config.getPollInterval());
if (msgs.isEmpty()) {
continue;
}
@ -388,7 +231,7 @@ public class TbRuleEngineQueueConsumerManager {
MsgProtos.TbMsgProto tbMsgProto = MsgProtos.TbMsgProto.parseFrom(msg.getValue().getTbMsg().toByteArray());
EntityId originator = EntityIdFactory.getByTypeAndUuid(tbMsgProto.getEntityType(), new UUID(tbMsgProto.getEntityIdMSB(), tbMsgProto.getEntityIdLSB()));
TopicPartitionInfo tpi = ctx.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, queue.getName(), TenantId.SYS_TENANT_ID, originator);
TopicPartitionInfo tpi = ctx.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, config.getName(), TenantId.SYS_TENANT_ID, originator);
ctx.getProducerProvider().getRuleEngineMsgProducer().send(tpi, msg, null);
n++;
} catch (Throwable e) {
@ -399,90 +242,11 @@ public class TbRuleEngineQueueConsumerManager {
}
}
if (n > 0) {
log.info("Moved {} messages from {} to system {}", n, queueKey, queue.getName());
log.info("Moved {} messages from {} to system {}", n, queueKey, config.getName());
}
} catch (Exception e) {
log.error("[{}] Failed to drain queue", queueKey, e);
}
}
private static String partitionsToString(Collection<TopicPartitionInfo> partitions) {
return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.joining(", ", "[", "]"));
}
interface ConsumerWrapper {
void updatePartitions(Set<TopicPartitionInfo> partitions);
Collection<TbQueueConsumerTask> getConsumers();
}
class ConsumerPerPartitionWrapper implements ConsumerWrapper {
private final Map<TopicPartitionInfo, TbQueueConsumerTask> consumers = new HashMap<>();
@Override
public void updatePartitions(Set<TopicPartitionInfo> partitions) {
Set<TopicPartitionInfo> addedPartitions = new HashSet<>(partitions);
addedPartitions.removeAll(consumers.keySet());
Set<TopicPartitionInfo> removedPartitions = new HashSet<>(consumers.keySet());
removedPartitions.removeAll(partitions);
log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, partitionsToString(addedPartitions), partitionsToString(removedPartitions));
removedPartitions.forEach((tpi) -> {
consumers.get(tpi).initiateStop();
});
removedPartitions.forEach((tpi) -> {
consumers.remove(tpi).awaitCompletion();
});
addedPartitions.forEach((tpi) -> {
String key = queueKey + "-" + tpi.getPartition().orElse(-999999);
TbQueueConsumerTask consumer = new TbQueueConsumerTask(key, ctx.getQueueFactory().createToRuleEngineMsgConsumer(queue));
consumers.put(tpi, consumer);
consumer.subscribe(Set.of(tpi));
launchConsumer(consumer);
});
}
@Override
public Collection<TbQueueConsumerTask> getConsumers() {
return consumers.values();
}
}
class SingleConsumerWrapper implements ConsumerWrapper {
private TbQueueConsumerTask consumer;
@Override
public void updatePartitions(Set<TopicPartitionInfo> partitions) {
log.info("[{}] New partitions: {}", queueKey, partitionsToString(partitions));
if (partitions.isEmpty()) {
if (consumer != null && consumer.isRunning()) {
consumer.initiateStop();
consumer.awaitCompletion();
}
consumer = null;
return;
}
if (consumer == null) {
consumer = new TbQueueConsumerTask(queueKey, ctx.getQueueFactory().createToRuleEngineMsgConsumer(queue));
}
consumer.subscribe(partitions);
if (!consumer.isRunning()) {
launchConsumer(consumer);
}
}
@Override
public Collection<TbQueueConsumerTask> getConsumers() {
if (consumer == null) {
return Collections.emptyList();
}
return List.of(consumer);
}
}
}

2
application/src/main/resources/thingsboard.yml

@ -1631,6 +1631,8 @@ queue:
partitions: "${TB_QUEUE_CORE_PARTITIONS:10}"
# Timeout for processing a message pack by Core microservices
pack-processing-timeout: "${TB_QUEUE_CORE_PACK_PROCESSING_TIMEOUT_MS:2000}"
# Enable/disable a separate consumer per partition for Core queue
consumer-per-partition: "${TB_QUEUE_CORE_CONSUMER_PER_PARTITION:true}"
ota:
# Default topic name for OTA updates
topic: "${TB_QUEUE_CORE_OTA_TOPIC:tb_ota_package}"

60
application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java

@ -27,6 +27,8 @@ import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.QueueId;
@ -65,6 +67,9 @@ import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
@ -86,7 +91,6 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@ -112,6 +116,9 @@ public class TbRuleEngineQueueConsumerManagerTest {
@Mock
private TbQueueAdmin queueAdmin;
private TbRuleEngineConsumerContext ruleEngineConsumerContext;
private ExecutorService consumersExecutor;
private ScheduledExecutorService scheduler;
private ExecutorService mgmtExecutor;
private TbRuleEngineQueueConsumerManager consumerManager;
private Queue queue;
@ -141,10 +148,10 @@ public class TbRuleEngineQueueConsumerManagerTest {
}).when(actorContext).tell(any());
ruleEngineMsgProducer = mock(TbQueueProducer.class);
when(producerProvider.getRuleEngineMsgProducer()).thenReturn(ruleEngineMsgProducer);
ruleEngineConsumerContext.setMgmtThreadPoolSize(2);
consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer"));
mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(3, "tb-rule-engine-mgmt");
scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer-scheduler"));
ruleEngineConsumerContext.setTopicDeletionDelayInSec(5);
ruleEngineConsumerContext.init();
ruleEngineConsumerContext.setReady(false);
queue = new Queue();
queue.setName("Test");
@ -174,14 +181,23 @@ public class TbRuleEngineQueueConsumerManagerTest {
}).when(queueFactory).createToRuleEngineMsgConsumer(any());
QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue);
consumerManager = new TbRuleEngineQueueConsumerManager(ruleEngineConsumerContext, queueKey);
consumerManager = TbRuleEngineQueueConsumerManager.create()
.ctx(ruleEngineConsumerContext)
.queueKey(queueKey)
.consumerExecutor(consumersExecutor)
.scheduler(scheduler)
.taskExecutor(mgmtExecutor)
.build();
}
@After
public void afterEach() {
consumerManager.stop();
consumerManager.awaitStop();
ruleEngineConsumerContext.stop();
consumersExecutor.shutdownNow();
scheduler.shutdownNow();
mgmtExecutor.shutdownNow();
if (generateQueueMsgs) {
await().atMost(10, TimeUnit.SECONDS)
@ -199,14 +215,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
Set<TopicPartitionInfo> partitions = createTpis(2, 3, 4);
consumerManager.update(partitions);
partitions = createTpis(3, 4, 5);
consumerManager.update(partitions);
partitions = createTpis(1, 2, 3);
consumerManager.update(partitions);
// simulated multiple partition change events before consumer is ready; only latest partitions should be processed
verifyNoInteractions(queueFactory);
ruleEngineConsumerContext.setReady(true);
await().atMost(2, TimeUnit.SECONDS)
.until(() -> consumers.size() == 3);
for (TopicPartitionInfo partition : partitions) {
@ -222,14 +231,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
Set<TopicPartitionInfo> partitions = createTpis(2, 3, 4);
consumerManager.update(partitions);
partitions = createTpis(3, 4, 5);
consumerManager.update(partitions);
partitions = createTpis(1, 2, 3);
consumerManager.update(partitions);
verifyNoInteractions(queueFactory);
ruleEngineConsumerContext.setReady(true);
await().atMost(2, TimeUnit.SECONDS)
.until(() -> consumers.size() == 1);
TestConsumer consumer = getConsumer();
@ -240,7 +242,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testPartitionsUpdate_singleConsumer() {
queue.setConsumerPerPartition(false);
consumerManager.init(queue);
ruleEngineConsumerContext.setReady(true);
Set<TopicPartitionInfo> partitions = Collections.emptySet();
consumerManager.update(partitions);
@ -273,7 +274,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testPartitionsUpdate_consumerPerPartition() {
queue.setConsumerPerPartition(true);
consumerManager.init(queue);
ruleEngineConsumerContext.setReady(true);
consumerManager.update(Collections.emptySet());
verify(queueFactory, after(1000).never()).createToRuleEngineMsgConsumer(any());
@ -316,7 +316,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testConfigUpdate_singleConsumer() {
queue.setConsumerPerPartition(false);
consumerManager.init(queue);
ruleEngineConsumerContext.setReady(true);
Set<TopicPartitionInfo> partitions = createTpis(1, 2, 3);
consumerManager.update(partitions);
TestConsumer consumer = getConsumer();
@ -342,7 +341,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testConfigUpdate_consumerPerPartition() {
queue.setConsumerPerPartition(true);
consumerManager.init(queue);
ruleEngineConsumerContext.setReady(true);
Set<TopicPartitionInfo> partitions = createTpis(1, 2, 3);
consumerManager.update(partitions);
TestConsumer consumer1 = getConsumer(1);
@ -375,7 +373,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testConfigUpdate_fromSingleToConsumerPerPartition() {
queue.setConsumerPerPartition(false);
consumerManager.init(queue);
ruleEngineConsumerContext.setReady(true);
Set<TopicPartitionInfo> partitions = createTpis(1, 2, 3);
consumerManager.update(partitions);
TestConsumer consumer = getConsumer();
@ -395,7 +392,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testConfigUpdate_fromConsumerPerPartitionToSingle() {
queue.setConsumerPerPartition(true);
consumerManager.init(queue);
ruleEngineConsumerContext.setReady(true);
Set<TopicPartitionInfo> partitions = createTpis(1, 2, 3);
consumerManager.update(partitions);
TestConsumer consumer1 = getConsumer(1);
@ -419,7 +415,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testStop() {
queue.setConsumerPerPartition(true);
consumerManager.init(queue);
ruleEngineConsumerContext.setReady(true);
consumerManager.update(createTpis(1));
TestConsumer consumer = getConsumer(1);
verifySubscribedAndLaunched(consumer, 1);
@ -437,7 +432,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testDelete_consumerPerPartition() {
queue.setConsumerPerPartition(true);
consumerManager.init(queue);
ruleEngineConsumerContext.setReady(true);
Set<TopicPartitionInfo> partitions = createTpis(1, 2);
consumerManager.update(partitions);
TestConsumer consumer1 = getConsumer(1);
@ -459,7 +453,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
int msgCount = totalConsumedMsgs.get();
await().atLeast(2, TimeUnit.SECONDS) // based on topicDeletionDelayInSec(5) = 5 - ( 3 seconds the code may execute starting consumerManager.delete() call)
.atMost(7, TimeUnit.SECONDS)
.atMost(20, TimeUnit.SECONDS)
.untilAsserted(() -> {
partitions.stream()
.map(TopicPartitionInfo::getFullTopicName)
@ -481,7 +475,6 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testDelete_singleConsumer() {
queue.setConsumerPerPartition(false);
consumerManager.init(queue);
ruleEngineConsumerContext.setReady(true);
Set<TopicPartitionInfo> partitions = createTpis(1, 2);
consumerManager.update(partitions);
TestConsumer consumer = getConsumer();
@ -499,7 +492,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
int msgCount = totalConsumedMsgs.get();
await().atLeast(2, TimeUnit.SECONDS) // based on topicDeletionDelayInSec(5) = 5 - ( 3 seconds the code may execute starting consumerManager.delete() call)
.atMost(7, TimeUnit.SECONDS)
.atMost(20, TimeUnit.SECONDS)
.untilAsserted(() -> {
partitions.stream()
.map(TopicPartitionInfo::getFullTopicName)
@ -520,10 +513,9 @@ public class TbRuleEngineQueueConsumerManagerTest {
public void testManyDifferentUpdates() throws Exception {
queue.setConsumerPerPartition(RandomUtils.nextBoolean());
consumerManager.init(queue);
ruleEngineConsumerContext.setReady(true);
Supplier<Queue> queueConfigUpdater = () -> {
Queue oldConfig = consumerManager.getQueue();
Queue oldConfig = consumerManager.getConfig();
Queue newConfig = JacksonUtil.clone(oldConfig);
newConfig.setConsumerPerPartition(RandomUtils.nextBoolean());
newConfig.setPollInterval(RandomUtils.nextInt(100, 501));
@ -571,7 +563,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
Set<TopicPartitionInfo> expectedPartitions = latestPartitions;
await().atMost(5, TimeUnit.SECONDS)
.untilAsserted(() -> {
assertThat(consumerManager.getQueue()).isEqualTo(expectedConfig);
assertThat(consumerManager.getConfig()).isEqualTo(expectedConfig);
assertThat(consumerManager.getPartitions()).isEqualTo(expectedPartitions);
});

1
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java

@ -39,4 +39,5 @@ public class OtherConfiguration extends PowerSavingConfiguration {
private Long pagingTransmissionWindow;
private String fwUpdateResource;
private String swUpdateResource;
private String defaultObjectIDVer;
}

2
common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java

@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
import java.util.Optional;
@Data
public class Queue extends BaseDataWithAdditionalInfo<QueueId> implements HasName, HasTenantId {
public class Queue extends BaseDataWithAdditionalInfo<QueueId> implements HasName, HasTenantId, QueueConfig {
private TenantId tenantId;
@NoXss
@Length(fieldName = "name")

24
common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueConfig.java

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.queue;
public interface QueueConfig {
boolean isConsumerPerPartition();
int getPollInterval();
}

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

@ -59,7 +59,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private long maxJSExecutions;
private long maxTbelExecutions;
private long maxDPStorageDays;
private int maxRuleNodeExecutionsPerMessage = 1000;
private int maxRuleNodeExecutionsPerMessage;
private long maxEmails;
private Boolean smsEnabled;
private long maxSms;

14
common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java

@ -15,11 +15,23 @@
*/
package org.thingsboard.server.common.msg.queue;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public enum ServiceType {
TB_CORE, TB_RULE_ENGINE, TB_TRANSPORT, JS_EXECUTOR, TB_VC_EXECUTOR;
TB_CORE("TB Core"),
TB_RULE_ENGINE("TB Rule Engine"),
TB_TRANSPORT("TB Transport"),
JS_EXECUTOR("JS Executor"),
TB_VC_EXECUTOR("TB VC Executor");
private final String label;
public static ServiceType of(String serviceType) {
return ServiceType.valueOf(serviceType.replace("-", "_").toUpperCase());
}
}

5
application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java → common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.queue.consumer;
package org.thingsboard.server.queue.common.consumer;
import lombok.Builder;
import lombok.Getter;
@ -106,4 +106,5 @@ public class QueueConsumerManager<M extends TbQueueMsg> {
public interface MsgPackProcessor<M extends TbQueueMsg> {
void process(List<M> msgs, TbQueueConsumer<M> consumer) throws Exception;
}
}
}

2
common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java

@ -217,7 +217,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder<TbProtoQueueMsg<ToCoreMsg>> consumerBuilder = TbKafkaConsumerTemplate.builder();
consumerBuilder.settings(kafkaSettings);
consumerBuilder.topic(topicService.buildTopicName(coreSettings.getTopic()));
consumerBuilder.clientId("monolith-core-consumer-" + serviceInfoProvider.getServiceId());
consumerBuilder.clientId("monolith-core-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet());
consumerBuilder.groupId(topicService.buildTopicName("monolith-core-consumer"));
consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders()));
consumerBuilder.admin(coreAdmin);

5
common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java

@ -56,6 +56,7 @@ import org.thingsboard.server.queue.settings.TbQueueVersionControlSettings;
import jakarta.annotation.PreDestroy;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicLong;
@Component
@ConditionalOnExpression("'${queue.type:null}'=='kafka' && '${service.type:null}'=='tb-core'")
@ -84,6 +85,8 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory {
private final TbQueueAdmin housekeeperAdmin;
private final TbQueueAdmin housekeeperReprocessingAdmin;
private final AtomicLong consumerCount = new AtomicLong();
public KafkaTbCoreQueueFactory(TopicService topicService,
TbKafkaSettings kafkaSettings,
TbServiceInfoProvider serviceInfoProvider,
@ -174,7 +177,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory {
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder<TbProtoQueueMsg<ToCoreMsg>> consumerBuilder = TbKafkaConsumerTemplate.builder();
consumerBuilder.settings(kafkaSettings);
consumerBuilder.topic(topicService.buildTopicName(coreSettings.getTopic()));
consumerBuilder.clientId("tb-core-consumer-" + serviceInfoProvider.getServiceId());
consumerBuilder.clientId("tb-core-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet());
consumerBuilder.groupId(topicService.buildTopicName("tb-core-node"));
consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders()));
consumerBuilder.admin(coreAdmin);

6
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java

@ -29,6 +29,7 @@ import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.registration.RegistrationListener;
import org.eclipse.leshan.server.registration.RegistrationUpdate;
import org.eclipse.leshan.server.send.SendListener;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import java.util.Collection;
@ -101,7 +102,10 @@ public class LwM2mServerListener {
@Override
public void onResponse(SingleObservation observation, Registration registration, ObserveResponse response) {
if (registration != null) {
service.onUpdateValueAfterReadResponse(registration, convertObjectIdToVersionedId(observation.getPath().toString(), registration), response);
LwM2mClient lwM2MClient = service.getClientContext().getClientByEndpoint(registration.getEndpoint());
if (lwM2MClient != null) {
service.onUpdateValueAfterReadResponse(registration, convertObjectIdToVersionedId(observation.getPath().toString(), lwM2MClient), response);
}
}
}

8
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java

@ -27,10 +27,10 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
@ -107,7 +107,8 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider {
@Override
public ObjectModel getObjectModel(int objectId) {
String version = String.valueOf(registration.getSupportedVersion(objectId));
LwM2mClient lwM2mClient = lwM2mClientContext.getClientByEndpoint(registration.getEndpoint());
String version = lwM2mClient.getSupportedObjectVersion(objectId).toString();
if (version != null) {
return this.getObjectModelDynamic(objectId, version);
}
@ -116,7 +117,8 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider {
@Override
public Collection<ObjectModel> getObjectModels() {
Map<Integer, LwM2m.Version> supportedObjects = this.registration.getSupportedObject();
LwM2mClient lwM2mClient = lwM2mClientContext.getClientByEndpoint(registration.getEndpoint());
Map<Integer, LwM2m.Version> supportedObjects = lwM2mClient.getSupportedClientObjects();
Collection<ObjectModel> result = new ArrayList<>(supportedObjects.size());
for (Map.Entry<Integer, LwM2m.Version> supportedObject : supportedObjects.entrySet()) {
ObjectModel objectModel = this.getObjectModelDynamic(supportedObject.getKey(), String.valueOf(supportedObject.getValue()));

101
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java

@ -21,6 +21,8 @@ import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.LwM2m;
import org.eclipse.leshan.core.LwM2m.Version;
import org.eclipse.leshan.core.link.Link;
import org.eclipse.leshan.core.link.attributes.Attribute;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
@ -37,6 +39,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto;
@ -61,8 +64,8 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LWM2M_OBJECT_VERSION_DEFAULT;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertMultiResourceValuesFromRpcBody;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertObjectIdToVersionedId;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.equalsResourceTypeGetSimpleName;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.getVerFromPathIdVerOrId;
@ -129,6 +132,12 @@ public class LwM2mClient {
@Setter
private UUID lastSentRpcId;
@Setter
private LwM2m.Version defaultObjectIDVer;
@Getter
private Map<Integer, Version> supportedClientObjects;
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
@ -142,6 +151,7 @@ public class LwM2mClient {
this.state = LwM2MClientState.CREATED;
this.lock = new ReentrantLock();
this.retryAttempts = new AtomicInteger(0);
this.supportedClientObjects = null;
}
public void init(ValidateDeviceCredentialsResponse credentials, UUID sessionId) {
@ -153,12 +163,14 @@ public class LwM2mClient {
this.edrxCycle = credentials.getDeviceInfo().getEdrxCycle();
this.psmActivityTimer = credentials.getDeviceInfo().getPsmActivityTimer();
this.pagingTransmissionWindow = credentials.getDeviceInfo().getPagingTransmissionWindow();
this.defaultObjectIDVer = getObjectIDVerFromDeviceProfile(credentials.getDeviceProfile());
}
public void setRegistration(Registration registration) {
this.registration = registration;
this.clientSupportContentFormats = clientSupportContentFormat(registration);
this.defaultContentFormat = calculateDefaultContentFormat(registration);
this.setSupportedClientObjects();
}
public void lock() {
@ -197,6 +209,18 @@ public class LwM2mClient {
builder.setDeviceType(deviceProfile.getName());
}
private LwM2m.Version getObjectIDVerFromDeviceProfile(DeviceProfile deviceProfile) {
String defaultObjectIdVer = null;
if (deviceProfile != null) {
defaultObjectIdVer = ((Lwm2mDeviceProfileTransportConfiguration) deviceProfile
.getProfileData()
.getTransportConfiguration())
.getClientLwM2mSettings()
.getDefaultObjectIDVer();
}
return new Version(defaultObjectIdVer == null ? LWM2M_OBJECT_VERSION_DEFAULT : defaultObjectIdVer);
}
public void refreshSessionId(String nodeId) {
UUID newId = UUID.randomUUID();
SessionInfoProto.Builder builder = SessionInfoProto.newBuilder().mergeFrom(session);
@ -240,67 +264,28 @@ public class LwM2mClient {
}
}
public Object getResourceValue(String pathRezIdVer, String pathRezId) {
String pathRez = pathRezIdVer == null ? convertObjectIdToVersionedId(pathRezId, this.registration) : pathRezIdVer;
if (this.resources.get(pathRez) != null) {
return this.resources.get(pathRez).getLwM2mResource().getValue();
}
return null;
}
public Object getResourceNameByRezId(String pathRezIdVer, String pathRezId) {
String pathRez = pathRezIdVer == null ? convertObjectIdToVersionedId(pathRezId, this.registration) : pathRezIdVer;
if (this.resources.get(pathRez) != null) {
return this.resources.get(pathRez).getResourceModel().name;
}
return null;
}
public String getRezIdByResourceNameAndObjectInstanceId(String resourceName, String pathObjectInstanceIdVer, LwM2mModelProvider modelProvider) {
LwM2mPath pathIds = getLwM2mPathFromString(pathObjectInstanceIdVer);
if (pathIds.isObjectInstance()) {
Set<Integer> rezIds = modelProvider.getObjectModel(registration)
.getObjectModel(pathIds.getObjectId()).resources.entrySet()
.stream()
.filter(map -> resourceName.equals(map.getValue().name))
.map(map -> map.getKey())
.collect(Collectors.toSet());
return rezIds.size() > 0 ? String.valueOf(rezIds.stream().findFirst().get()) : null;
}
return null;
}
public ResourceModel getResourceModel(String pathIdVer, LwM2mModelProvider modelProvider) {
LwM2mPath pathIds = getLwM2mPathFromString(pathIdVer);
String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId()));
String verSupportedObject = String.valueOf(this.getSupportedObjectVersion(pathIds.getObjectId()));
String verRez = getVerFromPathIdVerOrId(pathIdVer);
return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration)
.getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()) : null;
}
public boolean isResourceMultiInstances(String pathIdVer, LwM2mModelProvider modelProvider) {
var resourceModel = getResourceModel(pathIdVer, modelProvider);
if (resourceModel != null && resourceModel.multiple != null) {
return resourceModel.multiple;
} else {
return false;
}
}
public ObjectModel getObjectModel(String pathIdVer, LwM2mModelProvider modelProvider) {
try {
LwM2mPath pathIds = getLwM2mPathFromString(pathIdVer);
String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId()));
String verSupportedObject = String.valueOf(this.getSupportedObjectVersion(pathIds.getObjectId()));
String verRez = getVerFromPathIdVerOrId(pathIdVer);
return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration)
.getObjectModel(pathIds.getObjectId()) : null;
} catch (Exception e) {
if (registration == null) {
log.error("[{}] Failed Registration is null, GetObjectModelRegistration. ", this.endpoint, e);
} else if (registration.getSupportedObject() == null) {
} else if (this.getSupportedClientObjects() == null) {
log.error("[{}] Failed SupportedObject in Registration, GetObjectModelRegistration.", this.endpoint, e);
} else {
log.error("[{}] Failed ModelProvider.getObjectModel [{}] in Registration. ", this.endpoint, registration.getSupportedObject(), e);
log.error("[{}] Failed ModelProvider.getObjectModel [{}] in Registration. ", this.endpoint, this.getSupportedClientObjects(), e);
}
return null;
}
@ -371,7 +356,7 @@ public class LwM2mClient {
public String isValidObjectVersion(String path) {
LwM2mPath pathIds = getLwM2mPathFromString(path);
LwM2m.Version verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId());
LwM2m.Version verSupportedObject = this.getSupportedObjectVersion(pathIds.getObjectId());
if (verSupportedObject == null) {
return String.format("Specified object id %s absent in the list supported objects of the client or is security object!", pathIds.getObjectId());
} else {
@ -460,5 +445,31 @@ public class LwM2mClient {
return new LwM2mPath(fromVersionedIdToObjectId(path));
}
public LwM2m.Version getDefaultObjectIDVer() {
return this.defaultObjectIDVer == null ? new Version(LWM2M_OBJECT_VERSION_DEFAULT) : this.defaultObjectIDVer;
}
public LwM2m.Version getSupportedObjectVersion(Integer objectid) {
return this.supportedClientObjects.get(objectid);
}
private void setSupportedClientObjects(){
this.supportedClientObjects = new ConcurrentHashMap<>();
for (Link link: this.registration.getSortedObjectLinks()) {
LwM2mPath lwM2mPath = new LwM2mPath(link.getUriReference());
if (lwM2mPath.isObject()) {
LwM2m.Version ver;
if (link.getAttributes().get("ver")!= null) {
ver = (Version) link.getAttributes().get("ver").getValue();
} else {
ver = getDefaultObjectIDVer();
}
this.supportedClientObjects.put(lwM2mPath.getObjectId(), ver);
} else if (lwM2mPath.getObjectId() != null && this.supportedClientObjects.get(lwM2mPath.getObjectId()) == null){
this.supportedClientObjects.put(lwM2mPath.getObjectId(), getDefaultObjectIDVer());
}
}
}
}

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java

@ -396,7 +396,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
Arrays.stream(client.getRegistration().getObjectLinks()).forEach(link -> {
LwM2mPath pathIds = new LwM2mPath(link.getUriReference());
if (!pathIds.isRoot()) {
clientObjects.add(convertObjectIdToVersionedId(link.getUriReference(), client.getRegistration()));
clientObjects.add(convertObjectIdToVersionedId(link.getUriReference(), client));
}
});
return (clientObjects.size() > 0) ? clientObjects : null;

14
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java

@ -481,7 +481,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
}
private void startUpdateUsingUrl(LwM2mClient client, String id, String url) {
String targetIdVer = convertObjectIdToVersionedId(id, client.getRegistration());
String targetIdVer = convertObjectIdToVersionedId(id, client);
TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(targetIdVer).value(url).timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendWriteReplaceRequest(client, request, new TbLwM2MWriteResponseCallback(uplinkHandler, logService, client, targetIdVer));
}
@ -512,10 +512,10 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
}
switch (strategy) {
case OBJ_5_BINARY:
startUpdateUsingBinary(client, convertObjectIdToVersionedId(FW_PACKAGE_5_ID, client.getRegistration()), otaPackageId);
startUpdateUsingBinary(client, convertObjectIdToVersionedId(FW_PACKAGE_5_ID, client), otaPackageId);
break;
case OBJ_19_BINARY:
startUpdateUsingBinary(client, convertObjectIdToVersionedId(FW_PACKAGE_19_ID, client.getRegistration()), otaPackageId);
startUpdateUsingBinary(client, convertObjectIdToVersionedId(FW_PACKAGE_19_ID, client), otaPackageId);
break;
case OBJ_5_TEMP_URL:
startUpdateUsingUrl(client, FW_URL_ID, info.getBaseUrl() + "/" + FIRMWARE_UPDATE_COAP_RESOURCE + "/" + otaPackageId.toString());
@ -534,7 +534,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
LwM2MSoftwareUpdateStrategy strategy = info.getStrategy();
switch (strategy) {
case BINARY:
startUpdateUsingBinary(client, convertObjectIdToVersionedId(SW_PACKAGE_ID, client.getRegistration()), otaPackageId);
startUpdateUsingBinary(client, convertObjectIdToVersionedId(SW_PACKAGE_ID, client), otaPackageId);
break;
case TEMP_URL:
startUpdateUsingUrl(client, SW_PACKAGE_URI_ID, info.getBaseUrl() + "/" + FIRMWARE_UPDATE_COAP_RESOURCE + "/" + otaPackageId.toString());
@ -566,19 +566,19 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
}
private void executeFwUpdate(LwM2mClient client) {
String fwExecuteVerId = convertObjectIdToVersionedId(FW_EXECUTE_ID, client.getRegistration());
String fwExecuteVerId = convertObjectIdToVersionedId(FW_EXECUTE_ID, client);
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(fwExecuteVerId).timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, fwExecuteVerId));
}
private void executeSwInstall(LwM2mClient client) {
String swInstallVerId = convertObjectIdToVersionedId(SW_INSTALL_ID, client.getRegistration());
String swInstallVerId = convertObjectIdToVersionedId(SW_INSTALL_ID, client);
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(swInstallVerId).timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, swInstallVerId));
}
private void executeSwUninstallForUpdate(LwM2mClient client) {
String swInInstallVerId = convertObjectIdToVersionedId(SW_UN_INSTALL_ID, client.getRegistration());
String swInInstallVerId = convertObjectIdToVersionedId(SW_UN_INSTALL_ID, client);
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(swInInstallVerId).params("1").timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, swInInstallVerId));
}

10
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java

@ -23,6 +23,7 @@ import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.protobuf.util.JsonFormat;
import lombok.SneakyThrows;
import org.eclipse.leshan.core.LwM2m.Version;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
import org.eclipse.leshan.core.node.LwM2mNodeException;
@ -107,6 +108,8 @@ public class LwM2MClientSerDes {
if (client.getPagingTransmissionWindow() != null) {
o.addProperty("pagingTransmissionWindow", client.getPagingTransmissionWindow());
}
o.addProperty("defaultObjectIDVer", client.getDefaultObjectIDVer().toString());
if (client.getRegistration() != null) {
String registrationAddress = client.getRegistration().getAddress().toString();
JsonNode registrationNode = registrationSerDes.jSerialize(client.getRegistration());
@ -339,6 +342,13 @@ public class LwM2MClientSerDes {
pagingTransmissionWindowField.set(lwM2mClient, pagingTransmissionWindow.getAsLong());
}
JsonElement defaultObjectIDVer = o.get("defaultObjectIDVer");
if (defaultObjectIDVer != null) {
Field defaultObjectIDVerField = lwM2mClientClass.getDeclaredField("defaultObjectIDVer");
defaultObjectIDVerField.setAccessible(true);
defaultObjectIDVerField.set(lwM2mClient, new Version(defaultObjectIDVer.getAsString()));
}
JsonElement registration = o.get("registration");
if (registration != null) {
lwM2mClient.setRegistration(registrationSerDes.deserialize(toJsonNode(registration.getAsString())));

28
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java

@ -368,7 +368,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
LwM2mPath path = entry.getKey();
LwM2mNode node = entry.getValue();
LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint());
String stringPath = convertObjectIdToVersionedId(path.toString(), registration);
String stringPath = convertObjectIdToVersionedId(path.toString(), lwM2MClient);
ObjectModel objectModelVersion = lwM2MClient.getObjectModel(stringPath, modelProvider);
if (objectModelVersion != null) {
if (node instanceof LwM2mObject) {
@ -584,27 +584,27 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
private void updateResourcesValue(LwM2mClient lwM2MClient, LwM2mResource lwM2mResource, String path, Mode mode, int code) {
Registration registration = lwM2MClient.getRegistration();
if (lwM2MClient.saveResourceValue(path, lwM2mResource, modelProvider, mode)) {
if (path.equals(convertObjectIdToVersionedId(FW_NAME_ID, registration))) {
if (path.equals(convertObjectIdToVersionedId(FW_NAME_ID, lwM2MClient))) {
otaService.onCurrentFirmwareNameUpdate(lwM2MClient, (String) lwM2mResource.getValue());
} else if (path.equals(convertObjectIdToVersionedId(FW_3_VER_ID, registration))) {
} else if (path.equals(convertObjectIdToVersionedId(FW_3_VER_ID, lwM2MClient))) {
otaService.onCurrentFirmwareVersion3Update(lwM2MClient, (String) lwM2mResource.getValue());
} else if (path.equals(convertObjectIdToVersionedId(FW_VER_ID, registration))) {
} else if (path.equals(convertObjectIdToVersionedId(FW_VER_ID, lwM2MClient))) {
otaService.onCurrentFirmwareVersionUpdate(lwM2MClient, (String) lwM2mResource.getValue());
} else if (path.equals(convertObjectIdToVersionedId(FW_STATE_ID, registration))) {
} else if (path.equals(convertObjectIdToVersionedId(FW_STATE_ID, lwM2MClient))) {
otaService.onCurrentFirmwareStateUpdate(lwM2MClient, (Long) lwM2mResource.getValue());
} else if (path.equals(convertObjectIdToVersionedId(FW_RESULT_ID, registration))) {
} else if (path.equals(convertObjectIdToVersionedId(FW_RESULT_ID, lwM2MClient))) {
otaService.onCurrentFirmwareResultUpdate(lwM2MClient, (Long) lwM2mResource.getValue());
} else if (path.equals(convertObjectIdToVersionedId(FW_DELIVERY_METHOD, registration))) {
} else if (path.equals(convertObjectIdToVersionedId(FW_DELIVERY_METHOD, lwM2MClient))) {
otaService.onCurrentFirmwareDeliveryMethodUpdate(lwM2MClient, (Long) lwM2mResource.getValue());
} else if (path.equals(convertObjectIdToVersionedId(SW_NAME_ID, registration))) {
} else if (path.equals(convertObjectIdToVersionedId(SW_NAME_ID, lwM2MClient))) {
otaService.onCurrentSoftwareNameUpdate(lwM2MClient, (String) lwM2mResource.getValue());
} else if (path.equals(convertObjectIdToVersionedId(SW_VER_ID, registration))) {
} else if (path.equals(convertObjectIdToVersionedId(SW_VER_ID, lwM2MClient))) {
otaService.onCurrentSoftwareVersionUpdate(lwM2MClient, (String) lwM2mResource.getValue());
} else if (path.equals(convertObjectIdToVersionedId(SW_3_VER_ID, registration))) {
} else if (path.equals(convertObjectIdToVersionedId(SW_3_VER_ID, lwM2MClient))) {
otaService.onCurrentSoftwareVersion3Update(lwM2MClient, (String) lwM2mResource.getValue());
} else if (path.equals(convertObjectIdToVersionedId(SW_STATE_ID, registration))) {
} else if (path.equals(convertObjectIdToVersionedId(SW_STATE_ID, lwM2MClient))) {
otaService.onCurrentSoftwareStateUpdate(lwM2MClient, (Long) lwM2mResource.getValue());
} else if (path.equals(convertObjectIdToVersionedId(SW_RESULT_ID, registration))) {
} else if (path.equals(convertObjectIdToVersionedId(SW_RESULT_ID, lwM2MClient))) {
otaService.onCurrentSoftwareResultUpdate(lwM2MClient, (Long) lwM2mResource.getValue());
}
if (ResponseCode.BAD_REQUEST.getCode() > code) {
@ -969,6 +969,10 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
}
}
public LwM2mClientContext getClientContext(){
return this.clientContext;
};
private Map<String, String> getNamesFromProfileForSharedAttributes(LwM2mClient lwM2MClient) {
Lwm2mDeviceProfileTransportConfiguration profile = clientContext.getProfile(lwM2MClient.getProfileId());
return profile.getObserveAttr().getKeyName();

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
import java.util.Collection;
import java.util.Optional;
@ -77,4 +78,5 @@ public interface LwM2mUplinkMsgHandler {
LwM2mValueConverter getConverter();
LwM2mClientContext getClientContext();
}

81
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java

@ -20,16 +20,13 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.leshan.core.model.LwM2mModel;
import org.eclipse.leshan.core.model.ObjectLoader;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.model.StaticModel;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.LwM2mSingleResource;
import org.eclipse.leshan.core.node.codec.CodecException;
import org.eclipse.leshan.core.util.Hex;
import org.eclipse.leshan.server.registration.Registration;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
@ -49,8 +46,6 @@ import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdate
import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateResult;
import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateState;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -84,56 +79,6 @@ public class LwM2MTransportUtil {
public static final String LOG_LWM2M_WARN = "warn";
public static final int BOOTSTRAP_DEFAULT_SHORT_ID_0 = 0;
public enum LwM2MClientStrategy {
CLIENT_STRATEGY_1(1, "Read only resources marked as observation"),
CLIENT_STRATEGY_2(2, "Read all client resources");
public int code;
public String type;
LwM2MClientStrategy(int code, String type) {
this.code = code;
this.type = type;
}
public static LwM2MClientStrategy fromStrategyClientByType(String type) {
for (LwM2MClientStrategy to : LwM2MClientStrategy.values()) {
if (to.type.equals(type)) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported Client Strategy type : %s", type));
}
public static LwM2MClientStrategy fromStrategyClientByCode(int code) {
for (LwM2MClientStrategy to : LwM2MClientStrategy.values()) {
if (to.code == code) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported Client Strategy code : %s", code));
}
}
public static boolean equalsResourceValue(Object valueOld, Object valueNew, ResourceModel.Type type, LwM2mPath
resourcePath) throws CodecException {
switch (type) {
case BOOLEAN:
case INTEGER:
case FLOAT:
return String.valueOf(valueOld).equals(String.valueOf(valueNew));
case TIME:
return ((Date) valueOld).getTime() == ((Date) valueNew).getTime();
case STRING:
case OBJLNK:
return valueOld.equals(valueNew);
case OPAQUE:
return Arrays.equals(Hex.decodeHex(((String) valueOld).toCharArray()), Hex.decodeHex(((String) valueNew).toCharArray()));
default:
throw new CodecException("Invalid value type for resource %s, type %s", resourcePath, type);
}
}
public static LwM2mOtaConvert convertOtaUpdateValueToString(String pathIdVer, Object value, ResourceModel.Type currentType) {
String path = fromVersionedIdToObjectId(pathIdVer);
LwM2mOtaConvert lwM2mOtaConvert = new LwM2mOtaConvert();
@ -210,22 +155,8 @@ public class LwM2MTransportUtil {
return null;
}
public static String validPathIdVer(String pathIdVer, Registration registration) throws
IllegalArgumentException {
if (!pathIdVer.contains(LWM2M_SEPARATOR_PATH)) {
throw new IllegalArgumentException(String.format("Error:"));
} else {
String[] keyArray = pathIdVer.split(LWM2M_SEPARATOR_PATH);
if (keyArray.length > 1 && keyArray[1].split(LWM2M_SEPARATOR_KEY).length == 2) {
return pathIdVer;
} else {
return convertObjectIdToVersionedId(pathIdVer, registration);
}
}
}
public static String convertObjectIdToVersionedId(String path, Registration registration) {
String ver = String.valueOf(registration.getSupportedObject().get(new LwM2mPath(path).getObjectId()));
public static String convertObjectIdToVersionedId(String path, LwM2mClient lwM2MClient) {
String ver = String.valueOf(lwM2MClient.getSupportedObjectVersion(new LwM2mPath(path).getObjectId()));
return convertObjectIdToVerId(path, ver);
}
public static String convertObjectIdToVerId(String path, String ver) {
@ -243,14 +174,6 @@ public class LwM2MTransportUtil {
}
}
public static String validateObjectVerFromKey(String key) {
try {
return (key.split(LWM2M_SEPARATOR_PATH)[1].split(LWM2M_SEPARATOR_KEY)[1]);
} catch (Exception e) {
return ObjectModel.DEFAULT_VERSION;
}
}
/**
* "UNSIGNED_INTEGER": // Number -> Integer Example:
* Alarm Timestamp [32-bit unsigned integer]

70
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -21,6 +21,8 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
@ -97,6 +99,7 @@ import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.TbQueueRequestTemplate;
import org.thingsboard.server.queue.common.AsyncCallbackTemplate;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.consumer.QueueConsumerManager;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.discovery.TopicService;
@ -106,14 +109,12 @@ import org.thingsboard.server.queue.scheduler.SchedulerComponent;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbTransportComponent;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
@ -179,19 +180,17 @@ public class DefaultTransportService extends TransportActivityManager implements
protected TbQueueRequestTemplate<TbProtoQueueMsg<TransportApiRequestMsg>, TbProtoQueueMsg<TransportApiResponseMsg>> transportApiRequestTemplate;
protected TbQueueProducer<TbProtoQueueMsg<ToRuleEngineMsg>> ruleEngineMsgProducer;
protected TbQueueProducer<TbProtoQueueMsg<ToCoreMsg>> tbCoreMsgProducer;
protected TbQueueConsumer<TbProtoQueueMsg<ToTransportMsg>> transportNotificationsConsumer;
protected QueueConsumerManager<TbProtoQueueMsg<ToTransportMsg>> transportNotificationsConsumer;
protected MessagesStats ruleEngineProducerStats;
protected MessagesStats tbCoreProducerStats;
protected MessagesStats transportApiStats;
protected ExecutorService transportCallbackExecutor;
private ExecutorService mainConsumerExecutor;
private ExecutorService consumerExecutor;
private final Map<String, RpcRequestMetadata> toServerRpcPendingMap = new ConcurrentHashMap<>();
private volatile boolean stopped = false;
public DefaultTransportService(PartitionService partitionService,
TbServiceInfoProvider serviceInfoProvider,
TbTransportQueueFactory queueProvider,
@ -232,42 +231,33 @@ public class DefaultTransportService extends TransportActivityManager implements
transportApiRequestTemplate.setMessagesStats(transportApiStats);
ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer();
tbCoreMsgProducer = producerProvider.getTbCoreMsgProducer();
transportNotificationsConsumer = queueProvider.createTransportNotificationsConsumer();
TopicPartitionInfo tpi = topicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, serviceInfoProvider.getServiceId());
transportNotificationsConsumer.subscribe(Collections.singleton(tpi));
transportApiRequestTemplate.init();
mainConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("transport-consumer"));
consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("transport-consumer"));
transportNotificationsConsumer = QueueConsumerManager.<TbProtoQueueMsg<ToTransportMsg>>builder()
.name("TB Transport")
.msgPackProcessor(this::processNotificationMsgs)
.pollInterval(notificationsPollDuration)
.consumerCreator(queueProvider::createTransportNotificationsConsumer)
.consumerExecutor(consumerExecutor)
.build();
}
@AfterStartUp(order = AfterStartUp.TRANSPORT_SERVICE)
public void start() {
mainConsumerExecutor.execute(() -> {
while (!stopped) {
try {
List<TbProtoQueueMsg<ToTransportMsg>> records = transportNotificationsConsumer.poll(notificationsPollDuration);
if (records.size() == 0) {
continue;
}
records.forEach(record -> {
try {
processToTransportMsg(record.getValue());
} catch (Throwable e) {
log.warn("Failed to process the notification.", e);
}
});
transportNotificationsConsumer.commit();
} catch (Exception e) {
if (!stopped) {
log.warn("Failed to obtain messages from queue.", e);
try {
Thread.sleep(notificationsPollDuration);
} catch (InterruptedException e2) {
log.trace("Failed to wait until the server has capacity to handle new requests", e2);
}
}
}
TopicPartitionInfo tpi = topicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, serviceInfoProvider.getServiceId());
transportNotificationsConsumer.subscribe(Set.of(tpi));
transportNotificationsConsumer.launch();
}
private void processNotificationMsgs(List<TbProtoQueueMsg<ToTransportMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToTransportMsg>> consumer) {
msgs.forEach(msg -> {
try {
processToTransportMsg(msg.getValue());
} catch (Throwable e) {
log.warn("Failed to process the notification.", e);
}
});
consumer.commit();
}
private void invalidateRateLimits() {
@ -276,16 +266,14 @@ public class DefaultTransportService extends TransportActivityManager implements
@PreDestroy
public void destroy() {
stopped = true;
if (transportNotificationsConsumer != null) {
transportNotificationsConsumer.unsubscribe();
transportNotificationsConsumer.stop();
}
if (transportCallbackExecutor != null) {
transportCallbackExecutor.shutdownNow();
}
if (mainConsumerExecutor != null) {
mainConsumerExecutor.shutdownNow();
if (consumerExecutor != null) {
consumerExecutor.shutdownNow();
}
if (transportApiRequestTemplate != null) {
transportApiRequestTemplate.stop();

86
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java

@ -21,14 +21,13 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.eclipse.jgit.errors.LargeObjectException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.EntityType;
@ -68,16 +67,16 @@ import org.thingsboard.server.gen.transport.TransportProtos.VersionedEntityInfoP
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.common.consumer.QueueConsumerManager;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.provider.TbVersionControlQueueFactory;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbVersionControlComponent;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
@ -115,9 +114,8 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
private final Map<TenantId, PendingCommit> pendingCommitMap = new HashMap<>();
private volatile ExecutorService consumerExecutor;
private volatile TbQueueConsumer<TbProtoQueueMsg<ToVersionControlServiceMsg>> consumer;
private volatile QueueConsumerManager<TbProtoQueueMsg<ToVersionControlServiceMsg>> consumer;
private volatile TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> producer;
private volatile boolean stopped = false;
@Value("${queue.vc.poll-interval:25}")
private long pollDuration;
@ -134,20 +132,25 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
@PostConstruct
public void init() {
consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("vc-consumer"));
consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("vc-consumer"));
var threadFactory = ThingsBoardThreadFactory.forName("vc-io-thread");
for (int i = 0; i < ioPoolSize; i++) {
ioThreads.add(MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(threadFactory)));
}
producer = producerProvider.getTbCoreNotificationsMsgProducer();
consumer = queueFactory.createToVersionControlMsgConsumer();
consumer = QueueConsumerManager.<TbProtoQueueMsg<ToVersionControlServiceMsg>>builder()
.name("TB Version Control")
.msgPackProcessor(this::processMsgs)
.pollInterval(pollDuration)
.consumerCreator(queueFactory::createToVersionControlMsgConsumer)
.consumerExecutor(consumerExecutor)
.build();
}
@PreDestroy
public void stop() {
stopped = true;
if (consumer != null) {
consumer.unsubscribe();
consumer.stop();
}
if (consumerExecutor != null) {
consumerExecutor.shutdownNow();
@ -179,48 +182,29 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
return ServiceType.TB_VC_EXECUTOR.equals(event.getServiceType());
}
@EventListener(ApplicationReadyEvent.class)
@Order(value = 2)
public void onApplicationEvent(ApplicationReadyEvent event) {
consumerExecutor.execute(() -> consumerLoop(consumer));
@AfterStartUp(order = 2)
public void afterStartUp() {
consumer.launch();
}
void consumerLoop(TbQueueConsumer<TbProtoQueueMsg<ToVersionControlServiceMsg>> consumer) {
while (!stopped && !consumer.isStopped()) {
List<ListenableFuture<?>> futures = new ArrayList<>();
try {
List<TbProtoQueueMsg<ToVersionControlServiceMsg>> msgs = consumer.poll(pollDuration);
if (msgs.isEmpty()) {
continue;
}
for (TbProtoQueueMsg<ToVersionControlServiceMsg> msgWrapper : msgs) {
ToVersionControlServiceMsg msg = msgWrapper.getValue();
var ctx = new VersionControlRequestCtx(msg, msg.hasClearRepositoryRequest() ? null : ProtoUtils.fromProto(msg.getVcSettings()));
long startTs = System.currentTimeMillis();
log.trace("[{}][{}] RECEIVED task: {}", ctx.getTenantId(), ctx.getRequestId(), msg);
int threadIdx = Math.abs(ctx.getTenantId().hashCode() % ioPoolSize);
ListenableFuture<Void> future = ioThreads.get(threadIdx).submit(() -> processMessage(ctx, msg));
logTaskExecution(ctx, future, startTs);
futures.add(future);
}
try {
Futures.allAsList(futures).get(packProcessingTimeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
log.info("Timeout for processing the version control tasks.", e);
}
consumer.commit();
} catch (Exception e) {
if (!stopped) {
log.warn("Failed to obtain version control requests from queue.", e);
try {
Thread.sleep(pollDuration);
} catch (InterruptedException e2) {
log.trace("Failed to wait until the server has capacity to handle new version control messages", e2);
}
}
}
void processMsgs(List<TbProtoQueueMsg<ToVersionControlServiceMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToVersionControlServiceMsg>> consumer) throws Exception {
List<ListenableFuture<?>> futures = new ArrayList<>();
for (TbProtoQueueMsg<ToVersionControlServiceMsg> msgWrapper : msgs) {
ToVersionControlServiceMsg msg = msgWrapper.getValue();
var ctx = new VersionControlRequestCtx(msg, msg.hasClearRepositoryRequest() ? null : ProtoUtils.fromProto(msg.getVcSettings()));
long startTs = System.currentTimeMillis();
log.trace("[{}][{}] RECEIVED task: {}", ctx.getTenantId(), ctx.getRequestId(), msg);
int threadIdx = Math.abs(ctx.getTenantId().hashCode() % ioPoolSize);
ListenableFuture<Void> future = ioThreads.get(threadIdx).submit(() -> processMessage(ctx, msg));
logTaskExecution(ctx, future, startTs);
futures.add(future);
}
try {
Futures.allAsList(futures).get(packProcessingTimeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
log.info("Timeout for processing the version control tasks.", e);
}
log.info("TB Version Control request consumer stopped.");
consumer.commit();
}
private Void processMessage(VersionControlRequestCtx ctx, ToVersionControlServiceMsg msg) {
@ -273,7 +257,7 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
var ids = vcService.listEntitiesAtVersion(ctx.getTenantId(), request.getVersionId(), path)
.stream().skip(request.getOffset()).limit(request.getLimit()).collect(Collectors.toList());
if (!ids.isEmpty()) {
for (int i = 0; i < ids.size(); i++){
for (int i = 0; i < ids.size(); i++) {
VersionedEntityInfo info = ids.get(i);
var data = vcService.getFileContentAtCommit(ctx.getTenantId(),
getRelativePath(info.getExternalId().getEntityType(), info.getExternalId().getId().toString()), request.getVersionId());

4
dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java

@ -45,7 +45,7 @@ public interface AttributeKvRepository extends JpaRepository<AttributeKvEntity,
@Query(value = "SELECT DISTINCT attribute_key FROM attribute_kv WHERE " +
"entity_id in (SELECT id FROM device WHERE tenant_id = :tenantId and device_profile_id = :deviceProfileId limit 100) ORDER BY attribute_key", nativeQuery = true)
List<Integer> findAllKeysByDeviceProfileId(@Param("tenantId") UUID tenantId,
@Param("deviceProfileId") UUID deviceProfileId);
@Param("deviceProfileId") UUID deviceProfileId);
@Query(value = "SELECT DISTINCT attribute_key FROM attribute_kv WHERE " +
"entity_id in (SELECT id FROM device WHERE tenant_id = :tenantId limit 100) ORDER BY attribute_key", nativeQuery = true)
@ -58,6 +58,6 @@ public interface AttributeKvRepository extends JpaRepository<AttributeKvEntity,
@Query(value = "SELECT DISTINCT attribute_key FROM attribute_kv WHERE " +
"entity_id in :entityIds AND attribute_type = :attributeType ORDER BY attribute_key", nativeQuery = true)
List<Integer> findAllKeysByEntityIdsAndAttributeType(@Param("entityIds") List<UUID> entityIds,
@Param("attributeType") String attributeType);
@Param("attributeType") int attributeType);
}

2
dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java

@ -173,7 +173,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
@Override
public List<String> findAllKeysByEntityIdsAndAttributeType(TenantId tenantId, List<EntityId> entityIds, String attributeType) {
return attributeKvRepository
.findAllKeysByEntityIdsAndAttributeType(entityIds.stream().map(EntityId::getId).collect(Collectors.toList()), attributeType)
.findAllKeysByEntityIdsAndAttributeType(entityIds.stream().map(EntityId::getId).collect(Collectors.toList()), AttributeScope.valueOf(attributeType).getId())
.stream().map(id -> keyDictionaryDao.getKey(id)).collect(Collectors.toList());
}

42
dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java

@ -21,6 +21,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@ -47,6 +48,8 @@ import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Andrew Shvayka
*/
@ -221,6 +224,45 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest {
Assert.assertEquals(NEW_VALUE, value.get(1));
}
@Test
public void testFindAllKeysByEntityId() {
var tenantId = new TenantId(UUID.randomUUID());
var deviceId = new DeviceId(UUID.randomUUID());
saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key1", "123");
saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key2", "123");
Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
List<String> keys = attributesService.findAllKeysByEntityIds(tenantId, List.of(deviceId));
assertThat(keys).containsOnly("key1", "key2");
});
}
@Test
public void testFindAllKeysByEntityIdAndAttributeType() {
var tenantId = new TenantId(UUID.randomUUID());
var deviceId = new DeviceId(UUID.randomUUID());
saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key1", "123");
saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key2", "123");
Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
List<String> keys = attributesService.findAllKeysByEntityIds(tenantId, List.of(deviceId), AttributeScope.SERVER_SCOPE.name());
assertThat(keys).containsOnly("key1", "key2");
});
}
@Test
public void testFindAllByEntityIdAndAttributeType() {
var tenantId = new TenantId(UUID.randomUUID());
var deviceId = new DeviceId(UUID.randomUUID());
saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key1", "123");
saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key2", "123");
Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
List<AttributeKvEntry> attributes = attributesService.findAll(tenantId, deviceId, AttributeScope.SERVER_SCOPE).get();
assertThat(attributes).extracting(KvEntry::getKey).containsOnly("key1", "key2");
});
}
private void testConcurrentFetchAndUpdate(TenantId tenantId, DeviceId deviceId, ListeningExecutorService pool) throws Exception {
var scope = AttributeScope.SERVER_SCOPE;
var key = "TEST";

9
monitoring/src/main/resources/lwm2m/device_profile.json

@ -12,14 +12,14 @@
"transportConfiguration": {
"observeAttr": {
"observe": [
"/3_1.1/0/0"
"/3_1.0/0/0"
],
"attribute": [],
"telemetry": [
"/3_1.1/0/0"
"/3_1.0/0/0"
],
"keyName": {
"/3_1.1/0/0": "testData"
"/3_1.0/0/0": "testData"
},
"attributeLwm2m": {}
},
@ -44,8 +44,7 @@
"clientOnlyObserveAfterConnect": 1,
"fwUpdateStrategy": 1,
"swUpdateStrategy": 1,
"powerMode": "DRX",
"compositeOperationsSupport": false
"powerMode": "DRX"
},
"bootstrapServerUpdateEnable": false,
"type": "LWM2M"

4
monitoring/src/main/resources/lwm2m/models/test-model.xml

@ -23,9 +23,9 @@
<Description1>
<![CDATA[]]></Description1>
<ObjectID>3</ObjectID>
<ObjectURN>urn:oma:lwm2m:oma:3:1.1</ObjectURN>
<ObjectURN>urn:oma:lwm2m:oma:3</ObjectURN>
<LWM2MVersion>1.1</LWM2MVersion>
<ObjectVersion>1.1</ObjectVersion>
<ObjectVersion>1.0</ObjectVersion>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Mandatory</Mandatory>
<Resources>

2
monitoring/src/main/resources/lwm2m/resource.json

@ -2,5 +2,5 @@
"title": "",
"resourceType": "LWM2M_MODEL",
"fileName": "test-model.xml",
"data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQoKICAgIENvcHlyaWdodCDCqSAyMDE2LTIwMjIgVGhlIFRoaW5nc2JvYXJkIEF1dGhvcnMKCiAgICBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKICAgIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KICAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiAgICBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiAgICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiAgICBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiAgICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCi0tPgo8TFdNMk0geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIKICAgICAgIHhzaTpub05hbWVzcGFjZVNjaGVtYUxvY2F0aW9uPSJodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS12MV8xLnhzZCI+CiAgICA8T2JqZWN0IE9iamVjdFR5cGU9Ik1PRGVmaW5pdGlvbiI+CiAgICAgICAgPE5hbWU+THdNMk0gTW9uaXRvcmluZzwvTmFtZT4KICAgICAgICA8RGVzY3JpcHRpb24xPgogICAgICAgICAgICA8IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMT4KICAgICAgICA8T2JqZWN0SUQ+MzwvT2JqZWN0SUQ+CiAgICAgICAgPE9iamVjdFVSTj51cm46b21hOmx3bTJtOm9tYTozOjEuMTwvT2JqZWN0VVJOPgogICAgICAgIDxMV00yTVZlcnNpb24+MS4xPC9MV00yTVZlcnNpb24+CiAgICAgICAgPE9iamVjdFZlcnNpb24+MS4xPC9PYmplY3RWZXJzaW9uPgogICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgIDxNYW5kYXRvcnk+TWFuZGF0b3J5PC9NYW5kYXRvcnk+CiAgICAgICAgPFJlc291cmNlcz4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjAiPgogICAgICAgICAgICAgICAgPE5hbWU+VGVzdCBkYXRhPC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+UjwvT3BlcmF0aW9ucz4KICAgICAgICAgICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgICAgICAgICAgPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+U3RyaW5nPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbVGVzdCBkYXRhXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgIDwvUmVzb3VyY2VzPgogICAgICAgIDxEZXNjcmlwdGlvbjI+PC9EZXNjcmlwdGlvbjI+CiAgICA8L09iamVjdD4KPC9MV00yTT4K"
"data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQoKICAgIENvcHlyaWdodCDCqSAyMDE2LTIwMjQgVGhlIFRoaW5nc2JvYXJkIEF1dGhvcnMKCiAgICBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKICAgIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KICAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiAgICBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiAgICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiAgICBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiAgICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCi0tPgo8TFdNMk0geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIKICAgICAgIHhzaTpub05hbWVzcGFjZVNjaGVtYUxvY2F0aW9uPSJodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS12MV8xLnhzZCI+CiAgICA8T2JqZWN0IE9iamVjdFR5cGU9Ik1PRGVmaW5pdGlvbiI+CiAgICAgICAgPE5hbWU+THdNMk0gTW9uaXRvcmluZzwvTmFtZT4KICAgICAgICA8RGVzY3JpcHRpb24xPgogICAgICAgICAgICA8IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMT4KICAgICAgICA8T2JqZWN0SUQ+MzwvT2JqZWN0SUQ+CiAgICAgICAgPE9iamVjdFVSTj51cm46b21hOmx3bTJtOm9tYTozPC9PYmplY3RVUk4+CiAgICAgICAgPExXTTJNVmVyc2lvbj4xLjE8L0xXTTJNVmVyc2lvbj4KICAgICAgICA8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CiAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgPE1hbmRhdG9yeT5NYW5kYXRvcnk8L01hbmRhdG9yeT4KICAgICAgICA8UmVzb3VyY2VzPgogICAgICAgICAgICA8SXRlbSBJRD0iMCI+CiAgICAgICAgICAgICAgICA8TmFtZT5UZXN0IGRhdGE8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5TdHJpbmc8L1R5cGU+CiAgICAgICAgICAgICAgICA8UmFuZ2VFbnVtZXJhdGlvbj48L1JhbmdlRW51bWVyYXRpb24+CiAgICAgICAgICAgICAgICA8VW5pdHM+PC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVtUZXN0IGRhdGFdXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgPC9SZXNvdXJjZXM+CiAgICAgICAgPERlc2NyaXB0aW9uMj48L0Rlc2NyaXB0aW9uMj4KICAgIDwvT2JqZWN0Pgo8L0xXTTJNPgo="
}

11
ui-ngx/src/app/core/api/data-aggregator.ts

@ -48,7 +48,8 @@ class AggDataMap {
constructor(
private subsTw: SubscriptionTimewindow,
private endTs: number
private endTs: number,
private aggType: AggregationType
){};
set(ts: number, data: AggData) {
@ -78,7 +79,7 @@ class AggDataMap {
}
calculateAggInterval(timestamp: number): [number, number] {
return calculateAggIntervalWithSubscriptionTimeWindow(this.subsTw, this.endTs, timestamp);
return calculateAggIntervalWithSubscriptionTimeWindow(this.subsTw, this.endTs, timestamp, this.aggType);
}
updateLastInterval(endTs: number) {
@ -87,7 +88,7 @@ class AggDataMap {
const lastTs = this.map.maxKey();
if (lastTs) {
const data = this.map.get(lastTs);
const interval = calculateAggIntervalWithSubscriptionTimeWindow(this.subsTw, endTs, data.ts);
const interval = calculateAggIntervalWithSubscriptionTimeWindow(this.subsTw, endTs, data.ts, this.aggType);
data.interval = interval;
data.ts = interval[0] + Math.floor((interval[1] - interval[0]) / 2);
}
@ -415,7 +416,7 @@ export class DataAggregator {
const noAggregation = aggType === AggregationType.NONE;
let aggKeyData = aggregationMap.aggMap[id];
if (!aggKeyData) {
aggKeyData = new AggDataMap(this.subsTw, this.endTs);
aggKeyData = new AggDataMap(this.subsTw, this.endTs, aggType);
aggregationMap.aggMap[id] = aggKeyData;
}
const keyData = data[id];
@ -449,7 +450,7 @@ export class DataAggregator {
const noAggregation = aggType === AggregationType.NONE;
let aggKeyData = this.aggregationMap.aggMap[id];
if (!aggKeyData) {
aggKeyData = new AggDataMap(this.subsTw, this.endTs);
aggKeyData = new AggDataMap(this.subsTw, this.endTs, aggType);
this.aggregationMap.aggMap[id] = aggKeyData;
}
const keyData = data[id];

58
ui-ngx/src/app/core/services/dynamic-component-factory.service.ts

@ -17,7 +17,6 @@
import {
Compiler,
Component,
ComponentFactory,
Injectable,
Injector,
NgModule,
@ -26,9 +25,9 @@ import {
Type,
ɵresetCompiledComponents
} from '@angular/core';
import { from, Observable } from 'rxjs';
import { from, Observable, of } from 'rxjs';
import { CommonModule } from '@angular/common';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { mergeMap } from 'rxjs/operators';
@NgModule()
export abstract class DynamicComponentModule implements OnDestroy {
@ -79,33 +78,32 @@ export class DynamicComponentFactoryService {
declarations: [comp],
imports: moduleImports
})(class DynamicComponentInstanceModule extends DynamicComponentModule {});
return from(this.compiler.compileModuleAsync(dynamicComponentInstanceModule)).pipe(
map((module) => {
let moduleRef: NgModuleRef<any>;
try {
moduleRef = module.create(this.injector);
} catch (e) {
this.compiler.clearCacheFor(module.moduleType);
throw e;
}
this.dynamicComponentModulesMap.set(comp, {
moduleRef,
moduleType: module.moduleType
});
return {
componentType: comp,
componentModuleRef: moduleRef
};
}),
catchError((error) => {
if (compileAttempt === 1) {
ɵresetCompiledComponents();
return this.createDynamicComponent(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles);
} else {
throw error;
}
})
);
try {
const module = this.compiler.compileModuleSync(dynamicComponentInstanceModule);
let moduleRef: NgModuleRef<any>;
try {
moduleRef = module.create(this.injector);
} catch (e) {
this.compiler.clearCacheFor(module.moduleType);
throw e;
}
this.dynamicComponentModulesMap.set(comp, {
moduleRef,
moduleType: module.moduleType
});
return of( {
componentType: comp,
componentModuleRef: moduleRef
});
} catch (error) {
if (compileAttempt === 1) {
ɵresetCompiledComponents();
return this.createDynamicComponent(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles);
} else {
console.error(error);
throw error;
}
}
})
);
}

30
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html

@ -91,26 +91,16 @@
<tb-power-mode-settings [parentForm]="clientSettingsFormGroup">
</tb-power-mode-settings>
</fieldset>
<!-- <mat-accordion multi="true">-->
<!-- <div *ngIf="false">-->
<!-- <mat-expansion-panel>-->
<!-- <mat-expansion-panel-header>-->
<!-- <mat-panel-title>{{ 'device-profile.lwm2m.client-strategy' | translate }}</mat-panel-title>-->
<!-- </mat-expansion-panel-header>-->
<!-- <ng-template matExpansionPanelContent>-->
<!-- <div fxLayout="column">-->
<!-- <mat-form-field class="mat-block">-->
<!-- <mat-label>{{ 'device-profile.lwm2m.client-strategy-label' | translate }}</mat-label>-->
<!-- <mat-select formControlName="clientOnlyObserveAfterConnect">-->
<!-- <mat-option value=1>{{ 'device-profile.lwm2m.client-strategy-only-observe' | translate }}</mat-option>-->
<!-- <mat-option value=2>{{ 'device-profile.lwm2m.client-strategy-read-all' | translate }}</mat-option>-->
<!-- </mat-select>-->
<!-- </mat-form-field>-->
<!-- </div>-->
<!-- </ng-template>-->
<!-- </mat-expansion-panel>-->
<!-- </div>-->
<!-- </mat-accordion>-->
<fieldset class="fields-group">
<legend class="group-title" translate>device-profile.lwm2m.default-object-id</legend>
<mat-form-field fxFlex>
<mat-select formControlName="defaultObjectIDVer">
<mat-option *ngFor="let objectIDVer of objectIDVers" [value]="objectIDVer">
{{ objectIDVerTranslationMap.get(objectIDVer) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</fieldset>
</section>
</ng-template>
</mat-tab>

14
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts

@ -40,9 +40,11 @@ import {
ObjectLwM2M,
OBSERVE,
PowerMode,
ObjectIDVer,
RESOURCES,
ServerSecurityConfig,
TELEMETRY
TELEMETRY,
ObjectIDVerTranslationMap
} from './lwm2m-profile-config.models';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { deepClone, isDefinedAndNotNull, isEmpty, isUndefined } from '@core/utils';
@ -80,6 +82,10 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
lwm2mDeviceProfileFormGroup: UntypedFormGroup;
configurationValue: Lwm2mProfileConfigModels;
objectIDVers = Object.values(ObjectIDVer) as ObjectIDVer[];
objectIDVerTranslationMap = ObjectIDVerTranslationMap;
sortFunction: (key: string, value: object) => object;
get required(): boolean {
@ -116,7 +122,8 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
powerMode: [PowerMode.DRX, Validators.required],
edrxCycle: [{disabled: true, value: 0}, Validators.required],
psmActivityTimer: [{disabled: true, value: 0}, Validators.required],
pagingTransmissionWindow: [{disabled: true, value: 0}, Validators.required]
pagingTransmissionWindow: [{disabled: true, value: 0}, Validators.required],
defaultObjectIDVer: [ObjectIDVer.V1_0, Validators.required]
})
});
@ -274,7 +281,8 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
edrxCycle: this.configurationValue.clientLwM2mSettings.edrxCycle || DEFAULT_EDRX_CYCLE,
pagingTransmissionWindow:
this.configurationValue.clientLwM2mSettings.pagingTransmissionWindow || DEFAULT_PAGING_TRANSMISSION_WINDOW,
psmActivityTimer: this.configurationValue.clientLwM2mSettings.psmActivityTimer || DEFAULT_PSM_ACTIVITY_TIMER
psmActivityTimer: this.configurationValue.clientLwM2mSettings.psmActivityTimer || DEFAULT_PSM_ACTIVITY_TIMER,
defaultObjectIDVer: this.configurationValue.clientLwM2mSettings.defaultObjectIDVer || ObjectIDVer.V1_0
}
},
{emitEvent: false});

16
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts

@ -122,6 +122,18 @@ export const PowerModeTranslationMap = new Map<PowerMode, string>(
]
);
export enum ObjectIDVer {
V1_0 = '1.0',
V1_1 = '1.1'
}
export const ObjectIDVerTranslationMap = new Map<ObjectIDVer, string>(
[
[ObjectIDVer.V1_0, 'device-profile.lwm2m.default-object-id-ver.v1-0'],
[ObjectIDVer.V1_1, 'device-profile.lwm2m.default-object-id-ver.v1-1']
]
);
export interface ServerSecurityConfig {
host?: string;
port?: number;
@ -163,7 +175,7 @@ export interface ClientLwM2mSettings {
edrxCycle?: number;
pagingTransmissionWindow?: number;
psmActivityTimer?: number;
compositeOperationsSupport: boolean;
defaultObjectIDVer: ObjectIDVer;
}
export interface ObservableAttributes {
@ -190,7 +202,7 @@ export function getDefaultProfileClientLwM2mSettingsConfig(): ClientLwM2mSetting
fwUpdateStrategy: 1,
swUpdateStrategy: 1,
powerMode: PowerMode.DRX,
compositeOperationsSupport: false
defaultObjectIDVer: ObjectIDVer.V1_0
};
}

2
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts

@ -15,6 +15,7 @@
///
import {
CellClickColumnInfo,
WidgetActionDescriptor,
WidgetActionSource,
widgetActionTypeTranslationMap
@ -30,6 +31,7 @@ import { deepClone } from '@core/utils';
export interface WidgetActionCallbacks {
fetchDashboardStates: (query: string) => Array<string>;
fetchCellClickColumns: () => Array<CellClickColumnInfo>;
}
export interface WidgetActionsData {

21
ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html

@ -50,6 +50,27 @@
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row" *ngIf="widgetActionFormGroup.get('actionSourceId').value === 'cellClick'">
<div class="fixed-title-width">{{'widget-config.column-index' | translate}}*</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<mat-select #columnIndexSelect formControlName="columnIndex" placeholder="{{ columnIndexPlaceholderText }}">
<mat-option *ngFor="let column of configuredColumns; let $index = index"
[value]="$index"
[disabled]="usedCellClickColumns.includes($index)">
{{ getCellClickColumnInfo($index, column) }}
</mat-option>
</mat-select>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'widget-config.column-index-required' | translate"
*ngIf="widgetActionFormGroup.get('columnIndex').hasError('required') &&
widgetActionFormGroup.get('columnIndex').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width">{{'widget-config.action-name' | translate}}*</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">

58
ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, Inject, OnDestroy, OnInit, SkipSelf } from '@angular/core';
import { Component, Inject, OnDestroy, OnInit, SkipSelf, ViewChild } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
@ -40,6 +40,7 @@ import {
import { UtilsService } from '@core/services/utils.service';
import {
actionDescriptorToAction,
CellClickColumnInfo,
defaultWidgetAction,
WidgetActionSource,
widgetType
@ -47,6 +48,9 @@ import {
import { takeUntil } from 'rxjs/operators';
import { CustomActionEditorCompleter } from '@home/components/widget/lib/settings/common/action/custom-action.models';
import { WidgetService } from '@core/http/widget.service';
import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils';
import { MatSelect } from '@angular/material/select';
import { TranslateService } from '@ngx-translate/core';
export interface WidgetActionDialogData {
isAdd: boolean;
@ -78,6 +82,12 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
functionScopeVariables: string[];
configuredColumns: Array<CellClickColumnInfo> = [];
usedCellClickColumns: Array<number> = [];
@ViewChild('columnIndexSelect') columnIndexSelect: MatSelect;
columnIndexPlaceholderText = this.translate.instant('widget-config.select-column-index');
constructor(protected store: Store<AppState>,
protected router: Router,
private utils: UtilsService,
@ -85,7 +95,8 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
@Inject(MAT_DIALOG_DATA) public data: WidgetActionDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<WidgetActionDialogComponent, WidgetActionDescriptorInfo>,
public fb: FormBuilder) {
public fb: FormBuilder,
private translate: TranslateService) {
super(store, router, dialogRef);
this.isAdd = data.isAdd;
if (this.isAdd) {
@ -99,11 +110,15 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
this.action = this.data.action;
}
this.functionScopeVariables = this.widgetService.getWidgetScopeVariables();
if (this.action.actionSourceId === 'cellClick') {
this.getCellClickColumnsInfo();
}
}
ngOnInit(): void {
this.widgetActionFormGroup = this.fb.group({
actionSourceId: [this.action.actionSourceId, Validators.required],
columnIndex: [this.checkColumnIndex(this.action.columnIndex), Validators.required],
name: [this.action.name, [this.validateActionName(), Validators.required]],
icon: [this.action.icon, Validators.required],
useShowWidgetActionFunction: [this.action.useShowWidgetActionFunction],
@ -113,15 +128,29 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
this.updateShowWidgetActionForm();
this.widgetActionFormGroup.get('actionSourceId').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => {
).subscribe((value) => {
this.widgetActionFormGroup.get('name').updateValueAndValidity();
this.updateShowWidgetActionForm();
if (value === 'cellClick') {
this.widgetActionFormGroup.get('columnIndex').setValidators([Validators.required]);
this.getCellClickColumnsInfo();
} else {
this.widgetActionFormGroup.get('columnIndex').clearValidators();
}
this.widgetActionFormGroup.get('columnIndex').updateValueAndValidity();
});
this.widgetActionFormGroup.get('useShowWidgetActionFunction').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.updateShowWidgetActionForm();
});
setTimeout(() => {
if (this.action?.actionSourceId === 'cellClick' && isDefinedAndNotNull(this.action.columnIndex) &&
this.widgetActionFormGroup.get('columnIndex').value === null) {
this.columnIndexPlaceholderText = `${this.action.columnIndex} (${this.translate.instant('widget-config.not-set')})`;
this.columnIndexSelect.focus();
}
});
}
ngOnDestroy() {
@ -155,6 +184,22 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
this.widgetActionFormGroup.get('showWidgetActionFunction').updateValueAndValidity();
}
private checkColumnIndex(columnIndex: number): number | null {
return isDefinedAndNotNull(columnIndex) && this.configuredColumns.length - 1 < columnIndex ? null : columnIndex;
}
private getCellClickColumnsInfo(): void {
if (!this.configuredColumns.length) {
this.configuredColumns = this.data.callbacks.fetchCellClickColumns();
this.data.actionsData.actionsMap['cellClick']?.forEach(action => {
const actionColumn = this.configuredColumns[action.columnIndex];
if (actionColumn && action.columnIndex !== this.action.columnIndex) {
this.usedCellClickColumns.push(action.columnIndex);
}
});
}
}
private validateActionName(): ValidatorFn {
return (c: FormControl) => {
const newName = c.value;
@ -193,6 +238,10 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
}
}
public getCellClickColumnInfo(index: number, columnInfo: CellClickColumnInfo): string {
return `${index} (${isNotEmptyStr(columnInfo.label) ? columnInfo.label : columnInfo.name})`;
}
cancel(): void {
this.dialogRef.close(null);
}
@ -204,6 +253,9 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
{...this.widgetActionFormGroup.value, ...this.widgetActionFormGroup.get('widgetAction').value};
delete (result as any).widgetAction;
result.id = this.action.id;
if (!isDefinedAndNotNull(result.columnIndex)) {
delete result.columnIndex;
}
this.dialogRef.close(result);
}
}

6
ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html

@ -77,12 +77,14 @@
</mat-checkbox>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="column.def" *ngFor="let column of columns; trackBy: trackByColumnDef;">
<ng-container [matColumnDef]="column.def" *ngFor="let column of columns; let $index = index; trackBy: trackByColumnDef;">
<mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header [disabled]="isSorting(column)">
{{ column.title }}
</mat-header-cell>
<mat-cell *matCellDef="let alarm; let row = index"
[ngStyle]="cellStyle(alarm, column, row)">
[ngStyle]="cellStyle(alarm, column, row)"
(click)="onCellClick($event, alarm, column, $index)"
[class.tb-pointer]="columnHasCellClick($index)">
<span [innerHTML]="cellContent(alarm, column, row)"></span>
<ng-container *ngIf="column.entityKey.key === 'assignee'">
<span class="assignee-cell" fxLayout="row" fxLayoutAlign="start center">

29
ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts

@ -204,6 +204,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
private columnWidth: {[key: string]: string} = {};
private columnDefaultVisibility: {[key: string]: boolean} = {};
private columnSelectionAvailability: {[key: string]: boolean} = {};
private columnsWithCellClick: Array<number> = [];
private rowStylesInfo: RowStyleInfo;
@ -358,6 +359,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : false;
this.showCellActionsMenu = isDefined(this.settings.showCellActionsMenu) ? this.settings.showCellActionsMenu : true;
this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true;
this.columnsWithCellClick = this.ctx.actionsApi.getActionDescriptors('cellClick').map(action => action.columnIndex);
let enableFilter;
if (isDefined(this.settings.enableFilter)) {
enableFilter = this.settings.enableFilter;
@ -794,6 +796,33 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
return res;
}
public onCellClick($event: Event, alarm: AlarmDataInfo, key: EntityColumn, columnIndex: number) {
this.alarmsDatasource.toggleCurrentAlarm(alarm);
const descriptors = this.ctx.actionsApi.getActionDescriptors('cellClick');
let descriptor;
if (descriptors.length) {
descriptor = descriptors.find(desc => desc.columnIndex === columnIndex);
}
if ($event && descriptor) {
$event.stopPropagation();
let entityId;
let entityName;
let entityLabel;
if (alarm && alarm.originator) {
entityId = alarm.originator;
entityName = alarm.entityName;
entityLabel = alarm.entityLabel;
}
this.ctx.actionsApi.handleWidgetAction($event, descriptor, entityId, entityName, {alarm, key}, entityLabel);
}
}
public columnHasCellClick(columnIndex: number) {
if (this.columnsWithCellClick.length) {
return this.columnsWithCellClick.includes(columnIndex);
}
}
public onRowClick($event: Event, alarm: AlarmDataInfo) {
if ($event) {
$event.stopPropagation();

12
ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html

@ -40,12 +40,14 @@
<div fxFlex class="table-container">
<table mat-table [dataSource]="entityDatasource" [trackBy]="trackByEntityId"
matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear>
<ng-container [matColumnDef]="column.def" *ngFor="let column of columns; trackBy: trackByColumnDef;">
<ng-container [matColumnDef]="column.def" *ngFor="let column of columns; let $index = index; trackBy: trackByColumnDef;">
<mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header [disabled]="!column.sortable"> {{ column.title }} </mat-header-cell>
<mat-cell *matCellDef="let entity; let row = index"
[innerHTML]="cellContent(entity, column, row)"
[ngStyle]="cellStyle(entity, column, row)">
</mat-cell>
<mat-cell *matCellDef="let entity; let row = index"
[innerHTML]="cellContent(entity, column, row)"
[ngStyle]="cellStyle(entity, column, row)"
(click)="onCellClick($event, entity, column, $index)"
[class.tb-pointer]="columnHasCellClick($index)">
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction">
<mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: (entityDatasource.countCellButtonAction * 48) + 'px',

35
ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts

@ -169,6 +169,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
private columnWidth: {[key: string]: string} = {};
private columnDefaultVisibility: {[key: string]: boolean} = {};
private columnSelectionAvailability: {[key: string]: boolean} = {};
private columnsWithCellClick: Array<number> = [];
private rowStylesInfo: RowStyleInfo;
@ -230,6 +231,12 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
}
}
private isActionsConfigured(actionSourceIds: Array<string>): boolean {
let configured = false;
actionSourceIds.forEach(id => configured = configured || this.ctx.actionsApi.getActionDescriptors(id).length > 0 );
return configured;
}
ngOnDestroy(): void {
if (this.widgetResize$) {
this.widgetResize$.disconnect();
@ -284,6 +291,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
this.hasRowAction = !!this.ctx.actionsApi.getActionDescriptors('rowClick').length ||
!!this.ctx.actionsApi.getActionDescriptors('rowDoubleClick').length;
this.columnsWithCellClick = this.ctx.actionsApi.getActionDescriptors('cellClick').map(action => action.columnIndex);
if (this.settings.entitiesTitle && this.settings.entitiesTitle.length) {
this.ctx.widgetTitle = this.settings.entitiesTitle;
@ -694,6 +702,33 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
}
}
public onCellClick($event: Event, entity: EntityData, key: EntityColumn, columnIndex: number) {
this.entityDatasource.toggleCurrentEntity(entity);
const descriptors = this.ctx.actionsApi.getActionDescriptors('cellClick');
let descriptor;
if (descriptors.length) {
descriptor = descriptors.find(desc => desc.columnIndex === columnIndex);
}
if ($event && descriptor) {
$event.stopPropagation();
let entityId;
let entityName;
let entityLabel;
if (entity) {
entityId = entity.id;
entityName = entity.entityName;
entityLabel = entity.entityLabel;
}
this.ctx.actionsApi.handleWidgetAction($event, descriptor, entityId, entityName, {entity, key}, entityLabel);
}
}
public columnHasCellClick(index: number) {
if (this.columnsWithCellClick.length) {
return this.columnsWithCellClick.includes(index);
}
}
public onRowClick($event: Event, entity: EntityData, isDouble?: boolean) {
if ($event) {
$event.stopPropagation();

86
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.html

@ -0,0 +1,86 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-form-row space-between same-padding tb-flex column" [formGroup]="securityFormGroup">
<div class="tb-flex row space-between align-center no-gap fill-width">
<div class="fixed-title-width tb-required" translate>gateway.security</div>
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option *ngFor="let type of securityTypes" [value]="type">
{{ SecurityTypeTranslationsMap.get(type) | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<ng-container [ngSwitch]="securityFormGroup.get('type').value">
<ng-template [ngSwitchCase]="BrokerSecurityType.BASIC">
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.username</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="username" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.username-required') | translate"
*ngIf="securityFormGroup.get('username').hasError('required')
&& securityFormGroup.get('username').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.password</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="password" name="value" formControlName="password" placeholder="{{ 'gateway.set' | translate }}"/>
<div class="tb-flex no-gap align-center fill-height" matSuffix>
<tb-toggle-password class="tb-flex align-center fill-height"></tb-toggle-password>
</div>
</mat-form-field>
</div>
</div>
</ng-template>
<ng-template [ngSwitchCase]="BrokerSecurityType.CERTIFICATES">
<div class="tb-form-hint tb-primary-fill">{{ 'gateway.path-hint' | translate }}</div>
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.CA-certificate-path</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="pathToCACert" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.private-key-path</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="pathToPrivateKey" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.client-cert-path</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="pathToClientCert" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
</ng-template>
</ng-container>
</div>

20
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.scss

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

165
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts

@ -0,0 +1,165 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
NgZone,
OnDestroy,
ViewContainerRef
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { Subject } from 'rxjs';
import { Overlay } from '@angular/cdk/overlay';
import { UtilsService } from '@core/services/utils.service';
import { EntityService } from '@core/http/entity.service';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
BrokerSecurityType,
BrokerSecurityTypeTranslationsMap,
noLeadTrailSpacesRegex
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-broker-security',
templateUrl: './broker-security.component.html',
styleUrls: ['./broker-security.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BrokerSecurityComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => BrokerSecurityComponent),
multi: true
}
]
})
export class BrokerSecurityComponent extends PageComponent implements ControlValueAccessor, Validator, OnDestroy {
BrokerSecurityType = BrokerSecurityType;
securityTypes = Object.values(BrokerSecurityType);
SecurityTypeTranslationsMap = BrokerSecurityTypeTranslationsMap;
securityFormGroup: UntypedFormGroup;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(protected store: Store<AppState>,
public translate: TranslateService,
public dialog: MatDialog,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private dialogService: DialogService,
private entityService: EntityService,
private utils: UtilsService,
private zone: NgZone,
private cd: ChangeDetectorRef,
private elementRef: ElementRef,
private fb: FormBuilder) {
super(store);
this.securityFormGroup = this.fb.group({
type: [BrokerSecurityType.ANONYMOUS, []],
username: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
pathToCACert: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
pathToPrivateKey: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
pathToClientCert: ['', [Validators.pattern(noLeadTrailSpacesRegex)]]
});
this.securityFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateView(value);
});
this.securityFormGroup.get('type').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((type) => {
this.updateValidators(type);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
writeValue(deviceInfo: any) {
if (!deviceInfo.type) {
deviceInfo.type = BrokerSecurityType.ANONYMOUS;
}
this.securityFormGroup.reset(deviceInfo);
this.updateView(deviceInfo);
}
validate(): ValidationErrors | null {
return this.securityFormGroup.valid ? null : {
securityForm: { valid: false }
};
}
updateView(value: any) {
this.propagateChange(value);
}
private updateValidators(type) {
if (type) {
this.securityFormGroup.get('username').disable({emitEvent: false});
this.securityFormGroup.get('password').disable({emitEvent: false});
this.securityFormGroup.get('pathToCACert').disable({emitEvent: false});
this.securityFormGroup.get('pathToPrivateKey').disable({emitEvent: false});
this.securityFormGroup.get('pathToClientCert').disable({emitEvent: false});
if (type === BrokerSecurityType.BASIC) {
this.securityFormGroup.get('username').enable({emitEvent: false});
this.securityFormGroup.get('password').enable({emitEvent: false});
} else if (type === BrokerSecurityType.CERTIFICATES) {
this.securityFormGroup.get('pathToCACert').enable({emitEvent: false});
this.securityFormGroup.get('pathToPrivateKey').enable({emitEvent: false});
this.securityFormGroup.get('pathToClientCert').enable({emitEvent: false});
}
}
}
}

97
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html

@ -0,0 +1,97 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-form-panel stroked" [formGroup]="mappingFormGroup">
<div class="tb-form-panel-title" [class.tb-required]="required" translate>device.device</div>
<div class="tb-form-table no-padding no-gap">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell table-name-column" translate>gateway.device-info.entity-field</div>
<div *ngIf="useSource" class="tb-form-table-header-cell table-column" translate>gateway.device-info.source</div>
<div class="tb-form-table-header-cell table-column" translate>
gateway.device-info.expression
</div>
</div>
<div class="tb-form-table-body no-gap">
<div class="tb-form-table-row tb-form-row no-border same-padding top-same-padding"
[class.bottom-same-padding]="deviceInfoType !== DeviceInfoType.FULL">
<div class="fixed-title-width" translate>gateway.device-info.name</div>
<div class="tb-flex no-gap raw-value-option" *ngIf="useSource">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="deviceNameExpressionSource">
<mat-option *ngFor="let type of sourceTypes" [value]="type">
{{ SourceTypeTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-table-row-cell tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceNameExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-info.device-name-expression-required') | translate"
*ngIf="mappingFormGroup.get('deviceNameExpression').hasError('required') &&
mappingFormGroup.get('deviceNameExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
<div class="tb-form-table-row tb-form-row no-border same-padding bottom-same-padding"
*ngIf="deviceInfoType === DeviceInfoType.FULL">
<div class="fixed-title-width" translate>gateway.device-info.profile-name</div>
<div class="tb-flex no-gap raw-value-option" *ngIf="useSource">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="deviceProfileExpressionSource">
<mat-option *ngFor="let type of sourceTypes" [value]="type">
{{ SourceTypeTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-table-row-cell tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceProfileExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-info.device-profile-expression-required') | translate"
*ngIf="mappingFormGroup.get('deviceProfileExpression').hasError('required') &&
mappingFormGroup.get('deviceProfileExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
</div>
</div>
</div>

57
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss

@ -0,0 +1,57 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
display: block;
.tb-form-row {
&.bottom-same-padding {
padding-bottom: 16px;
}
&.top-same-padding {
padding-top: 16px;
}
.fixed-title-width {
width: 19%;
}
}
.table-column {
width: 40%;
}
.table-name-column {
width: 20%;
}
.raw-name {
width: 19%;
}
.raw-value-option {
max-width: 40%;
}
}
:host ::ng-deep {
.mat-mdc-form-field-icon-suffix {
display: flex;
}
}

179
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts

@ -0,0 +1,179 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
Input,
NgZone,
OnDestroy,
OnInit,
ViewContainerRef
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Overlay } from '@angular/cdk/overlay';
import { UtilsService } from '@core/services/utils.service';
import { EntityService } from '@core/http/entity.service';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
DeviceInfoType,
noLeadTrailSpacesRegex,
SourceTypes,
SourceTypeTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-device-info-table',
templateUrl: './device-info-table.component.html',
styleUrls: ['./device-info-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceInfoTableComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DeviceInfoTableComponent),
multi: true
}
]
})
export class DeviceInfoTableComponent extends PageComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
SourceTypeTranslationsMap = SourceTypeTranslationsMap;
DeviceInfoType = DeviceInfoType;
@coerceBoolean()
@Input()
useSource = true;
@coerceBoolean()
@Input()
required = false;
@Input()
sourceTypes: Array<SourceTypes> = Object.values(SourceTypes);
deviceInfoTypeValue: any;
get deviceInfoType(): any {
return this.deviceInfoTypeValue;
}
@Input()
set deviceInfoType(value: any) {
if (this.deviceInfoTypeValue !== value) {
this.deviceInfoTypeValue = value;
}
}
mappingFormGroup: UntypedFormGroup;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(protected store: Store<AppState>,
public translate: TranslateService,
public dialog: MatDialog,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private dialogService: DialogService,
private entityService: EntityService,
private utils: UtilsService,
private zone: NgZone,
private cd: ChangeDetectorRef,
private elementRef: ElementRef,
private fb: FormBuilder) {
super(store);
}
ngOnInit() {
this.mappingFormGroup = this.fb.group({
deviceNameExpression: ['', this.required ?
[Validators.required, Validators.pattern(noLeadTrailSpacesRegex)] : [Validators.pattern(noLeadTrailSpacesRegex)]]
});
if (this.useSource) {
this.mappingFormGroup.addControl('deviceNameExpressionSource',
this.fb.control(SourceTypes.MSG, []));
}
if (this.deviceInfoType === DeviceInfoType.FULL) {
if (this.useSource) {
this.mappingFormGroup.addControl('deviceProfileExpressionSource',
this.fb.control(SourceTypes.MSG, []));
}
this.mappingFormGroup.addControl('deviceProfileExpression',
this.fb.control('', this.required ?
[Validators.required, Validators.pattern(noLeadTrailSpacesRegex)] : [Validators.pattern(noLeadTrailSpacesRegex)]));
}
this.mappingFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateView(value);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
writeValue(deviceInfo: any) {
this.mappingFormGroup.patchValue(deviceInfo, {emitEvent: false});
}
validate(): ValidationErrors | null {
return this.mappingFormGroup.valid ? null : {
mappingForm: { valid: false }
};
}
updateView(value: any) {
this.propagateChange(value);
}
}

128
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts

@ -0,0 +1,128 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
Directive,
ElementRef,
Inject,
Input,
OnDestroy,
Renderer2
} from '@angular/core';
import { isEqual } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
import { WINDOW } from '@core/services/window.service';
import { fromEvent, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: '[tb-ellipsis-chip-list]'
})
export class EllipsisChipListDirective implements OnDestroy {
chipsValue: string[];
private destroy$ = new Subject<void>();
@Input('tb-ellipsis-chip-list')
set chips(value: string[]) {
if (!isEqual(this.chipsValue, value)) {
this.chipsValue = value;
setTimeout(() => {
this.adjustChips();
}, 0);
}
}
constructor(private el: ElementRef,
private renderer: Renderer2,
private translate: TranslateService,
@Inject(WINDOW) private window: Window) {
this.renderer.setStyle(this.el.nativeElement, 'max-height', '48px');
this.renderer.setStyle(this.el.nativeElement, 'overflow', 'auto');
fromEvent(window, 'resize').pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.adjustChips();
});
}
private adjustChips(): void {
const chipListElement = this.el.nativeElement;
const ellipsisChip = this.el.nativeElement.querySelector('.ellipsis-chip');
const margin = parseFloat(this.window.getComputedStyle(ellipsisChip).marginLeft) || 0;
const chipNodes = chipListElement.querySelectorAll('mat-chip:not(.ellipsis-chip)');
if (this.chipsValue.length > 1) {
const ellipsisText = this.el.nativeElement.querySelector('.ellipsis-text');
this.renderer.setStyle(ellipsisChip, 'display', 'inline-flex');
ellipsisText.innerHTML = this.translate.instant('gateway.ellipsis-chips-text',
{count: (this.chipsValue.length)});
const availableWidth = chipListElement.offsetWidth - (ellipsisChip.offsetWidth + margin);
let usedWidth = 0;
let visibleChipsCount = 0;
chipNodes.forEach((chip) => {
this.renderer.setStyle(chip, 'display', 'inline-flex');
const textLabelContainer = chip.querySelector('.mdc-evolution-chip__text-label');
this.applyMaxChipTextWidth(textLabelContainer, (availableWidth / 3));
if ((usedWidth + (chip.offsetWidth + margin) <= availableWidth) && (visibleChipsCount < this.chipsValue.length)) {
visibleChipsCount++;
usedWidth += chip.offsetWidth + margin;
} else {
this.renderer.setStyle(chip, 'display', 'none');
}
});
ellipsisText.innerHTML = this.translate.instant('gateway.ellipsis-chips-text',
{count: (this.chipsValue.length - visibleChipsCount)});
if (visibleChipsCount === this.chipsValue?.length) {
this.renderer.setStyle(ellipsisChip, 'display', 'none');
}
} else if (this.chipsValue.length === 1) {
const chipLabelContainer = chipNodes[0].querySelector('.mdc-evolution-chip__action');
const textLabelContainer = chipLabelContainer.querySelector('.mdc-evolution-chip__text-label');
const leftPadding = parseFloat(this.window.getComputedStyle(chipLabelContainer).paddingLeft) || 0;
const rightPadding = parseFloat(this.window.getComputedStyle(chipLabelContainer).paddingRight) || 0;
const computedTextWidth = chipListElement.offsetWidth - margin -
(leftPadding + rightPadding);
this.renderer.setStyle(ellipsisChip, 'display', 'none');
this.renderer.setStyle(chipNodes[0], 'display', 'inline-flex');
this.applyMaxChipTextWidth(textLabelContainer, computedTextWidth);
} else {
this.renderer.setStyle(ellipsisChip, 'display', 'none');
}
}
private applyMaxChipTextWidth(element: HTMLElement, widthLimit: number): void {
this.renderer.setStyle(element, 'max-width', widthLimit + 'px');
this.renderer.setStyle(element, 'overflow', 'hidden');
this.renderer.setStyle(element, 'text-overflow', 'ellipsis');
this.renderer.setStyle(element, 'white-space', 'nowrap');
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

195
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html

@ -0,0 +1,195 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-mapping-keys-panel">
<div class="tb-form-panel no-border no-padding">
<div class="tb-form-panel-title">{{ panelTitle | translate }}{{' (' + keysListFormArray.controls.length + ')'}}</div>
<div class="tb-form-panel no-border no-padding key-panel" *ngIf="keysListFormArray.controls.length; else noKeys">
<div class="tb-form-panel no-border no-padding tb-flex no-flex row center fill-width"
*ngFor="let keyControl of keysListFormArray.controls; trackBy: trackByKey; let $index = index; let last = last;">
<div class="tb-form-panel stroked tb-flex">
<ng-container [formGroup]="keyControl">
<mat-expansion-panel class="tb-settings" [expanded]="last">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<div class="title-container">{{ keyControl.get('key').value }}</div>
{{ '-' }}
<div class="title-container">{{ valueTitle(keyControl.get('value').value) }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-panel no-border no-padding"
*ngIf="keysType !== MappingKeysType.CUSTOM; else customPanel">
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.platform-side</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}" translate>
gateway.key
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.key-required') | translate"
*ngIf="keyControl.get('key').hasError('required') &&
keyControl.get('key').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.connector-side</div>
<div class="tb-form-row">
<div class="fixed-title-width tb-required" translate>gateway.type</div>
<mat-form-field class="tb-flex no-gap fill-width" appearance="outline" subscriptSizing="dynamic">
<mat-select name="valueType" formControlName="type">
<mat-select-trigger *ngIf="!rawData">
<div class="tb-flex align-center">
<mat-icon class="tb-mat-18" [svgIcon]="valueTypes.get(keyControl.get('type').value)?.icon">
</mat-icon>
<span>
{{ (rawData ? 'gateway.raw' : valueTypes.get(keyControl.get('type').value)?.name) | translate}}
</span>
</div>
</mat-select-trigger>
<ng-container *ngIf="!rawData; else rawOption">
<mat-option *ngFor="let valueType of valueTypeKeys" [value]="valueType">
<mat-icon class="tb-mat-20" svgIcon="{{ valueTypes.get(valueType).icon }}">
</mat-icon>
<span>{{ valueTypes.get(valueType).name | translate }}</span>
</mat-option>
</ng-container>
<ng-template #rawOption>
<mat-option [value]="'raw'">
<span>{{ 'gateway.raw' | translate }}</span>
</mat-option>
</ng-template>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}" translate>
gateway.value
</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-flex no-gap">
<input matInput required formControlName="value"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.value-required') | translate"
*ngIf="keyControl.get('value').hasError('required') &&
keyControl.get('value').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
</div>
<ng-template #customPanel>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.key</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.key-required') | translate"
*ngIf="keyControl.get('key').hasError('required') && keyControl.get('key').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.value</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-inline-field flex tb-suffix-absolute">
<input matInput required formControlName="value"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.value-required') | translate"
*ngIf="keyControl.get('value').hasError('required') && keyControl.get('value').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</ng-template>
</ng-template>
</mat-expansion-panel>
</ng-container>
</div>
<button type="button"
mat-icon-button
(click)="deleteKey($event, $index)"
[matTooltip]="deleteKeyTitle | translate"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
<div>
<button type="button" mat-stroked-button color="primary" (click)="addKey()">
{{ addKeyTitle | translate }}
</button>
</div>
</div>
<ng-template #noKeys>
<div class="tb-flex no-flex center align-center key-panel">
<span class="tb-prompt" translate>{{ noKeysText }}</span>
</div>
</ng-template>
<div class="tb-flex flex-end">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyKeysData()"
[disabled]="keysListFormArray.invalid || !keysListFormArray.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

60
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss

@ -0,0 +1,60 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-mapping-keys-panel {
width: 77vw;
max-width: 700px;
.title-container {
max-width: 11vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
.key-panel {
height: 500px;
overflow: auto;
}
tb-value-input {
width: 100%;
}
.tb-form-panel {
.mat-mdc-icon-button {
width: 56px;
height: 56px;
padding: 16px;
color: rgba(0, 0, 0, 0.54);
}
}
.see-example {
width: 32px;
height: 32px;
margin: 4px;
}
}
}
:host ::ng-deep {
.mat-mdc-form-field-icon-suffix {
display: flex;
}
}

173
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts

@ -0,0 +1,173 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
Component,
EventEmitter,
Input,
OnInit,
Output
} from '@angular/core';
import {
AbstractControl,
UntypedFormArray,
UntypedFormBuilder,
Validators
} from '@angular/forms';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
import { isDefinedAndNotNull } from '@core/utils';
import {
MappingDataKey,
MappingKeysType,
MappingValueType,
mappingValueTypesMap,
noLeadTrailSpacesRegex
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@Component({
selector: 'tb-mapping-data-keys-panel',
templateUrl: './mapping-data-keys-panel.component.html',
styleUrls: ['./mapping-data-keys-panel.component.scss'],
providers: []
})
export class MappingDataKeysPanelComponent extends PageComponent implements OnInit {
@Input()
panelTitle: string;
@Input()
addKeyTitle: string;
@Input()
deleteKeyTitle: string;
@Input()
noKeysText: string;
@Input()
keys: Array<MappingDataKey> | {[key: string]: any};
@Input()
keysType: string;
@Input()
@coerceBoolean()
rawData = false;
@Input()
popover: TbPopoverComponent<MappingDataKeysPanelComponent>;
@Output()
keysDataApplied = new EventEmitter<Array<MappingDataKey> | {[key: string]: any}>();
valueTypeKeys = Object.values(MappingValueType);
MappingKeysType = MappingKeysType;
valueTypeEnum = MappingValueType;
valueTypes = mappingValueTypesMap;
dataKeyType: DataKeyType;
keysListFormArray: UntypedFormArray;
errorText = '';
constructor(private fb: UntypedFormBuilder,
protected store: Store<AppState>) {
super(store);
}
ngOnInit() {
this.keysListFormArray = this.prepareKeysFormArray(this.keys)
}
trackByKey(index: number, keyControl: AbstractControl): any {
return keyControl;
}
addKey(): void {
const dataKeyFormGroup = this.fb.group({
key: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]]
});
if (this.keysType !== MappingKeysType.CUSTOM) {
dataKeyFormGroup.addControl('type', this.fb.control(this.rawData ? 'raw' : MappingValueType.STRING));
}
this.keysListFormArray.push(dataKeyFormGroup);
}
deleteKey($event: Event, index: number): void {
if ($event) {
$event.stopPropagation();
}
this.keysListFormArray.removeAt(index);
this.keysListFormArray.markAsDirty();
}
cancel() {
this.popover?.hide();
}
applyKeysData() {
let keys = this.keysListFormArray.value;
if (this.keysType === MappingKeysType.CUSTOM) {
keys = {};
for (let key of this.keysListFormArray.value) {
keys[key.key] = key.value;
}
}
this.keysDataApplied.emit(keys);
}
private prepareKeysFormArray(keys: Array<MappingDataKey> | {[key: string]: any}): UntypedFormArray {
const keysControlGroups: Array<AbstractControl> = [];
if (keys) {
if (this.keysType === MappingKeysType.CUSTOM) {
keys = Object.keys(keys).map(key => {
return {key, value: keys[key], type: ''};
});
}
keys.forEach((keyData) => {
const { key, value, type } = keyData;
const dataKeyFormGroup = this.fb.group({
key: [key, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [type, []]
});
keysControlGroups.push(dataKeyFormGroup);
});
}
return this.fb.array(keysControlGroups);
}
valueTitle(value: any): string {
if (isDefinedAndNotNull(value)) {
if (typeof value === 'object') {
return JSON.stringify(value);
}
return value;
}
return '';
}
}

123
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html

@ -0,0 +1,123 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-mapping-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="tb-mapping-table-content">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode">
<div class="mat-toolbar-tools">
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">
<span class="tb-mapping-table-title">{{mappingTypeTranslationsMap.get(mappingType) | translate}}</span>
</div>
<span fxFlex></span>
<button mat-icon-button
(click)="manageMapping($event)"
matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
<button mat-icon-button
(click)="enterFilterMode()"
matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
</div>
</mat-toolbar>
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="textSearchMode">
<div class="mat-toolbar-tools">
<button mat-icon-button
matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
<mat-form-field fxFlex>
<mat-label>&nbsp;</mat-label>
<input #searchInput matInput
[formControl]="textSearch"
placeholder="{{ 'common.enter-search' | translate }}"/>
</mat-form-field>
<button mat-icon-button (click)="exitFilterMode()"
matTooltip="{{ 'action.close' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</mat-toolbar>
<div class="table-container">
<table mat-table [dataSource]="dataSource">
<ng-container [matColumnDef]="column.def" *ngFor="let column of mappingColumns; let i = index">
<mat-header-cell *matHeaderCellDef class="table-value-column" [class.request-column]="mappingType === mappingTypeEnum.REQUESTS">
{{ column.title | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column" [class.request-column]="mappingType === mappingTypeEnum.REQUESTS">
{{ mapping[column.def] }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px', textAlign: 'center'}">
</mat-header-cell>
<mat-cell *matCellDef="let mapping; let i = index"
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px'}">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button
(click)="manageMapping($event, i)">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
(click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon>
</button>
</div>
<div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button
(click)="$event.stopPropagation()"
[matMenuTriggerFor]="cellActionsMenu">
<mat-icon class="material-icons">more_vert</mat-icon>
</button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-icon-button
(click)="manageMapping($event, i)">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
(click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon>
</button>
</mat-menu>
</div>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row *matRowDef="let mapping; columns: displayedColumns;"></mat-row>
</table>
<section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center"
class="mat-headline-5 tb-absolute-fill tb-add-new">
<button mat-button class="connector"
(click)="manageMapping($event)">
<mat-icon class="tb-mat-96">add</mat-icon>
<span>{{ 'gateway.add-mapping' | translate }}</span>
</button>
</section>
</div>
<span [fxShow]="textSearchMode && (dataSource.isEmpty() | async)"
fxLayoutAlign="center center"
class="no-data-found" translate>
widget.no-data-found
</span>
</div>
</div>

101
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss

@ -0,0 +1,101 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../scss/constants';
:host {
width: 100%;
height: 100%;
display: block;
.tb-mapping-table {
.tb-mapping-table-content {
width: 100%;
height: 100%;
background: #fff;
overflow: hidden;
&.tb-outlined-border {
box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%);
border: solid 1px #e0e0e0;
border-radius: 4px;
}
.mat-toolbar-tools{
min-height: auto;
}
.title-container{
overflow: hidden;
}
.tb-mapping-table-title {
padding-right: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.table-container {
overflow: auto;
.mat-mdc-table {
table-layout: fixed;
min-width: 450px;
.table-value-column {
padding: 0 12px;
width: 23%;
&.request-column {
width: 38%;
}
}
}
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.no-data-found {
height: calc(100% - 120px);
}
@media #{$mat-xs} {
.mat-toolbar {
height: auto;
min-height: 100px;
.tb-mapping-table-title{
padding-bottom: 5px;
width: 100%;
}
}
}
}
:host ::ng-deep {
mat-cell.tb-value-cell {
cursor: pointer;
.mat-icon {
height: 24px;
width: 24px;
font-size: 24px;
color: #757575
}
}
}

339
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts

@ -0,0 +1,339 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
Input,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
import { Overlay } from '@angular/cdk/overlay';
import { UtilsService } from '@core/services/utils.service';
import { EntityService } from '@core/http/entity.service';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR, UntypedFormArray } from '@angular/forms';
import {
ConvertorTypeTranslationsMap,
MappingInfo,
MappingType,
MappingTypeTranslationsMap,
RequestType,
RequestTypesTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dialog/mapping-dialog.component';
import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
@Component({
selector: 'tb-mapping-table',
templateUrl: './mapping-table.component.html',
styleUrls: ['./mapping-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MappingTableComponent),
multi: true
}
]
})
export class MappingTableComponent extends PageComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy {
mappingTypeTranslationsMap = MappingTypeTranslationsMap;
mappingTypeEnum = MappingType;
displayedColumns = [];
mappingColumns = [];
textSearchMode = false;
dataSource: MappingDatasource;
hidePageSize = false;
activeValue = false;
dirtyValue = false;
viewsInited = false;
mappingTypeValue: MappingType;
get mappingType(): MappingType {
return this.mappingTypeValue;
}
@Input()
set mappingType(value: MappingType) {
if (this.mappingTypeValue !== value) {
this.mappingTypeValue = value;
}
}
@ViewChild('searchInput') searchInputField: ElementRef;
mappingFormGroup: UntypedFormArray;
textSearch = this.fb.control('', {nonNullable: true});
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(protected store: Store<AppState>,
public translate: TranslateService,
public dialog: MatDialog,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private dialogService: DialogService,
private entityService: EntityService,
private utils: UtilsService,
private zone: NgZone,
private cd: ChangeDetectorRef,
private elementRef: ElementRef,
private fb: FormBuilder) {
super(store);
this.mappingFormGroup = this.fb.array([]);
this.dirtyValue = !this.activeValue;
this.dataSource = new MappingDatasource();
}
ngOnInit() {
if (this.mappingType === MappingType.DATA) {
this.mappingColumns.push(
{def: 'topicFilter', title: 'gateway.topic-filter'},
{def: 'QoS', title: 'gateway.mqtt-qos'},
{def: 'converter', title: 'gateway.payload-type'}
)
} else {
this.mappingColumns.push(
{def: 'type', title: 'gateway.type'},
{def: 'details', title: 'gateway.details'}
);
}
this.displayedColumns.push(...this.mappingColumns.map(column => column.def), 'actions');
this.mappingFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateTableData(value);
this.updateView(value);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
ngAfterViewInit() {
this.textSearch.valueChanges.pipe(
debounceTime(150),
distinctUntilChanged((prev, current) => (prev ?? '') === current.trim()),
takeUntil(this.destroy$)
).subscribe((text) => {
const searchText = text.trim();
this.updateTableData(this.mappingFormGroup.value, searchText.trim())
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
writeValue(config: any) {
if (isUndefinedOrNull(config)) {
config = this.mappingType === MappingType.REQUESTS ? {} : [];
}
let mappingConfigs = config;
if (this.mappingType === MappingType.REQUESTS) {
mappingConfigs = [];
Object.keys(config).forEach((configKey) => {
for (let mapping of config[configKey]) {
mappingConfigs.push({
requestType: configKey,
requestValue: mapping
});
}
});
}
this.mappingFormGroup.clear({emitEvent: false});
for (let mapping of mappingConfigs) {
this.mappingFormGroup.push(this.fb.group(mapping), {emitEvent: false});
}
this.updateTableData(mappingConfigs);
}
updateView(mappingConfigs: Array<{[key: string]: any}>) {
let config;
if (this.mappingType === MappingType.REQUESTS) {
config = {};
for (let mappingConfig of mappingConfigs) {
if (config[mappingConfig.requestType]) {
config[mappingConfig.requestType].push(mappingConfig.requestValue);
} else {
config[mappingConfig.requestType] = [mappingConfig.requestValue];
}
}
} else {
config = mappingConfigs;
}
this.propagateChange(config);
}
enterFilterMode() {
this.textSearchMode = true;
setTimeout(() => {
this.searchInputField.nativeElement.focus();
this.searchInputField.nativeElement.setSelectionRange(0, 0);
}, 10);
}
exitFilterMode() {
this.updateTableData(this.mappingFormGroup.value);
this.textSearchMode = false;
this.textSearch.reset();
}
manageMapping($event: Event, index?: number) {
if ($event) {
$event.stopPropagation();
}
const value = isDefinedAndNotNull(index) ? this.mappingFormGroup.at(index).value : {};
this.dialog.open<MappingDialogComponent, MappingInfo, boolean>(MappingDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
mappingType: this.mappingType,
value,
buttonTitle: isUndefinedOrNull(index) ? 'action.add' : 'action.apply'
}
}).afterClosed().subscribe(
(res) => {
if (res) {
if (isDefinedAndNotNull(index)) {
this.mappingFormGroup.at(index).patchValue(res);
} else {
this.mappingFormGroup.push(this.fb.group(res));
}
}
}
);
}
updateTableData(value: Array<{[key: string]: any}>, textSearch?: string): void {
let tableValue = value;
if (this.mappingType === MappingType.DATA) {
tableValue = tableValue.map((value) => {
return {
topicFilter: value.topicFilter,
QoS: value.subscriptionQos,
converter: this.translate.instant(ConvertorTypeTranslationsMap.get(value.converter.type))
};
});
} else {
tableValue = tableValue.map((value) => {
let details;
if (value.requestType === RequestType.ATTRIBUTE_UPDATE) {
details = value.requestValue.attributeFilter;
} else if (value.requestType === RequestType.SERVER_SIDE_RPC) {
details = value.requestValue.methodFilter;
} else {
details = value.requestValue.topicFilter;
}
return {
type: this.translate.instant(RequestTypesTranslationsMap.get(value.requestType)),
details
};
});
}
if (textSearch) {
tableValue = tableValue.filter(value =>
Object.values(value).some(val =>
val.toString().toLowerCase().includes(textSearch.toLowerCase())
)
);
}
this.dataSource.loadMappings(tableValue);
}
deleteMapping($event: Event, index: number) {
if ($event) {
$event.stopPropagation();
}
this.dialogService.confirm(
this.translate.instant('gateway.delete-mapping-title'),
'',
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
).subscribe((result) => {
if (result) {
this.mappingFormGroup.removeAt(index);
}
});
}
}
export class MappingDatasource implements DataSource<{[key: string]: any}> {
private mappingSubject = new BehaviorSubject<Array<{[key: string]: any}>>([]);
private allMappings: Observable<Array<{[key: string]: any}>>;
constructor() {}
connect(collectionViewer: CollectionViewer): Observable<Array<{[key: string]: any}>> {
return this.mappingSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.mappingSubject.complete();
}
loadMappings(mappings: Array<{[key: string]: any}>, pageLink?: PageLink, reload: boolean = false): void {
if (reload) {
this.allMappings = null;
}
this.mappingSubject.next(mappings);
}
isEmpty(): Observable<boolean> {
return this.mappingSubject.pipe(
map((mappings) => !mappings.length)
);
}
total(): Observable<number> {
return this.mappingSubject.pipe(
map((mappings) => mappings.length)
);
}
}

100
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html

@ -0,0 +1,100 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div [formGroup]="connectorForm" class="add-connector">
<mat-toolbar color="primary">
<h2>{{ "gateway.add-connector" | translate}}</h2>
<span fxFlex></span>
<div [tb-help]="helpLinkId()"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<div class="tb-form-panel no-border no-padding" fxLayout="column">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.type</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
<mat-option *ngFor="let type of gatewayConnectorDefaultTypesTranslatesMap | keyvalue" [value]="type.key">
{{ type.value }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(connectorForm.get('name').hasError('duplicateName') ?
'gateway.connector-duplicate-name' :'gateway.name-required') | translate"
*ngIf="(connectorForm.get('name').hasError('required') && connectorForm.get('name').touched)
|| connectorForm.get('name').hasError('duplicateName')"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.remote-logging-level</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="logLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{ logLevel }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="useDefaults">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.fill-connector-defaults-hint' | translate }}">
{{ 'gateway.fill-connector-defaults' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div *ngIf="connectorForm.get('type').value === connectorType.MQTT" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="sendDataOnlyOnChange">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.send-change-data-hint' | translate }}">
{{ 'gateway.send-change-data' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
cdkFocusInitial
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="connectorForm.invalid || !connectorForm.dirty">
{{ 'action.add' | translate }}
</button>
</div>
</div>

22
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss

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

122
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts

@ -0,0 +1,122 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnDestroy } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, UntypedFormControl, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms';
import { BaseData, HasId } from '@shared/models/base-data';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import {
AddConnectorConfigData,
ConnectorType,
CreatedConnectorConfigData,
GatewayConnectorDefaultTypesTranslatesMap,
GatewayLogLevel,
getDefaultConfig,
noLeadTrailSpacesRegex
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Subject } from 'rxjs';
import { ResourcesService } from '@core/services/resources.service';
@Component({
selector: 'tb-add-connector-dialog',
templateUrl: './add-connector-dialog.component.html',
styleUrls: ['./add-connector-dialog.component.scss'],
providers: [],
})
export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDialogComponent, BaseData<HasId>> implements OnDestroy {
connectorForm: UntypedFormGroup;
connectorType = ConnectorType;
gatewayConnectorDefaultTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap;
gatewayLogLevel = Object.values(GatewayLogLevel);
submitted = false;
private destroy$ = new Subject<void>();
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddConnectorConfigData,
public dialogRef: MatDialogRef<AddConnectorDialogComponent, CreatedConnectorConfigData>,
private fb: FormBuilder,
private resourcesService: ResourcesService) {
super(store, router, dialogRef);
this.connectorForm = this.fb.group({
type: [ConnectorType.MQTT, []],
name: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(noLeadTrailSpacesRegex)]],
logLevel: [GatewayLogLevel.INFO, []],
useDefaults: [true, []],
sendDataOnlyOnChange: [false, []],
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
helpLinkId(): string {
return 'https://thingsboard.io/docs/iot-gateway/configuration/';
}
cancel(): void {
this.dialogRef.close(null);
}
add(): void {
this.submitted = true;
const value = this.connectorForm.getRawValue();
if (value.useDefaults) {
getDefaultConfig(this.resourcesService, value.type).subscribe((defaultConfig) => {
value.configurationJson = defaultConfig;
if (this.connectorForm.valid) {
this.dialogRef.close(value);
}
});
}
if (this.connectorForm.valid) {
this.dialogRef.close(value);
}
}
private uniqNameRequired(): ValidatorFn {
return (c: UntypedFormControl) => {
const newName = c.value.trim().toLowerCase();
const found = this.data.dataSourceData.find((connectorAttr) => {
const connectorData = connectorAttr.value;
return connectorData.name.toLowerCase() === newName;
});
if (found) {
if (c.hasError('required')) {
return c.getError('required');
}
return {
duplicateName: {
valid: false
}
};
}
return null;
};
}
}

634
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html

@ -0,0 +1,634 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div [formGroup]="mappingForm" class="key-mapping">
<mat-toolbar color="primary">
<h2>{{ MappingTypeTranslationsMap.get(this.data?.mappingType) | translate}}</h2>
<span fxFlex></span>
<div [tb-help]="helpLinkId()"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<div class="tb-form-panel no-border no-padding" fxLayout="column">
<div class="tb-form-hint tb-primary-fill">
{{ MappingHintTranslationsMap.get(this.data?.mappingType) | translate }}
</div>
<ng-container [ngSwitch]="data.mappingType">
<ng-template [ngSwitchCase]="MappingType.DATA">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.topic-filter</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="topicFilter" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.topic-required') | translate"
*ngIf="mappingForm.get('topicFilter').hasError('required') &&
mappingForm.get('topicFilter').touched;"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/topic-filter_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}" translate>
gateway.mqtt-qos
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="subscriptionQos">
<mat-option *ngFor="let type of qualityTypes" [value]="type">
{{ QualityTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<ng-container formGroupName="converter">
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.payload-type</div>
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option *ngFor="let type of convertorTypes" [value]="type">
{{ ConvertorTypeTranslationsMap.get(type) | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.data-conversion</div>
<div class="tb-form-hint tb-primary-fill">
{{ DataConversionTranslationsMap.get(converterType) | translate }}
</div>
<ng-container [formGroupName]="converterType" [ngSwitch]="converterType">
<ng-template [ngSwitchCase]="ConvertorTypeEnum.JSON">
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.FULL" required="true">
</tb-device-info-table>
</ng-template>
<ng-template [ngSwitchCase]="ConvertorTypeEnum.BYTES">
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.FULL"
[sourceTypes]="[sourceTypesEnum.MSG, sourceTypesEnum.CONST]" required="true">
</tb-device-info-table>
</ng-template>
<div class="tb-form-panel no-border no-padding"
*ngIf="converterType === ConvertorTypeEnum.BYTES || converterType === ConvertorTypeEnum.JSON">
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.attributes</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox [tb-ellipsis-chip-list]="converterAttributes" class="tb-flex">
<mat-chip *ngFor="let attribute of converterAttributes">
{{ attribute }}
</mat-chip>
<mat-chip class="mat-mdc-chip ellipsis-chip">
<label class="ellipsis-text"></label>
</mat-chip>
</mat-chip-listbox>
<button type="button"
mat-icon-button
color="primary"
#attributesButton
(click)="manageKeys($event, attributesButton, MappingKeysType.ATTRIBUTES)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.timeseries</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox class="tb-flex" [tb-ellipsis-chip-list]="converterTelemetry">
<mat-chip *ngFor="let telemetry of converterTelemetry">
{{ telemetry }}
</mat-chip>
<mat-chip class="mat-mdc-chip ellipsis-chip">
<label class="ellipsis-text"></label>
</mat-chip>
</mat-chip-listbox>
<button type="button"
mat-icon-button
color="primary"
#telemetryButton
(click)="manageKeys($event, telemetryButton, MappingKeysType.TIMESERIES)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
</div>
<div class="tb-form-panel no-border no-padding" *ngIf="converterType === ConvertorTypeEnum.CUSTOM">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.extension-hint' | translate }}" translate>
gateway.extension
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="extension" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.extension-required') | translate"
*ngIf="mappingForm.get('converter.custom.extension').hasError('required') &&
mappingForm.get('converter.custom.extension').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between same-padding tb-flex column">
<div class="tb-form-panel-title" translate>gateway.extension-configuration</div>
<div class="tb-form-hint tb-primary-fill">{{ 'gateway.extension-configuration-hint' | translate }}</div>
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.keys</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox [tb-ellipsis-chip-list]="customKeys" class="tb-flex">
<mat-chip *ngFor="let telemetry of customKeys">
{{ telemetry }}
</mat-chip>
<mat-chip class="mat-mdc-chip ellipsis-chip">
<label class="ellipsis-text"></label>
</mat-chip>
</mat-chip-listbox>
<button type="button"
mat-icon-button
color="primary"
#keysButton
(click)="manageKeys($event, keysButton, MappingKeysType.CUSTOM)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
</div>
</div>
</ng-container>
</div>
</ng-container>
</ng-template>
<ng-template [ngSwitchCase]="MappingType.REQUESTS">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.request-type</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="requestType">
<mat-option *ngFor="let type of requestTypes" [value]="type">
{{ RequestTypesTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<ng-container formGroupName="requestValue">
<ng-container [formGroup]="mappingForm.get('requestValue').get(requestMappingType)" [ngSwitch]="requestMappingType">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"
*ngIf="requestMappingType === RequestTypeEnum.ATTRIBUTE_REQUEST ||
requestMappingType === RequestTypeEnum.CONNECT_REQUEST ||
requestMappingType === RequestTypeEnum.DISCONNECT_REQUEST">
<div class="fixed-title-width tb-required" translate>gateway.topic-filter</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" [formControl]="mappingForm.get('requestValue').get(requestMappingType).get('topicFilter')"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.topic-required') | translate"
*ngIf="mappingForm.get('requestValue').get(requestMappingType).get('topicFilter').hasError('required') &&
mappingForm.get('requestValue').get(requestMappingType).get('topicFilter').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/topic-filter_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
<ng-template [ngSwitchCase]="RequestTypeEnum.CONNECT_REQUEST">
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.FULL" required="true">
</tb-device-info-table>
</ng-template>
<ng-template [ngSwitchCase]="RequestTypeEnum.DISCONNECT_REQUEST">
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.PARTIAL" required="true">
</tb-device-info-table>
</ng-template>
<ng-template [ngSwitchCase]="RequestTypeEnum.ATTRIBUTE_REQUEST">
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title tb-required" translate>gateway.from-device-request-settings</div>
<div class="tb-form-hint tb-primary-fill" translate>
gateway.from-device-request-settings-hint
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center" formGroupName="deviceInfo">
<div class="fixed-title-width tb-flex no-flex align-center" translate>
<div class="tb-required" translate>gateway.device-info.device-name-expression</div>
</div>
<div class="tb-flex">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="deviceNameExpressionSource">
<mat-option *ngFor="let type of sourceTypes" [value]="type">
{{ SourceTypeTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceNameExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-info.device-name-expression-required') | translate"
*ngIf="(mappingForm.get('requestValue.attributeRequests.deviceInfo.deviceNameExpression').hasError('required') &&
mappingForm.get('requestValue.attributeRequests.deviceInfo.deviceNameExpression').touched)"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.attribute-name-expression</div>
<div class="tb-flex">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="attributeNameExpressionSource">
<mat-option *ngFor="let type of sourceTypes" [value]="type">
{{ SourceTypeTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="attributeNameExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.attribute-name-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeRequests.attributeNameExpression').hasError('required') &&
mappingForm.get('requestValue.attributeRequests.attributeNameExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title tb-required" translate>gateway.to-device-response-settings</div>
<div class="tb-form-hint tb-primary-fill" translate>
gateway.to-device-response-settings-hint
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.response-value-expression</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="valueExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.response-value-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeRequests.valueExpression').hasError('required') &&
mappingForm.get('requestValue.attributeRequests.valueExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.response-topic-expression</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="topicExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.response-topic-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeRequests.topicExpression').hasError('required') &&
mappingForm.get('requestValue.attributeRequests.topicExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="retain">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.retain-hint' | translate }}">
{{ 'gateway.retain' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>
</ng-template>
<ng-template [ngSwitchCase]="RequestTypeEnum.ATTRIBUTE_UPDATE">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}" translate>
gateway.device-name-filter
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceNameFilter" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-name-filter-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeUpdates.deviceNameFilter').hasError('required') &&
mappingForm.get('requestValue.attributeUpdates.deviceNameFilter').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.attribute-filter-hint' | translate }}" translate>
gateway.attribute-filter
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="attributeFilter" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.attribute-filter-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeUpdates.attributeFilter').hasError('required') &&
mappingForm.get('requestValue.attributeUpdates.attributeFilter').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.response-value-expression</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="valueExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.response-value-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeUpdates.valueExpression').hasError('required') &&
mappingForm.get('requestValue.attributeUpdates.valueExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.response-topic-expression</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="topicExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.response-topic-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeUpdates.topicExpression').hasError('required') &&
mappingForm.get('requestValue.attributeUpdates.topicExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="retain">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.retain-hint' | translate }}">
{{ 'gateway.retain' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</ng-template>
<ng-template [ngSwitchCase]="RequestTypeEnum.SERVER_SIDE_RPC">
<div class="tb-flex row center align-center no-gap fill-width">
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option [value]="ServerSideRPCType.TWO_WAY">
{{ 'gateway.with-response' | translate }}
</tb-toggle-option>
<tb-toggle-option [value]="ServerSideRPCType.ONE_WAY">
{{ 'gateway.without-response' | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}" translate>
gateway.device-name-filter
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceNameFilter" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-name-filter-required') | translate"
*ngIf="mappingForm.get('requestValue.serverSideRpc.deviceNameFilter').hasError('required') &&
mappingForm.get('requestValue.serverSideRpc.deviceNameFilter').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.method-filter-hint' | translate }}" translate>
gateway.method-filter
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="methodFilter" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.method-filter-required') | translate"
*ngIf="mappingForm.get('requestValue.serverSideRpc.methodFilter').hasError('required') &&
mappingForm.get('requestValue.serverSideRpc.methodFilter').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.request-topic-expression</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="requestTopicExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.request-topic-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.serverSideRpc.requestTopicExpression').hasError('required') &&
mappingForm.get('requestValue.serverSideRpc.requestTopicExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.value-expression</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="valueExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.value-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.serverSideRpc.valueExpression').hasError('required') &&
mappingForm.get('requestValue.serverSideRpc.valueExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
<ng-container *ngIf="mappingForm.get('requestValue.serverSideRpc.type').value === ServerSideRPCType.TWO_WAY">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.response-topic-expression</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="responseTopicExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.response-topic-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.serverSideRpc.responseTopicExpression').hasError('required') &&
mappingForm.get('requestValue.serverSideRpc.responseTopicExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}" translate>
gateway.response-topic-Qos
</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="responseTopicQoS">
<mat-option *ngFor="let type of qualityTypes" [value]="type">
{{ QualityTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.response-timeout</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" type="number" min="1" formControlName="responseTimeout"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="responseTimeoutErrorTooltip"
*ngIf="(mappingForm.get('requestValue.serverSideRpc.responseTimeout').hasError('required') ||
mappingForm.get('requestValue.serverSideRpc.responseTimeout').hasError('min')) &&
mappingForm.get('requestValue.serverSideRpc.responseTimeout').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</ng-container>
</ng-template>
</ng-container>
</ng-container>
</ng-template>
</ng-container>
</div>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
cdkFocusInitial
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="mappingForm.invalid || !mappingForm.dirty || !keysPopupClosed">
{{ this.data.buttonTitle | translate }}
</button>
</div>
</div>

76
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss

@ -0,0 +1,76 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
display: grid;
height: 100%;
.key-mapping {
max-width: 900px;
display: flex;
flex-direction: column;
.mat-toolbar {
min-height: 64px;
}
tb-toggle-select {
padding: 4px 0;
}
}
.mat-mdc-dialog-content {
max-height: 670px;
height: 670px;
}
.ellipsis-chips-container {
max-width: 70%;
}
}
:host ::ng-deep {
.key-mapping {
.mat-mdc-chip-listbox {
.mdc-evolution-chip-set__chips {
justify-content: flex-end;
align-items: center;
}
}
}
.tb-form-row {
.fixed-title-width {
min-width: 40px;
width: 35%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mat-mdc-form-field {
width: 0;
}
}
.see-example {
width: 32px;
height: 32px;
margin: 4px;
}
.mat-mdc-form-field-icon-suffix {
display: flex;
}
}

360
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts

@ -0,0 +1,360 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnDestroy, Renderer2, ViewContainerRef } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { BaseData, HasId } from '@shared/models/base-data';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import {
ConvertorType,
ConvertorTypeTranslationsMap,
DataConversionTranslationsMap,
DeviceInfoType,
MappingHintTranslationsMap,
MappingInfo,
MappingKeysAddKeyTranslationsMap,
MappingKeysDeleteKeyTranslationsMap,
MappingKeysNoKeysTextTranslationsMap,
MappingKeysPanelTitleTranslationsMap,
MappingKeysType,
MappingType,
MappingTypeTranslationsMap,
noLeadTrailSpacesRegex,
QualityTypes,
QualityTypeTranslationsMap,
RequestType,
RequestTypesTranslationsMap,
ServerSideRPCType,
SourceTypes,
SourceTypeTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Subject } from 'rxjs';
import { startWith, takeUntil } from 'rxjs/operators';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'tb-mapping-dialog',
templateUrl: './mapping-dialog.component.html',
styleUrls: ['./mapping-dialog.component.scss'],
providers: [],
})
export class MappingDialogComponent extends DialogComponent<MappingDialogComponent, BaseData<HasId>> implements OnDestroy {
mappingForm: UntypedFormGroup;
MappingType = MappingType;
qualityTypes = QualityTypes;
QualityTranslationsMap = QualityTypeTranslationsMap;
convertorTypes = Object.values(ConvertorType);
ConvertorTypeEnum = ConvertorType;
ConvertorTypeTranslationsMap = ConvertorTypeTranslationsMap;
sourceTypes = Object.values(SourceTypes);
sourceTypesEnum = SourceTypes;
SourceTypeTranslationsMap = SourceTypeTranslationsMap;
requestTypes = Object.values(RequestType);
RequestTypeEnum = RequestType;
RequestTypesTranslationsMap = RequestTypesTranslationsMap;
DeviceInfoType = DeviceInfoType;
ServerSideRPCType = ServerSideRPCType;
MappingKeysType = MappingKeysType;
MappingHintTranslationsMap = MappingHintTranslationsMap;
MappingTypeTranslationsMap = MappingTypeTranslationsMap;
DataConversionTranslationsMap = DataConversionTranslationsMap;
hiddenAttributesCount = 0;
keysPopupClosed = true;
submitted = false;
private destroy$ = new Subject<void>();
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: MappingInfo,
public dialogRef: MatDialogRef<MappingDialogComponent, {[key: string]: any}>,
private fb: FormBuilder,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private translate: TranslateService) {
super(store, router, dialogRef);
this.createMappingForm();
}
get converterAttributes(): Array<string> {
if (this.converterType) {
return this.mappingForm.get('converter').get(this.converterType).value.attributes.map(value => value.key);
}
}
get converterTelemetry(): Array<string> {
if (this.converterType) {
return this.mappingForm.get('converter').get(this.converterType).value.timeseries.map(value => value.key);
}
}
get converterType(): ConvertorType {
return this.mappingForm.get('converter').get('type').value;
}
get customKeys(): Array<string> {
return Object.keys(this.mappingForm.get('converter').get('custom').value.extensionConfig);
}
get requestMappingType(): RequestType {
return this.mappingForm.get('requestType').value;
}
get responseTimeoutErrorTooltip(): string {
const control = this.mappingForm.get('requestValue.serverSideRpc.responseTimeout');
if (control.hasError('required')) {
return this.translate.instant('gateway.response-timeout-required');
} else if (control.hasError('min')) {
return this.translate.instant('gateway.response-timeout-limits-error', {min: 1});
}
return '';
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
private createMappingForm(): void {
this.mappingForm = this.fb.group({});
if (this.data.mappingType === MappingType.DATA) {
this.mappingForm.addControl('topicFilter',
this.fb.control('', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]));
this.mappingForm.addControl('subscriptionQos', this.fb.control(0));
this.mappingForm.addControl('converter', this.fb.group({
type: [ConvertorType.JSON, []],
json: this.fb.group({
deviceInfo: [{}, []],
attributes: [[], []],
timeseries: [[], []]
}),
bytes: this.fb.group({
deviceInfo: [{}, []],
attributes: [[], []],
timeseries: [[], []]
}),
custom: this.fb.group({
extension: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
extensionConfig: [{}, []]
}),
}));
this.mappingForm.patchValue(this.prepareFormValueData());
this.mappingForm.get('converter.type').valueChanges.pipe(
startWith(this.mappingForm.get('converter.type').value),
takeUntil(this.destroy$)
).subscribe((value) => {
const converterGroup = this.mappingForm.get('converter');
converterGroup.get('json').disable({emitEvent: false});
converterGroup.get('bytes').disable({emitEvent: false});
converterGroup.get('custom').disable({emitEvent: false});
converterGroup.get(value).enable({emitEvent: false});
})
}
if (this.data.mappingType === MappingType.REQUESTS) {
this.mappingForm.addControl('requestType', this.fb.control(RequestType.CONNECT_REQUEST, []));
this.mappingForm.addControl('requestValue', this.fb.group({
connectRequests: this.fb.group({
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceInfo: [{}, []]
}),
disconnectRequests: this.fb.group({
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceInfo: [{}, []]
}),
attributeRequests: this.fb.group({
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceInfo: this.fb.group({
deviceNameExpressionSource: [SourceTypes.MSG, []],
deviceNameExpression: ['', [Validators.required]],
}),
attributeNameExpressionSource: [SourceTypes.MSG, []],
attributeNameExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
topicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
retain: [false, []]
}),
attributeUpdates: this.fb.group({
deviceNameFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
attributeFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
topicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
retain: [true, []]
}),
serverSideRpc: this.fb.group({
type: [ServerSideRPCType.TWO_WAY, []],
deviceNameFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
methodFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
requestTopicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTopicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTopicQoS: [0, []],
responseTimeout: [10000, [Validators.required, Validators.min(1)]],
})
}));
this.mappingForm.get('requestType').valueChanges.pipe(
startWith(this.mappingForm.get('requestType').value),
takeUntil(this.destroy$)
).subscribe((value) => {
const requestValueGroup = this.mappingForm.get('requestValue');
requestValueGroup.get('connectRequests').disable({emitEvent: false});
requestValueGroup.get('disconnectRequests').disable({emitEvent: false});
requestValueGroup.get('attributeRequests').disable({emitEvent: false});
requestValueGroup.get('attributeUpdates').disable({emitEvent: false});
requestValueGroup.get('serverSideRpc').disable({emitEvent: false});
requestValueGroup.get(value).enable();
});
this.mappingForm.get('requestValue.serverSideRpc.type').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
const requestValueGroup = this.mappingForm.get('requestValue.serverSideRpc');
if (value === ServerSideRPCType.ONE_WAY) {
requestValueGroup.get('responseTopicExpression').disable({emitEvent: false});
requestValueGroup.get('responseTopicQoS').disable({emitEvent: false});
requestValueGroup.get('responseTimeout').disable({emitEvent: false});
} else {
requestValueGroup.get('responseTopicExpression').enable({emitEvent: false});
requestValueGroup.get('responseTopicQoS').enable({emitEvent: false});
requestValueGroup.get('responseTimeout').enable({emitEvent: false});
}
});
this.mappingForm.patchValue(this.prepareFormValueData());
}
}
helpLinkId(): string {
return 'https://thingsboard.io/docs/iot-gateway/config/mqtt/#section-mapping';
}
cancel(): void {
if (this.keysPopupClosed) {
this.dialogRef.close(null);
}
}
add(): void {
this.submitted = true;
if (this.mappingForm.valid) {
this.dialogRef.close(this.prepareMappingData());
}
}
manageKeys($event: Event, matButton: MatButton, keysType: MappingKeysType) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const keysControl = this.mappingForm.get('converter').get(this.converterType).get(keysType);
const ctx: any = {
keys: keysControl.value,
keysType: keysType,
rawData: this.mappingForm.get('converter.type').value === ConvertorType.BYTES,
panelTitle: MappingKeysPanelTitleTranslationsMap.get(keysType),
addKeyTitle: MappingKeysAddKeyTranslationsMap.get(keysType),
deleteKeyTitle: MappingKeysDeleteKeyTranslationsMap.get(keysType),
noKeysText: MappingKeysNoKeysTextTranslationsMap.get(keysType)
};
this.keysPopupClosed = false;
const dataKeysPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, MappingDataKeysPanelComponent, 'leftBottom', false, null,
ctx,
{},
{}, {}, true);
dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover;
dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.subscribe((keysData) => {
dataKeysPanelPopover.hide();
keysControl.patchValue(keysData);
keysControl.markAsDirty();
});
dataKeysPanelPopover.tbHideStart.subscribe(() => {
this.keysPopupClosed = true;
});
}
}
private prepareMappingData(): {[key: string]: any} {
const formValue = this.mappingForm.value;
if (this.data.mappingType === MappingType.DATA) {
const { converter, topicFilter, subscriptionQos } = formValue;
return {
topicFilter,
subscriptionQos,
converter: {
type: converter.type,
...converter[converter.type]
}
};
} else {
return {
requestType: formValue.requestType,
requestValue: formValue.requestValue[formValue.requestType]
};
}
}
private prepareFormValueData(): {[key: string]: any} {
if (this.data.value && Object.keys(this.data.value).length) {
if (this.data.mappingType === MappingType.DATA) {
const { converter, topicFilter, subscriptionQos } = this.data.value;
return {
topicFilter,
subscriptionQos,
converter: {
type: converter.type,
[converter.type]: { ...converter }
}
};
} else {
return {
requestType: this.data.value.requestType,
requestValue: {
[this.data.value.requestType]: this.data.value.requestValue
}
};
}
}
return this.data.value;
}
}

38
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html

@ -80,6 +80,15 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.accessToken').hasError('required')">
{{ 'security.access-token-required' | translate }}
</mat-error>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="gatewayConfigGroup.get('thingsboard.security.accessToken').value"
[copyText]="gatewayConfigGroup.get('thingsboard.security.accessToken').value"
tooltipText="{{ 'device.copy-access-token' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.token' | translate }}">info_outlined
</mat-icon>
@ -92,6 +101,15 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.clientId').hasError('required')">
{{ 'security.clientId-required' | translate }}
</mat-error>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="gatewayConfigGroup.get('thingsboard.security.clientId').value"
[copyText]="gatewayConfigGroup.get('thingsboard.security.clientId').value"
tooltipText="{{ 'gateway.copy-client-id' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.client-id' | translate }}">info_outlined
</mat-icon>
@ -102,6 +120,15 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.username').hasError('required')">
{{ 'security.username-required' | translate }}
</mat-error>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="gatewayConfigGroup.get('thingsboard.security.username').value"
[copyText]="gatewayConfigGroup.get('thingsboard.security.username').value"
tooltipText="{{ 'gateway.copy-username' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.username' | translate }}">info_outlined
</mat-icon>
@ -109,8 +136,17 @@
</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'">
<mat-label translate>security.password</mat-label>
<mat-label translate>gateway.password</mat-label>
<input matInput formControlName="password"/>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="gatewayConfigGroup.get('thingsboard.security.password').value"
[copyText]="gatewayConfigGroup.get('thingsboard.security.password').value"
tooltipText="{{ 'gateway.copy-password' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.password' | translate }}">info_outlined
</mat-icon>

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss

@ -97,5 +97,9 @@
color: #9E9E9E;
}
}
.mat-mdc-form-field-icon-suffix {
display: flex;
}
}

275
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html

@ -22,13 +22,21 @@
<span fxFlex></span>
<button mat-icon-button
[disabled]="isLoading$ | async"
(click)="addAttribute()"
(click)="addConnector($event)"
matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
</mat-toolbar>
<div class="table-container">
<section *ngIf="!dataSource?.data?.length" fxLayoutAlign="center center"
class="mat-headline-5 tb-absolute-fill tb-add-new">
<button mat-button class="connector"
(click)="addConnector($event)">
<mat-icon class="tb-mat-96">add</mat-icon>
<span>{{ 'gateway.add-connector' | translate }}</span>
</button>
</section>
<table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear>
@ -74,6 +82,7 @@
<span class="dot"
matTooltip="{{ 'Errors: '+ getErrorsCount(attribute)}}"
matTooltipPosition="above"
(click)="connectorLogs(attribute, $event)"
[class]="{'hasErrors': +getErrorsCount(attribute) > 0,
'noErrors': +getErrorsCount(attribute) === 0 || getErrorsCount(attribute) === ''}"></span>
</mat-cell>
@ -119,7 +128,7 @@
<mat-icon>private_connectivity</mat-icon>
</button>
<button mat-icon-button
matTooltip="Delete connector"
matTooltip="Logs"
matTooltipPosition="above"
(click)="connectorLogs(attribute, $event)">
<mat-icon>list</mat-icon>
@ -137,49 +146,231 @@
<mat-header-row class="mat-row-select"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row class="mat-row-select" [class]="{'tb-current-entity': isSameConnector(attribute)}"
*matRowDef="let attribute; columns: displayedColumns;" (click)="selectConnector(attribute)"></mat-row>
*matRowDef="let attribute; let i = index; columns: displayedColumns;" (click)="selectConnector($event, attribute)"></mat-row>
</table>
</div>
</section>
<section [formGroup]="connectorForm" class="tb-form-panel no-border no-padding flex">
<section class="tb-form-panel input-container section-container">
<section class="tb-form-row tb-standard-fields no-padding no-border column-lt-md input-container">
<mat-form-field class="flex" subscriptSizing="dynamic">
<mat-label>{{ 'gateway.connectors-table-name' | translate }}</mat-label>
<input matInput formControlName="name" #nameInput/>
</mat-form-field>
<mat-form-field class="flex" subscriptSizing="dynamic" hideRequiredMarker>
<mat-label>{{ 'gateway.connectors-table-type' | translate }}</mat-label>
<mat-select formControlName="type">
<mat-option style="text-transform: uppercase"
*ngFor="let type of gatewayConnectorDefaultTypes | keyvalue" [value]="type.key">{{ type.value }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex" *ngIf="connectorForm.get('type').value === 'grpc'" subscriptSizing="dynamic">
<mat-label>{{ 'gateway.connectors-table-key' | translate }}</mat-label>
<input matInput formControlName="key"/>
</mat-form-field>
<mat-form-field class="flex" *ngIf="connectorForm.get('type').value === 'custom'" subscriptSizing="dynamic">
<mat-label>{{ 'gateway.connectors-table-class' | translate }}</mat-label>
<input matInput formControlName="class"/>
</mat-form-field>
<mat-form-field class="flex" subscriptSizing="dynamic" hideRequiredMarker>
<mat-label translate>gateway.remote-logging-level</mat-label>
<mat-select formControlName="logLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{ logLevel }}</mat-option>
</mat-select>
</mat-form-field>
</section>
</section>
<section class="tb-form-panel flex section-container">
<tb-json-object-edit
fillHeight="true"
class="flex"
fxLayout="column"
jsonRequired
label="{{ 'gateway.configuration' | translate }}"
formControlName="configurationJson">
</tb-json-object-edit>
<section [formGroup]="connectorForm" class="tb-form-panel section-container flex">
<div class="tb-form-panel-title tb-flex no-flex space-between align-center">
<div class="tb-form-panel-title">
{{ initialConnector?.type ? gatewayConnectorDefaultTypes.get(initialConnector.type) : '' }}
{{ 'gateway.configuration' | translate }}
</div>
<tb-toggle-select *ngIf="initialConnector && initialConnector.type === connectorType.MQTT" formControlName="mode" appearance="fill">
<tb-toggle-option [value]="connectorConfigurationModes.BASIC">
{{ 'gateway.basic' | translate }}
</tb-toggle-option>
<tb-toggle-option [value]="connectorConfigurationModes.ADVANCED">
{{ 'gateway.advanced' | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<span [fxShow]="!initialConnector"
fxLayoutAlign="center center"
class="no-data-found" translate>
gateway.select-connector
</span>
<section class="tb-form-panel section-container no-border no-padding tb-flex space-between" *ngIf="initialConnector">
<mat-tab-group>
<mat-tab label="{{ 'gateway.general' | translate }}">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.name-required') | translate"
*ngIf="connectorForm.get('name').hasError('required') && connectorForm.get('name').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.logs-configuration</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="enableRemoteLogging">
<mat-label>
{{ 'gateway.enable-remote-logging' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.remote-logging-level</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="logLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{ logLevel }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<div *ngIf="connectorForm.get('type').value === connectorType.MQTT" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="sendDataOnlyOnChange">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.send-change-data-hint' | translate }}">
{{ 'gateway.send-change-data' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>
</mat-tab>
<ng-container *ngIf="connectorForm.get('type').value === connectorType.MQTT &&
connectorForm.get('mode').value === connectorConfigurationModes.BASIC; else defaultConfig"
formGroupName="basicConfig">
<mat-tab label="{{ 'gateway.broker.connection' | translate }}*">
<ng-container formGroupName="broker">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.host</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="host" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.host-required') | translate"
*ngIf="connectorForm.get('basicConfig.broker.host').hasError('required')
&& connectorForm.get('basicConfig.broker.host').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="{{portLimits.MIN}}" max="{{portLimits.MAX}}"
name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="portErrorTooltip"
*ngIf="(connectorForm.get('basicConfig.broker.port').hasError('required') ||
connectorForm.get('basicConfig.broker.port').hasError('min') ||
connectorForm.get('basicConfig.broker.port').hasError('max')) &&
connectorForm.get('basicConfig.broker.port').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.mqtt-version</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="version">
<mat-option *ngFor="let version of mqttVersions" [value]="version.value">{{ version.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.client-id</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="clientId" placeholder="{{ 'gateway.set' | translate }}"/>
<button type="button"
matSuffix
mat-icon-button
aria-label="Generate"
matTooltip="{{ 'gateway.generate-client-id' | translate }}"
matTooltipPosition="above"
(click)="generate('basicConfig.broker.clientId')"
*ngIf="!connectorForm.get('basicConfig.broker.clientId').value">
<mat-icon>autorenew</mat-icon>
</button>
</mat-form-field>
</div>
</div>
<tb-broker-security [formControl]="connectorForm.get('basicConfig.broker.security')">
</tb-broker-security>
</div>
</ng-container>
</mat-tab>
<mat-tab label="{{ 'gateway.data-mapping' | translate }}*">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">
<tb-mapping-table formControlName="dataMapping" [mappingType]="mappingTypes.DATA"></tb-mapping-table>
</div>
</mat-tab>
<mat-tab label="{{ 'gateway.requests-mapping' | translate }}">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">
<tb-mapping-table formControlName="requestsMapping" [mappingType]="mappingTypes.REQUESTS"></tb-mapping-table>
</div>
</mat-tab>
<mat-tab label="{{ 'gateway.workers-settings' | translate }}">
<div class="tb-form-panel no-border no-padding">
<ng-container formGroupName="broker">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" [style.width.%]="50"
tb-hint-tooltip-icon="{{ 'gateway.max-number-of-workers-hint' | translate }}" translate>
gateway.max-number-of-workers
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" type="number" min="1" formControlName="maxNumberOfWorkers"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.max-number-of-workers-required') | translate"
*ngIf="connectorForm.get('basicConfig.broker.maxNumberOfWorkers').hasError('min') ||
(connectorForm.get('basicConfig.broker.maxNumberOfWorkers').hasError('required') &&
connectorForm.get('basicConfig.broker.maxNumberOfWorkers').touched)"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" [style.width.%]="50"
tb-hint-tooltip-icon="{{ 'gateway.max-messages-queue-for-worker-hint' | translate }}" translate>
gateway.max-messages-queue-for-worker
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" type="number" min="1" formControlName="maxMessageNumberPerWorker"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.max-messages-queue-for-worker-required') | translate"
*ngIf="connectorForm.get('basicConfig.broker.maxMessageNumberPerWorker').hasError('min') ||
(connectorForm.get('basicConfig.broker.maxMessageNumberPerWorker').hasError('required') &&
connectorForm.get('basicConfig.broker.maxMessageNumberPerWorker').touched)"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</div>
</ng-container>
</div>
</mat-tab>
</ng-container>
<ng-template #defaultConfig>
<mat-tab label="{{ 'gateway.configuration' | translate }}*">
<tb-json-object-edit
fillHeight="true"
class="tb-flex fill-height"
fxLayout="column"
jsonRequired
label="{{ 'gateway.configuration' | translate }}"
formControlName="configurationJson">
</tb-json-object-edit>
</mat-tab>
</ng-template>
</mat-tab-group>
<div fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="button"

45
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss

@ -104,3 +104,48 @@
background-color: rgb(25, 128, 56);
}
}
:host ::ng-deep {
.connector-container {
.mat-mdc-tab-group, .mat-mdc-tab-body-wrapper {
height: 100%;
}
.mat-mdc-tab-body.mat-mdc-tab-body-active {
position: absolute;
}
.tb-form-row {
.fixed-title-width {
min-width: 120px;
width: 30%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.tb-add-new {
display: flex;
z-index: 999;
pointer-events: none;
background-color: #fff;
button.connector {
height: auto;
padding-right: 12px;
font-size: 20px;
border-style: dashed;
border-width: 2px;
border-radius: 8px;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
color: rgba(0, 0, 0, 0.38);
}
}
}
}

341
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts

@ -17,11 +17,20 @@
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, NgZone, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, UntypedFormControl, ValidatorFn, Validators } from '@angular/forms';
import {
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
NgForm,
UntypedFormControl,
ValidatorFn,
Validators
} from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { AttributeService } from '@core/http/attribute.service';
import { TranslateService } from '@ngx-translate/core';
import { forkJoin, Observable } from 'rxjs';
import { forkJoin, Observable, of, Subject, Subscription } from 'rxjs';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link';
@ -33,34 +42,44 @@ import { MatTableDataSource } from '@angular/material/table';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models';
import { camelCase, deepClone, isString } from '@core/utils';
import { camelCase, deepClone, generateSecret, isEqual, isString } from '@core/utils';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { DatasourceType, widgetType } from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@shared/models/entity-type.models';
import {
AddConnectorConfigData,
ConnectorConfigurationModes,
ConnectorType,
GatewayConnector,
GatewayConnectorDefaultTypesTranslates,
GatewayLogLevel
GatewayConnectorDefaultTypesTranslatesMap,
GatewayLogLevel,
MappingType,
MqttVersions,
noLeadTrailSpacesRegex,
PortLimits
} from './gateway-widget.models';
import { MatDialog } from '@angular/material/dialog';
import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component';
import { takeUntil } from 'rxjs/operators';
import { ErrorStateMatcher } from '@angular/material/core';
import { PageData } from '@shared/models/page/page-data';
export class ForceErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
return (control && control.invalid);
}
}
@Component({
selector: 'tb-gateway-connector',
templateUrl: './gateway-connectors.component.html',
providers: [{ provide: ErrorStateMatcher, useClass: ForceErrorStateMatcher }],
styleUrls: ['./gateway-connectors.component.scss']
})
export class GatewayConnectorComponent extends PageComponent implements AfterViewInit {
pageLink: PageLink;
dataSource: MatTableDataSource<AttributeData>;
displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
gatewayConnectorDefaultTypes = GatewayConnectorDefaultTypesTranslates;
@Input()
ctx: WidgetContext;
@ -70,6 +89,20 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
@ViewChild('nameInput') nameInput: ElementRef;
@ViewChild(MatSort, {static: false}) sort: MatSort;
pageLink: PageLink;
connectorType = ConnectorType;
dataSource: MatTableDataSource<AttributeData>;
displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
mqttVersions = MqttVersions;
gatewayConnectorDefaultTypes = GatewayConnectorDefaultTypesTranslatesMap;
connectorConfigurationModes = ConnectorConfigurationModes;
connectorForm: FormGroup;
textSearchMode: boolean;
@ -78,6 +111,14 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
gatewayLogLevel = Object.values(GatewayLogLevel);
mappingTypes = MappingType;
portLimits = PortLimits;
mode: ConnectorConfigurationModes = this.connectorConfigurationModes.BASIC;
initialConnector: GatewayConnector;
private inactiveConnectors: Array<string>;
private attributeDataSource: AttributeDatasource;
@ -92,7 +133,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private sharedAttributeData: Array<AttributeData> = [];
private initialConnector: GatewayConnector;
private basicConfigSub: Subscription;
private subscriptionOptions: WidgetSubscriptionOptions = {
callbacks: {
@ -105,6 +146,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
};
private destroy$ = new Subject<void>();
private subscription: IWidgetSubscription;
constructor(protected store: Store<AppState>,
@ -112,6 +154,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private translate: TranslateService,
private attributeService: AttributeService,
private dialogService: DialogService,
private dialog: MatDialog,
private telemetryWsService: TelemetryWebsocketService,
private zone: NgZone,
private utils: UtilsService,
@ -124,24 +167,39 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.serverDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate);
this.dataSource = new MatTableDataSource<AttributeData>([]);
this.connectorForm = this.fb.group({
name: ['', [Validators.required, this.uniqNameRequired()]],
mode: [ConnectorConfigurationModes.BASIC, []],
name: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(noLeadTrailSpacesRegex)]],
type: ['', [Validators.required]],
enableRemoteLogging: [false, []],
logLevel: ['', [Validators.required]],
sendDataOnlyOnChange: [false, []],
key: ['auto'],
class: [''],
configuration: [''],
configurationJson: [{}, [Validators.required]]
configurationJson: [{}, [Validators.required]],
basicConfig: this.fb.group({})
});
this.connectorForm.disable();
}
ngAfterViewInit() {
this.connectorForm.valueChanges.subscribe(() => {
this.cd.detectChanges();
});
get portErrorTooltip(): string {
if (this.connectorForm.get('basicConfig.broker.port').hasError('required')) {
return this.translate.instant('gateway.port-required');
} else if (
this.connectorForm.get('basicConfig.broker.port').hasError('min') ||
this.connectorForm.get('basicConfig.broker.port').hasError('max')
) {
return this.translate.instant('gateway.port-limits-error',
{min: PortLimits.MIN, max: PortLimits.MAX});
}
return '';
}
this.connectorForm.get('type').valueChanges.subscribe(type=> {
if(type && !this.initialConnector) {
ngAfterViewInit() {
this.connectorForm.get('type').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(type => {
if (type && !this.initialConnector) {
this.attributeService.getEntityAttributes(this.device, AttributeScope.CLIENT_SCOPE,
[`${type.toUpperCase()}_DEFAULT_CONFIG`], {ignoreErrors: true}).subscribe(defaultConfig=>{
if (defaultConfig && defaultConfig.length) {
@ -155,6 +213,29 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
});
this.connectorForm.get('name').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((name) => {
if (this.connectorForm.get('type').value === ConnectorType.MQTT) {
this.connectorForm.get('basicConfig').get('broker.name')?.setValue(name);
}
});
this.connectorForm.get('configurationJson').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((config) => {
const basicConfig = this.connectorForm.get('basicConfig');
const type = this.connectorForm.get('type').value;
const mode = this.connectorForm.get('mode').value;
if (
!isEqual(config, basicConfig?.value) &&
type === ConnectorType.MQTT &&
mode === ConnectorConfigurationModes.ADVANCED
) {
this.connectorForm.get('basicConfig').patchValue(config, {emitEvent: false});
}
});
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = (data: AttributeData, sortHeaderId: string) => {
if (sortHeaderId === 'syncStatus') {
@ -211,9 +292,16 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
};
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
saveConnector(): void {
const value = this.connectorForm.value;
value.configuration = camelCase(value.name) + '.json';
delete value.basicConfig;
if (value.type !== ConnectorType.GRPC) {
delete value.key;
}
@ -268,16 +356,18 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
);
this.initialConnector = value;
this.updateData(true);
this.connectorForm.markAsPristine();
});
}
private updateData(reload: boolean = false) {
private updateData(reload: boolean = false): void {
this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.attributeDataSource.loadAttributes(this.device, AttributeScope.CLIENT_SCOPE, this.pageLink, reload).subscribe(data => {
this.activeData = data.data.filter(value => this.activeConnectors.includes(value.key));
this.combineData();
this.generateSubscription();
this.setClientData(data);
});
this.inactiveConnectorsDataSource.loadAttributes(this.device, AttributeScope.SHARED_SCOPE, this.pageLink, reload).subscribe(data => {
this.sharedAttributeData = data.data.filter(value => this.activeConnectors.includes(value.key));
@ -308,7 +398,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
return sharedIndex !== -1;
}
private combineData() {
private combineData(): void {
this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) =>
index === self.findIndex((t) => t.key === item.key)
).map(attribute => {
@ -317,42 +407,37 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
});
}
addAttribute(): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
this.nameInput.nativeElement.focus();
this.clearOutConnectorForm();
}
private clearOutConnectorForm(): void {
this.initialConnector = null;
this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false});
this.connectorForm.setValue({
mode: ConnectorConfigurationModes.BASIC,
name: '',
type: ConnectorType.MQTT,
sendDataOnlyOnChange: false,
enableRemoteLogging: false,
logLevel: GatewayLogLevel.INFO,
key: 'auto',
class: '',
configuration: '',
configurationJson: {}
});
configurationJson: {},
basicConfig: {}
}, {emitEvent: false});
this.connectorForm.markAsPristine();
}
selectConnector(attribute: AttributeData): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
selectConnector($event: Event, attribute: AttributeData): void {
if ($event) {
$event.stopPropagation();
}
const connector = attribute.value;
if (!connector.configuration) {
connector.configuration = '';
}
if (!connector.key) {
connector.key = 'auto';
if (connector?.name !== this.initialConnector?.name) {
this.confirmConnectorChange().subscribe((result) => {
if (result) {
this.setFormValue(connector);
}
});
}
this.initialConnector = connector;
this.connectorForm.patchValue(connector);
this.connectorForm.markAsPristine();
}
isSameConnector(attribute: AttributeData): boolean {
@ -363,7 +448,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
return this.initialConnector.name === connector.name;
}
showToast(message: string) {
showToast(message: string): void {
this.store.dispatch(new ActionNotificationShow(
{
message,
@ -385,15 +470,14 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
if ($event) {
$event.stopPropagation();
}
this.initialConnector = attribute.value;
const title = `Delete connector ${attribute.key}?`;
const title = `Delete connector \"${attribute.key}\"?`;
const content = `All connector data will be deleted.`;
this.dialogService.confirm(title, content, 'Cancel', 'Delete').subscribe(result => {
if (result) {
const tasks: Array<Observable<any>> = [];
const scope = (this.initialConnector && this.activeConnectors.includes(this.initialConnector.name))
? AttributeScope.SHARED_SCOPE
: AttributeScope.SERVER_SCOPE;
const scope = this.activeConnectors.includes(attribute.value?.name) ?
AttributeScope.SHARED_SCOPE :
AttributeScope.SERVER_SCOPE;
tasks.push(this.attributeService.deleteEntityAttributes(this.device, scope, [attribute]));
const activeIndex = this.activeConnectors.indexOf(attribute.key);
const inactiveIndex = this.inactiveConnectors.indexOf(attribute.key);
@ -467,7 +551,55 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
});
}
private onDataUpdateError(e: any) {
getErrorsCount(attribute: AttributeData): string {
const connectorName = attribute.key;
const connector = this.subscription && this.subscription.data
.find(data => data && data.dataKey.name === `${connectorName}_ERRORS_COUNT`);
return (connector && this.activeConnectors.includes(connectorName)) ? (connector.data[0][1] || 0) : 'Inactive';
}
addConnector($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.confirmConnectorChange().subscribe((changeConfirmed) => {
if (changeConfirmed) {
return this.dialog.open<AddConnectorDialogComponent,
AddConnectorConfigData>(AddConnectorDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dataSourceData: this.dataSource.data
}
}).afterClosed().subscribe((value) => {
if (value && changeConfirmed) {
this.initialConnector = null;
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
if (!value.configurationJson) {
value.configurationJson = {};
}
value.basicConfig = value.configurationJson;
if (value.type === ConnectorType.MQTT) {
this.addMQTTConfigControls();
} else {
this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false});
}
this.connectorForm.patchValue(value, {emitEvent: false});
this.generate('basicConfig.broker.clientId');
this.saveConnector();
}
});
}
});
}
generate(formControlName: string): void {
this.connectorForm.get(formControlName)?.patchValue('tb_gw_' + generateSecret(5));
}
private onDataUpdateError(e: any): void {
const exceptionData = this.utils.parseException(e);
let errorText = exceptionData.name;
if (exceptionData.message) {
@ -476,11 +608,11 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
console.error(errorText);
}
private onDataUpdated() {
private onDataUpdated(): void {
this.cd.detectChanges();
}
private generateSubscription() {
private generateSubscription(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
@ -502,10 +634,101 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
}
getErrorsCount(attribute: AttributeData): string {
const connectorName = attribute.key;
const connector = this.subscription && this.subscription.data
.find(data => data && data.dataKey.name === `${connectorName}_ERRORS_COUNT`);
return (connector && this.activeConnectors.includes(connectorName)) ? (connector.data[0][1] || 0) : 'Inactive';
private addMQTTConfigControls(): void {
const configControl = this.fb.group({});
const brokerGroup = this.fb.group({
name: ['', []],
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],
version: [5, []],
clientId: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
maxNumberOfWorkers: [100, [Validators.required, Validators.min(1)]],
maxMessageNumberPerWorker: [10, [Validators.required, Validators.min(1)]],
security: [{}, [Validators.required]]
});
configControl.addControl('broker', brokerGroup);
configControl.addControl('dataMapping', this.fb.control([], Validators.required));
configControl.addControl('requestsMapping', this.fb.control({}));
if (this.connectorForm.get('basicConfig')) {
this.connectorForm.setControl('basicConfig', configControl, {emitEvent: false});
} else {
this.connectorForm.addControl('basicConfig', configControl, {emitEvent: false});
}
this.createBasicConfigWatcher();
}
private createBasicConfigWatcher(): void {
if (this.basicConfigSub) {
this.basicConfigSub.unsubscribe();
}
this.basicConfigSub = this.connectorForm.get('basicConfig').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((config) => {
const configJson = this.connectorForm.get('configurationJson');
const type = this.connectorForm.get('type').value;
const mode = this.connectorForm.get('mode').value;
if (
!isEqual(config, configJson?.value) &&
type === ConnectorType.MQTT &&
mode === ConnectorConfigurationModes.BASIC
) {
const newConfig = { ...configJson.value, ...config };
this.connectorForm.get('configurationJson').patchValue(newConfig, {emitEvent: false});
}
});
}
private confirmConnectorChange(): Observable<boolean> {
if (this.initialConnector && this.connectorForm.dirty) {
return this.dialogService.confirm(
this.translate.instant('gateway.change-connector-title'),
this.translate.instant('gateway.change-connector-text'),
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
);
}
return of(true);
}
private setFormValue(connector: GatewayConnector): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
if (!connector.configuration) {
connector.configuration = '';
}
if (!connector.key) {
connector.key = 'auto';
}
if (!connector.configurationJson) {
connector.configurationJson = {};
}
connector.basicConfig = connector.configurationJson;
this.initialConnector = connector;
if (connector.type === ConnectorType.MQTT) {
this.addMQTTConfigControls();
} else {
this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false});
}
this.connectorForm.patchValue(connector, {emitEvent: false});
this.connectorForm.markAsPristine();
}
private setClientData(data: PageData<AttributeData>): void {
if (this.initialConnector) {
const clientConnectorData = data.data.find(attr => attr.key === this.initialConnector.name);
if (clientConnectorData) {
clientConnectorData.value = typeof clientConnectorData.value === 'string' ?
JSON.parse(clientConnectorData.value) : clientConnectorData.value;
if (this.isConnectorSynced(clientConnectorData) && clientConnectorData.value.configurationJson) {
this.setFormValue(clientConnectorData.value);
}
}
}
}
}

7
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts

@ -33,10 +33,11 @@ import {
BLEMethodsTranslates,
CANByteOrders,
ConnectorType,
GatewayConnectorDefaultTypesTranslates,
GatewayConnectorDefaultTypesTranslatesMap,
HTTPMethods,
ModbusCodesTranslate,
ModbusCommandTypes,
noLeadTrailSpacesRegex,
RPCCommand,
RPCTemplateConfig,
SNMPMethods,
@ -53,8 +54,6 @@ import {
import { jsonRequired } from '@shared/components/json-object-edit.component';
import { deepClone } from '@core/utils';
export const noLeadTrailSpacesRegex: RegExp = /^(?! )[\S\s]*(?<! )$/;
@Component({
selector: 'tb-gateway-service-rpc-connector',
templateUrl: './gateway-service-rpc-connector.component.html',
@ -97,7 +96,7 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
bLEMethodsTranslates = BLEMethodsTranslates;
SocketMethodProcessingsTranslates = SocketMethodProcessingsTranslates;
SNMPMethodsTranslations = SNMPMethodsTranslations;
gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslates;
gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslatesMap;
modbusCodesTranslate = ModbusCodesTranslate;
urlPattern = /^[-a-zA-Zd_$:{}?~+=\/.0-9-]*$/;

247
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts

@ -14,6 +14,13 @@
/// limitations under the License.
///
import { ResourcesService } from '@core/services/resources.service';
import { Observable } from 'rxjs';
import { ValueTypeData } from '@shared/models/constants';
import { Validators } from '@angular/forms';
export const noLeadTrailSpacesRegex: RegExp = /^(?! )[\S\s]*(?<! )$/;
export enum StorageTypes {
MEMORY = 'memory',
FILE = 'file',
@ -33,6 +40,11 @@ export enum GatewayLogLevel {
DEBUG = 'DEBUG'
}
export enum PortLimits {
MIN = 1,
MAX = 65535
}
export const GatewayStatus = {
...GatewayLogLevel,
...DeviceGatewayStatus
@ -93,18 +105,19 @@ export const GecurityTypesTranslationsMap = new Map<SecurityTypes, string>(
[
[SecurityTypes.ACCESS_TOKEN, 'gateway.security-types.access-token'],
[SecurityTypes.USERNAME_PASSWORD, 'gateway.security-types.username-password'],
[SecurityTypes.TLS_ACCESS_TOKEN, 'gateway.security-types.tls-access-token'],
// [SecurityTypes.TLS_PRIVATE_KEY, 'gateway.security-types.tls-private-key'],
[SecurityTypes.TLS_ACCESS_TOKEN, 'gateway.security-types.tls-access-token']
]
);
export interface GatewayConnector {
name: string;
type: string;
type: ConnectorType;
configuration?: string;
configurationJson: string;
configurationJson: string | {[key: string]: any};
basicConfig?: string | {[key: string]: any};
logLevel: string;
key?: string;
class?: string;
}
export enum ConnectorType {
@ -127,7 +140,7 @@ export enum ConnectorType {
CUSTOM = 'custom'
}
export const GatewayConnectorDefaultTypesTranslates = new Map<ConnectorType, string>([
export const GatewayConnectorDefaultTypesTranslatesMap = new Map<ConnectorType, string>([
[ConnectorType.MQTT, 'MQTT'],
[ConnectorType.MODBUS, 'MODBUS'],
[ConnectorType.GRPC, 'GRPC'],
@ -303,3 +316,227 @@ export interface GatewayLogData {
message: string;
status: GatewayStatus;
}
export interface AddConnectorConfigData {
dataSourceData: Array<any>
}
export interface CreatedConnectorConfigData {
type: ConnectorType,
name: string,
logLevel: GatewayLogLevel,
useDefaults: boolean,
sendDataOnlyOnChange: boolean,
configurationJson?: {[key: string]: any}
}
export interface MappingDataKey {
key: string,
value: any,
type: MappingValueType
}
export interface MappingInfo {
mappingType: MappingType,
value: {[key: string]: any},
buttonTitle: string
}
export enum ConnectorConfigurationModes {
BASIC = 'basic',
ADVANCED = 'advanced'
}
export enum BrokerSecurityType {
ANONYMOUS = 'anonymous',
BASIC = 'basic',
CERTIFICATES = 'certificates'
}
export const BrokerSecurityTypeTranslationsMap = new Map<BrokerSecurityType, string>(
[
[BrokerSecurityType.ANONYMOUS, 'gateway.broker.security-types.anonymous'],
[BrokerSecurityType.BASIC, 'gateway.broker.security-types.basic'],
[BrokerSecurityType.CERTIFICATES, 'gateway.broker.security-types.certificates']
]
);
export const MqttVersions = [
{ name: 3.1, value: 3 },
{ name: 3.11, value: 4 },
{ name: 5, value: 5 }
];
export enum MappingType {
DATA = 'data',
REQUESTS = 'requests'
}
export const MappingTypeTranslationsMap = new Map<MappingType, string>(
[
[MappingType.DATA, 'gateway.data-mapping'],
[MappingType.REQUESTS, 'gateway.requests-mapping']
]
);
export const MappingHintTranslationsMap = new Map<MappingType, string>(
[
[MappingType.DATA, 'gateway.data-mapping-hint'],
[MappingType.REQUESTS, 'gateway.requests-mapping-hint']
]
);
export const QualityTypes = [0, 1 ,2];
export const QualityTypeTranslationsMap = new Map<number, string>(
[
[0, 'gateway.qos.at-most-once'],
[1, 'gateway.qos.at-least-once'],
[2, 'gateway.qos.exactly-once']
]
);
export enum ConvertorType {
JSON = 'json',
BYTES = 'bytes',
CUSTOM = 'custom'
}
export const ConvertorTypeTranslationsMap = new Map<ConvertorType, string>(
[
[ConvertorType.JSON, 'gateway.JSON'],
[ConvertorType.BYTES, 'gateway.bytes'],
[ConvertorType.CUSTOM, 'gateway.custom']
]
);
export enum SourceTypes {
MSG = 'message',
TOPIC = 'topic',
CONST = 'constant'
}
export enum DeviceInfoType {
FULL = 'full',
PARTIAL = 'partial'
}
export const SourceTypeTranslationsMap = new Map<SourceTypes, string>(
[
[SourceTypes.MSG, 'gateway.source-type.msg'],
[SourceTypes.TOPIC, 'gateway.source-type.topic'],
[SourceTypes.CONST, 'gateway.source-type.const'],
]
);
export enum RequestType {
CONNECT_REQUEST = 'connectRequests',
DISCONNECT_REQUEST = 'disconnectRequests',
ATTRIBUTE_REQUEST = 'attributeRequests',
ATTRIBUTE_UPDATE = 'attributeUpdates',
SERVER_SIDE_RPC = 'serverSideRpc'
}
export const RequestTypesTranslationsMap = new Map<RequestType, string>(
[
[RequestType.CONNECT_REQUEST, 'gateway.request.connect-request'],
[RequestType.DISCONNECT_REQUEST, 'gateway.request.disconnect-request'],
[RequestType.ATTRIBUTE_REQUEST, 'gateway.request.attribute-request'],
[RequestType.ATTRIBUTE_UPDATE, 'gateway.request.attribute-update'],
[RequestType.SERVER_SIDE_RPC, 'gateway.request.rpc-connection'],
]
);
export enum MappingKeysType {
ATTRIBUTES = 'attributes',
TIMESERIES = 'timeseries',
CUSTOM = 'extensionConfig'
}
export const MappingKeysPanelTitleTranslationsMap = new Map<MappingKeysType, string>(
[
[MappingKeysType.ATTRIBUTES, 'gateway.attributes'],
[MappingKeysType.TIMESERIES, 'gateway.timeseries'],
[MappingKeysType.CUSTOM, 'gateway.keys']
]
);
export const MappingKeysAddKeyTranslationsMap = new Map<MappingKeysType, string>(
[
[MappingKeysType.ATTRIBUTES, 'gateway.add-attribute'],
[MappingKeysType.TIMESERIES, 'gateway.add-timeseries'],
[MappingKeysType.CUSTOM, 'gateway.add-key']
]
);
export const MappingKeysDeleteKeyTranslationsMap = new Map<MappingKeysType, string>(
[
[MappingKeysType.ATTRIBUTES, 'gateway.delete-attribute'],
[MappingKeysType.TIMESERIES, 'gateway.delete-timeseries'],
[MappingKeysType.CUSTOM, 'gateway.delete-key']
]
);
export const MappingKeysNoKeysTextTranslationsMap = new Map<MappingKeysType, string>(
[
[MappingKeysType.ATTRIBUTES, 'gateway.no-attributes'],
[MappingKeysType.TIMESERIES, 'gateway.no-timeseries'],
[MappingKeysType.CUSTOM, 'gateway.no-keys']
]
);
export enum ServerSideRPCType {
ONE_WAY = 'oneWay',
TWO_WAY = 'twoWay'
}
export const getDefaultConfig = (resourcesService: ResourcesService, type: string): Observable<any> =>
resourcesService.loadJsonResource(`/assets/metadata/connector-default-configs/${type}.json`);
export enum MappingValueType {
STRING = 'string',
INTEGER = 'integer',
DOUBLE = 'double',
BOOLEAN = 'boolean'
}
export const mappingValueTypesMap = new Map<MappingValueType, ValueTypeData>(
[
[
MappingValueType.STRING,
{
name: 'value.string',
icon: 'mdi:format-text'
}
],
[
MappingValueType.INTEGER,
{
name: 'value.integer',
icon: 'mdi:numeric'
}
],
[
MappingValueType.DOUBLE,
{
name: 'value.double',
icon: 'mdi:numeric'
}
],
[
MappingValueType.BOOLEAN,
{
name: 'value.boolean',
icon: 'mdi:checkbox-marked-outline'
}
]
]
);
export const DataConversionTranslationsMap = new Map<ConvertorType, string>(
[
[ConvertorType.JSON, 'gateway.JSON-hint'],
[ConvertorType.BYTES, 'gateway.bytes-hint'],
[ConvertorType.CUSTOM, 'gateway.custom-hint']
]
);

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

@ -86,6 +86,13 @@ import { PowerButtonWidgetComponent } from '@home/components/widget/lib/rpc/powe
import { SliderWidgetComponent } from '@home/components/widget/lib/rpc/slider-widget.component';
import { ToggleButtonWidgetComponent } from '@home/components/widget/lib/button/toggle-button-widget.component';
import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/chart/time-series-chart-widget.component';
import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component';
import { MappingTableComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table.component';
import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dialog/mapping-dialog.component';
import { DeviceInfoTableComponent } from '@home/components/widget/lib/gateway/connectors-configuration/device-info-table.component';
import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component';
import { BrokerSecurityComponent } from '@home/components/widget/lib/gateway/connectors-configuration/broker-security.component';
import { EllipsisChipListDirective } from '@home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive';
import { StatusWidgetComponent } from '@home/components/widget/lib/indicator/status-widget.component';
import { LatestChartComponent } from '@home/components/widget/lib/chart/latest-chart.component';
import { PieChartWidgetComponent } from '@home/components/widget/lib/chart/pie-chart-widget.component';
@ -117,6 +124,12 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad
LegendComponent,
FlotWidgetComponent,
GatewayConnectorComponent,
AddConnectorDialogComponent,
MappingTableComponent,
MappingDialogComponent,
DeviceInfoTableComponent,
MappingDataKeysPanelComponent,
BrokerSecurityComponent,
GatewayLogsComponent,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
@ -126,6 +139,7 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad
GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent,
GatewayServiceRPCConnectorTemplateDialogComponent,
EllipsisChipListDirective,
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent,
CountWidgetComponent,
@ -180,9 +194,16 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad
LegendComponent,
FlotWidgetComponent,
GatewayConnectorComponent,
AddConnectorDialogComponent,
MappingTableComponent,
MappingDialogComponent,
DeviceInfoTableComponent,
MappingDataKeysPanelComponent,
BrokerSecurityComponent,
GatewayLogsComponent,
GatewayServiceRPCConnectorComponent,
GatewayServiceRPCConnectorTemplatesComponent,
EllipsisChipListDirective,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
DeviceGatewayCommandComponent,

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

@ -30,6 +30,7 @@ import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
CellClickColumnInfo,
DataKey,
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation,
@ -161,7 +162,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
generateDataKey: this.generateDataKey.bind(this),
fetchEntityKeysForDevice: this.fetchEntityKeysForDevice.bind(this),
fetchEntityKeys: this.fetchEntityKeys.bind(this),
fetchDashboardStates: this.fetchDashboardStates.bind(this)
fetchDashboardStates: this.fetchDashboardStates.bind(this),
fetchCellClickColumns: this.fetchCellClickColumns.bind(this)
};
widgetEditMode = this.utils.widgetEditMode;
@ -870,6 +872,30 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
}
}
private fetchCellClickColumns(): Array<CellClickColumnInfo> {
if (this.modelValue) {
const configuredColumns = new Array<CellClickColumnInfo>();
if (this.modelValue.config?.datasources[0]?.dataKeys?.length) {
configuredColumns.push(...this.keysToCellClickColumns(this.modelValue.config.datasources[0].dataKeys));
}
if (this.modelValue.config?.alarmSource?.dataKeys?.length) {
configuredColumns.push(...this.keysToCellClickColumns(this.modelValue.config.alarmSource.dataKeys));
}
return configuredColumns;
}
}
private keysToCellClickColumns(dataKeys: Array<DataKey>): Array<CellClickColumnInfo> {
const result: Array<CellClickColumnInfo> = [];
for (const dataKey of dataKeys) {
result.push({
name: dataKey.name,
label: dataKey?.label
});
}
return result;
}
private createFilterForDashboardState(query: string): (stateId: string) => boolean {
const lowercaseQuery = query.toLowerCase();
return stateId => stateId.toLowerCase().indexOf(lowercaseQuery) === 0;

4
ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss

@ -17,13 +17,15 @@
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
gap: 4px;
}
:host {
.tb-hint-tooltip-icon {
color: #E0E0E0;
overflow: visible;
order: 3;
margin-left: 4px;
&:hover {
color: #9E9E9E;
}

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

@ -1116,8 +1116,8 @@ export const endIntervalDate = (current: moment_.Moment, interval: IntervalType)
};
export const calculateAggIntervalWithSubscriptionTimeWindow
= (subsTw: SubscriptionTimewindow, endTs: number, timestamp: number): [number, number] => {
if (subsTw.aggregation.type === AggregationType.NONE) {
= (subsTw: SubscriptionTimewindow, endTs: number, timestamp: number, aggType?: AggregationType): [number, number] => {
if ((aggType || subsTw.aggregation.type) === AggregationType.NONE) {
return [timestamp, timestamp];
} else {
return calculateInterval(subsTw.startTs, endTs, subsTw.aggregation.interval, subsTw.tsOffset, subsTw.timezone, timestamp);

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

@ -359,6 +359,8 @@ export interface DataKey extends KeyInfo {
_hash?: number;
}
export type CellClickColumnInfo = Pick<DataKey, 'name' | 'label'>
export enum DataKeyConfigMode {
general = 'general',
advanced = 'advanced'
@ -686,6 +688,7 @@ export interface WidgetActionDescriptor extends WidgetAction {
displayName?: string;
useShowWidgetActionFunction?: boolean;
showWidgetActionFunction?: string;
columnIndex?: number;
}
export const actionDescriptorToAction = (descriptor: WidgetActionDescriptor): WidgetAction => {
@ -696,6 +699,7 @@ export const actionDescriptorToAction = (descriptor: WidgetActionDescriptor): Wi
delete result.displayName;
delete result.useShowWidgetActionFunction;
delete result.showWidgetActionFunction;
delete result.columnIndex;
return result;
};

96
ui-ngx/src/assets/help/en_US/widget/lib/gateway/expressions_fn.md

@ -0,0 +1,96 @@
### Expressions
#### JSON Path:
The expression field is used to extract data from the MQTT message. There are various available options for different parts of the messages:
- The JSONPath format can be used to extract data from the message body.
- The regular expression format can be used to extract data from the topic where the message will arrive.
- Slices can only be used in the expression fields of bytes converters.
JSONPath expressions specify the items within a JSON structure (which could be an object, array, or nested combination of both) that you want to access. These expressions can select elements from JSON data on specific criteria. Here's a basic overview of how JSONPath expressions are structured:
- `$`: The root element of the JSON document;
- `.`: Child operator used to select child elements. For example, $.store.book ;
- `[]`: Child operator used to select child elements. $['store']['book'] accesses the book array within a store object;
##### Examples:
For example, if we want to extract the device name from the following message, we can use the expression below:
MQTT message:
```
{
"sensorModelInfo": {
"sensorName": "AM-123",
"sensorType": "myDeviceType"
},
"data": {
"temp": 12.2,
"hum": 56,
"status": "ok"
}
}
{:copy-code}
```
Expression:
`${sensorModelInfo.sensorName}`
Converted data:
`AM-123`
If we want to extract all data from the message above, we can use the following expression:
`${data}`
Converted data:
`{"temp": 12.2, "hum": 56, "status": "ok"}`
Or if we want to extract specific data (for example “temperature”), you can use the following expression:
`${data.temp}`
And as a converted data we will get:
`12.2`
<br/>
#### Regular expressions for topic:
Device name or device profile can be parsed from the MQTT topic using regular expression. A regular expression, often abbreviated as regex or regexp, is a sequence of characters that forms a search pattern, primarily used for string matching and manipulation.
##### Regular expression for topic examples:
| Topic | Regular expression | Output data | Description |
|:---------------------------|----------------------------------|--------------------------|--------------------------------------|
| /devices/AM123/mytype/data | /devices/([^/]+)/mytype/data | AM123 | Getting device name from topic |
| /devices/AM123/mytype/data | /devices/[A-Z0-9]+/([^/]+)/data | mytype | Getting device profile from topic |
<br/>
#### Bytes converter:
For bytes converter, expression fields can use slices format only. A slice specifies how to slice a sequence, determining the start point, and the endpoint. Here's a basic overview of slice components:
- `start`: The starting index of the slice. It is included in the slice. If omitted, slicing starts at the beginning of the sequence. Indexing starts at 0, so the first element of the sequence is at index 0.
- `stop`: The ending index of the slice. It is excluded from the slice, meaning the slice will end just before this index. If omitted, slicing goes through the end of the sequence.
##### Bytes parsing examples:
| Message body | Slice | Output data | Description |
|:-----------------------|-----------------|--------------------------|------------------------------|
| AM123,mytype,12.2,45 | [:5] | AM123 | Extracting device name |
| AM123,mytype,12.2,45 | [:] | AM123,mytype,12.2,45 | Extracting all data |
| AM123,mytype,12.2,45 | [18:] | 45 | Extracting humidity value |
| AM123,mytype,12.2,45 | [13:17] | 12.2 | Extracting temperature value |

15
ui-ngx/src/assets/help/en_US/widget/lib/gateway/topic-filter_fn.md

@ -0,0 +1,15 @@
### Topic filter
The `topicFilter` supports special symbols: `#` and `+`, allowing to subscribe to multiple topics.
Also, the MQTT connector supports shared subscriptions. To create a shared subscription you need to add `$share/`
as a prefix for the topic filter and shared subscription group name. For example to subscribe to the my-shared-topic
in group **my-group-name** you can set the topic filter to “$share/**my-group-name**/my-shared-topic”.
#### Examples:
| Topic | Topic filter | Payload | Description |
|:-----------------------|----------------------------------|---------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| sensor/data | sensor/data | `{"serialNumber": "SN-001", "sensorType": "Thermometer", "sensorModel": "T1000", "temp": 42, "hum": 58}` | Device Name is part of the payload, to extract it you need to use JSON Path expression |
| sensor/SN-001/data | sensor/+/data | `{"sensorType": "Thermometer", "sensorModel": "T1000", "temp": 42, "hum": 58}` | Device Name is part of the topic, to extract it you need to use regular expression. |

3
ui-ngx/src/assets/locale/locale.constant-ar_AE.json

@ -2078,8 +2078,7 @@
"step": "خطوة",
"min-evaluation-period": "الفترة الدنيا للتقييم",
"max-evaluation-period": "الفترة القصوى للتقييم"
},
"composite-operations-support": "يدعم عمليات القراءة/الكتابة/المراقبة المركبة"
}
},
"snmp": {
"add-communication-config": "إضافة تكوين الاتصال",

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

@ -1899,6 +1899,11 @@
"step": "Step",
"min-evaluation-period": "Minimum evaluation period",
"max-evaluation-period": "Maximum evaluation period"
},
"default-object-id": "Default Object Version (Attribute)",
"default-object-id-ver": {
"v1-0": "1.0",
"v1-1": "1.1"
}
},
"snmp": {
@ -2709,11 +2714,38 @@
},
"gateway": {
"add-entry": "Add configuration",
"add-attribute": "Add attribute",
"add-key": "Add key",
"add-timeseries": "Add time series",
"add-mapping": "Add mapping",
"advanced": "Advanced",
"attributes": "Attributes",
"attribute-filter": "Attribute filter",
"attribute-filter-hint": "Filter for incoming attribute name from platform, supports regular expression.",
"attribute-filter-required": "Attribute filter required.",
"attribute-name-expression": "Attribute name expression",
"attribute-name-expression-required": "Attribute name expression required.",
"attribute-name-expression-hint": "Hint for Attribute name expression",
"basic": "Basic",
"broker": {
"connection": "Connection to broker",
"name": "Broker name",
"name-required": "Broker name required.",
"security-types": {
"anonymous": "Anonymous",
"basic": "Basic",
"certificates": "Certificates"
}
},
"CA-certificate-path": "Path to CA certificate file",
"path-to-CA-cert-required": "Path to CA certificate file is required.",
"change-connector-title": "Confirm connector change",
"change-connector-text": "Switching connectors will discard any unsaved changes. Continue?",
"checking-device-activity": "Checking device activity",
"command": "Docker commands",
"command-copied-message": "Docker command has been copied to clipboard",
"configuration": "Configuration",
"add-connector": "Add connector",
"connector-add": "Add new connector",
"connector-enabled": "Enable connector",
"connector-name": "Connector name",
@ -2729,6 +2761,9 @@
"connectors-table-actions": "Actions",
"connectors-table-key": "Key",
"connectors-table-class": "Class",
"copy-username": "Copy username",
"copy-password": "Copy password",
"copy-client-id": "Copy client ID",
"connector-created": "Connector created.",
"connector-updated": "Connector updated.",
"rpc-command-save-template": "Save Template",
@ -2738,10 +2773,30 @@
"gateway-configuration": "General Configuration",
"docker-label": "Use the following instruction to run IoT Gateway in Docker compose with credentials for selected device",
"install-docker-compose": "Use the instructions to download, install and setup docker compose",
"device-info-settings": "Device info settings",
"device-info": {
"entity-field": "Entity field",
"source": "Source",
"expression": "Value / Expression",
"expression-hint": "Show help",
"name": "Name",
"profile-name": "Profile name",
"device-name-expression": "Device name expression",
"device-name-expression-required": "Device name expression required.",
"device-profile-expression-required": "Device profile expression required."
},
"device-name-filter": "Device name filter",
"device-name-filter-hint": "This field supports Regular expressions to filter incoming data by device name.",
"device-name-filter-required": "Device name filter is required.",
"details": "Details",
"delete-mapping-title": "Delete mapping ?",
"download-configuration-file": "Download configuration file",
"download-docker-compose": "Download docker-compose.yml for your gateway",
"enable-remote-logging": "Enable remote logging",
"ellipsis-chips-text": "+ {{count}} more",
"launch-gateway": "Launch gateway",
"launch-docker-compose": "Start the gateway using the following command in the terminal from folder with docker-compose.yml file",
"logs-configuration": "Logs configuration",
"create-new-gateway": "Create a new gateway",
"create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?",
"created-time": "Created time",
@ -2750,14 +2805,46 @@
"configuration-delete-dialog-input": "Gateway name",
"configuration-delete-dialog-input-required": "Gateway name is mandatory",
"configuration-delete-dialog-confirm": "Turn Off",
"connector-duplicate-name": "Connector with such name already exists.",
"connector-side": "Connector side",
"payload-type": "Payload type",
"platform-side": "Platform side",
"JSON": "JSON",
"JSON-hint": "Converter for this payload type processes MQTT messages in JSON format. It uses JSON Path expressions to extract vital details such as device names, device profile names, attributes, and time series from the message. And regular expressions to get device details from topics.",
"bytes": "Bytes",
"bytes-hint": "Converter for this payload type designed for binary MQTT payloads, this converter directly interprets binary data to retrieve device names and device profile names, along with attributes and time series, using specific byte positions for data extraction.",
"custom": "Custom",
"custom-hint": "This option allows you to use a custom converter for specific data tasks. You need to add your custom converter to the extension folder and enter its class name in the UI settings. Any keys you provide will be sent as configuration to your custom converter.",
"client-cert-path": "Path to client certificate file",
"path-to-client-cert-required": "Path to client certificate file is required.",
"client-id": "Client ID",
"data-conversion": "Data conversion",
"data-mapping": "Data mapping",
"data-mapping-hint": "Data mapping provides the capability to parse and convert the data received from a MQTT client in incoming messages into specific attributes and time series data keys.",
"delete": "Delete configuration",
"delete-attribute": "Delete attribute",
"delete-key": "Delete key",
"delete-timeseries": "Delete time series",
"default": "Default",
"download-tip": "Download configuration file",
"drop-file": "Drop file here or",
"extension": "Extension",
"extension-hint": "Put your converter classname in the field. Custom converter with such class should be in extension/mqtt folder.",
"extension-required": "Extension is required.",
"extension-configuration": "Extension configuration",
"extension-configuration-hint": "Configuration for convertor",
"fill-connector-defaults": "Fill configuration with default values",
"fill-connector-defaults-hint": "This property allows to fill connector configuration with default values on it's creation.",
"from-device-request-settings": "Input request parsing",
"from-device-request-settings-hint": "These fields support JSONPath expressions to extract a name from incoming message.",
"to-device-response-settings": "Output request processing",
"to-device-response-settings-hint": "For these fields you can use the following variables and they will be replaced with actual values: ${deviceName}, ${attributeKey}, ${attributeValue}",
"gateway": "Gateway",
"gateway-exists": "Device with same name is already exists.",
"gateway-name": "Gateway name",
"gateway-name-required": "Gateway name is required.",
"gateway-saved": "Gateway configuration successfully saved.",
"generate-client-id": "Generate Client ID",
"grpc": "GRPC",
"grpc-keep-alive-timeout": "Keep alive timeout (in ms)",
"grpc-keep-alive-timeout-required": "Keep alive timeout is required",
@ -2787,8 +2874,11 @@
"inactivity-timeout-seconds-required": "Inactivity timeout is required",
"inactivity-timeout-seconds-min": "Inactivity timeout can not be less then 1",
"inactivity-timeout-seconds-pattern": "Inactivity timeout is not valid",
"host": "Host",
"host-required": "Host is required.",
"json-parse": "Not valid JSON.",
"json-required": "Field cannot be empty.",
"JSONPath-hint": "This field supports constants and JSONPath expressions.",
"logs": {
"logs": "Logs",
"days": "days",
@ -2813,16 +2903,39 @@
"backup-count-min": "Backup count can not be less then 1",
"backup-count-required": "Backup count required"
},
"max-number-of-workers": "Max number of workers",
"max-number-of-workers-hint": "Maximal number of workers threads for converters \n(The amount of workers changes dynamically, depending on load) \nRecommended amount 50-150.",
"max-number-of-workers-required": "Max number of workers is required.",
"max-messages-queue-for-worker": "Max messages queue per worker",
"max-messages-queue-for-worker-hint": "Maximal messages count that will be in the queue \nfor each converter worker.",
"max-messages-queue-for-worker-required": "Max messages queue per worker is required.",
"min-pack-send-delay": "Min pack send delay (in ms)",
"min-pack-send-delay-required": "Min pack send delay is required",
"min-pack-send-delay-min": "Min pack send delay can not be less then 0",
"mqtt-version": "MQTT version",
"name": "Name",
"name-required": "Name is required.",
"no-attributes": "No attributes",
"no-connectors": "No connectors",
"no-data": "No configurations",
"no-gateway-found": "No gateway found.",
"no-gateway-matching": " '{{item}}' not found.",
"no-timeseries": "No time series",
"no-keys": "No keys",
"path-hint": "The path is local to the gateway file system",
"path-logs": "Path to log files",
"path-logs-required": "Path is required.",
"password": "Password",
"password-required": "Password is required.",
"permit-without-calls": "Keep alive permit without calls",
"port": "Port",
"port-required": "Port is required.",
"port-limits-error": "Port should be number from {{min}} to {{max}}.",
"private-key-path": "Path to private key file",
"path-to-private-key-required": "Path to private key file is required.",
"raw": "Raw",
"retain": "Retain",
"retain-hint": "This flag tells the broker to store the message for a topic\nand ensures any new client subscribing to that topic\nwill receive the stored message.",
"remote": "Remote configuration",
"remote-logging-level": "Logging level",
"remove-entry": "Remove configuration",
@ -2918,8 +3031,36 @@
"write-multiple-holding-registers": "16: Write Multiple Holding Registers",
"json-value-invalid": "JSON value has an invalid format"
},
"request" : {
"connect-request": "Connect request",
"disconnect-request": "Disconnect request",
"attribute-request": "Attribute request",
"attribute-update": "Attribute update",
"rpc-connection": "RPC command"
},
"request-type": "Request type",
"requests-mapping": "Requests mapping",
"requests-mapping-hint": "MQTT Connector requests allows you to connect, disconnect, process attribute requests from the device, handle attribute updates on the server and RPC processing configuration.",
"request-topic-expression": "Request topic expression",
"request-topic-expression-required": "Request topic expression is required.",
"response-timeout": "Response timeout (ms)",
"response-timeout-required": "Response timeout is required.",
"response-timeout-limits-error": "Timeout must be more then {{min}} ms.",
"response-topic-Qos": "Response topic QoS",
"response-topic-Qos-hint": "MQTT Quality of Service (QoS) is an agreement between the message sender and receiver that defines the level of delivery guarantee for a specific message.",
"response-topic-expression": "Response topic expression",
"response-topic-expression-required": "Response topic expression is required.",
"response-value-expression": "Response value expression",
"response-value-expression-required": "Response value expression is required.",
"value": "Value",
"value-required": "Value is required.",
"value-expression": "Value expression",
"value-expression-required": "Value expression is required.",
"with-response": "With response",
"without-response": "Without response",
"other": "Other",
"save-tip": "Save configuration file",
"security": "Security",
"security-type": "Security type",
"security-types": {
"access-token": "Access Token",
@ -2928,11 +3069,15 @@
"tls-access-token": "TLS + Access Token",
"tls-private-key": "TLS + Private Key"
},
"select-connector": "Select connector to display config",
"send-change-data": "Send data only on change",
"send-change-data-hint": "The values will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and time series in the converter output.",
"server-port": "Server port",
"set": "Set",
"statistics": {
"statistic": "Statistic",
"statistics": "Statistics",
"statistic-commands-empty": "No statistics available",
"statistic-commands-empty": "No configured statistic keys found. You can configure them in \"Statistics\" tab in general configuration.",
"commands": "Commands",
"send-period": "Statistic send period (in sec)",
"send-period-required": "Statistic send period is required",
@ -2987,11 +3132,21 @@
"memory-storage": "Memory storage",
"sqlite": "SQLITE"
},
"source-type": {
"msg": "Extract from message",
"topic": "Extract from topic",
"const": "Constant"
},
"workers-settings": "Workers settings",
"thingsboard": "ThingsBoard",
"general": "General",
"thingsboard-host": "ThingsBoard host",
"timeseries": "Time series",
"key": "Key",
"keys": "Keys",
"key-required": "Key is required.",
"thingsboard-host": "Platform host",
"thingsboard-host-required": "Host is required.",
"thingsboard-port": "ThingsBoard port",
"thingsboard-port": "Platform port",
"thingsboard-port-max": "Maximum port number is 65535.",
"thingsboard-port-min": "Minimum port number is 1.",
"thingsboard-port-pattern": "Port is not valid.",
@ -2999,8 +3154,14 @@
"tidy": "Tidy",
"tidy-tip": "Tidy config JSON",
"title-connectors-json": "Connector {{typeName}} configuration",
"type": "Type",
"topic-filter": "Topic filter",
"topic-required": "Topic filter is required.",
"tls-path-ca-certificate": "Path to CA certificate on gateway",
"tls-path-client-certificate": "Path to client certificate on gateway",
"method-filter": "Method filter",
"method-filter-hint": "Regular expression to filter incoming RPC method from platform.",
"method-filter-required": "Method filter is required.",
"messages-ttl-check-in-hours": "Messages TTL check in hours",
"messages-ttl-check-in-hours-required": "Messages TTL check in hours is required.",
"messages-ttl-check-in-hours-min": "Min number is 1.",
@ -3012,19 +3173,26 @@
"mqtt-qos": "QoS",
"mqtt-qos-required": "QoS is required",
"mqtt-qos-range": "QoS values range is from 0 to 1",
"qos": {
"at-most-once": "0 - At most once",
"at-least-once": "1 - At least once",
"exactly-once": "2 - Exactly once"
},
"tls-path-private-key": "Path to private key on gateway",
"toggle-fullscreen": "Toggle fullscreen",
"transformer-json-config": "Configuration JSON*",
"update-config": "Add/update configuration JSON",
"username": "Username",
"username-required": "Username is required.",
"hints": {
"remote-configuration": "Enables remote configuration and management of the gateway",
"remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget",
"host": "Hostname or IP address of ThingsBoard server",
"port": "Port of MQTT service on ThingsBoard server",
"token": "Access token for the gateway from ThingsBoard server",
"client-id": "MQTT client id for the gateway form ThingsBoard server",
"username": "MQTT username for the gateway form ThingsBoard server",
"password": "MQTT password for the gateway form ThingsBoard server",
"host": "Hostname or IP address of platform server",
"port": "Port of MQTT service on platform server",
"token": "Access token for the gateway from platform server",
"client-id": "MQTT client id for the gateway form platform server",
"username": "MQTT username for the gateway form platform server",
"password": "MQTT password for the gateway form platform server",
"ca-cert": "Path to CA certificate file",
"date-form": "Date format in log message",
"data-folder": "Path to folder, that will contains data (Relative or Absolute)",
@ -3033,10 +3201,10 @@
"backup-count": "If backup count is > 0, when a rollover is done, no more than backup count files are kept - the oldest ones are deleted",
"storage": "Provides configuration for saving incoming data before it is sent to the platform",
"max-file-count": "Maximum count of file that will be created",
"max-read-count": "Count of messages to get from storage and send to ThingsBoard",
"max-read-count": "Count of messages to get from storage and send to platform",
"max-records": "Maximum count of records that will be stored in one file",
"read-record-count": "Count of messages to get from storage and send to ThingsBoard",
"max-records-count": "Maximum count of data in storage before send to ThingsBoard",
"read-record-count": "Count of messages to get from storage and send to platform",
"max-records-count": "Maximum count of data in storage before send to platform",
"ttl-check-hour": "How often will Gateway check data for obsolescence",
"ttl-messages-day": "Maximum days that storage will save data",
"commands": "Commands for collecting additional statistic",
@ -3054,7 +3222,10 @@
"grpc-min-time-between-pings": "Minimum amount of time the server should wait between sending keepalive ping messages",
"grpc-max-pings-without-data": "Maximum number of keepalive ping messages that the server can send without receiving any data before it considers the connection dead.",
"grpc-min-ping-interval-without-data": "Minimum amount of time the server should wait between sending keepalive ping messages when there is no data being sent or received.",
"permit-without-calls": "Allow server to keep the GRPC connection alive even when there are no active RPC calls."
"permit-without-calls": "Allow server to keep the GRPC connection alive even when there are no active RPC calls.",
"memory": "Your data will be stored in the in-memory queue, it is a fastest but no persistence guarantee.",
"file": "Your data will be stored in separated files and will be saved even after the gateway restart.",
"sqlite": "Your data will be stored in file based database. And will be saved even after the gateway restart."
}
},
"grid": {
@ -5295,6 +5466,10 @@
"action-source": "Action source",
"select-action-source": "Select action source",
"action-source-required": "Action source is required.",
"column-index": "Column index",
"select-column-index": "Select column index",
"column-index-required": "Column index is required.",
"not-set": "Not set",
"action-name": "Name",
"action-name-required": "Action name is required.",
"action-name-not-unique": "Another action with the same name already exists.\nAction name should be unique within the same action source.",
@ -7299,6 +7474,7 @@
"widget-action": {
"action-cell-button": "Action cell button",
"row-click": "On row click",
"cell-click": "On cell click",
"polygon-click": "On polygon click",
"marker-click": "On marker click",
"circle-click": "On circle click",
@ -7307,6 +7483,7 @@
"element-click": "On HTML element click",
"pie-slice-click": "On slice click",
"row-double-click": "On row double click",
"cell-double-click": "On cell double click",
"card-click": "On card click",
"click": "On click"
}

58
ui-ngx/src/assets/metadata/connector-default-configs/bacnet.json

@ -0,0 +1,58 @@
{
"general": {
"objectName": "TB_gateway",
"address": "0.0.0.0:47808",
"objectIdentifier": 599,
"maxApduLengthAccepted": 1476,
"segmentationSupported": "segmentedBoth",
"vendorIdentifier": 15
},
"devices": [
{
"deviceName": "BACnet Device ${objectName}",
"deviceType": "default",
"address": "192.168.2.110:47808",
"pollPeriod": 10000,
"attributes": [
{
"key": "temperature",
"type": "string",
"objectId": "analogOutput:1",
"propertyId": "presentValue"
}
],
"timeseries": [
{
"key": "state",
"type": "bool",
"objectId": "binaryValue:1",
"propertyId": "presentValue"
}
],
"attributeUpdates": [
{
"key": "brightness",
"requestType": "writeProperty",
"objectId": "analogOutput:1",
"propertyId": "presentValue"
}
],
"serverSideRpc": [
{
"method": "set_state",
"requestType": "writeProperty",
"requestTimeout": 10000,
"objectId": "binaryOutput:1",
"propertyId": "presentValue"
},
{
"method": "get_state",
"requestType": "readProperty",
"requestTimeout": 10000,
"objectId": "binaryOutput:1",
"propertyId": "presentValue"
}
]
}
]
}

73
ui-ngx/src/assets/metadata/connector-default-configs/ble.json

@ -0,0 +1,73 @@
{
"passiveScanMode": true,
"showMap": false,
"scanner": {
"timeout": 10000,
"deviceName": "Device name"
},
"devices": [
{
"name": "Temperature and humidity sensor",
"MACAddress": "4C:65:A8:DF:85:C0",
"pollPeriod": 500000,
"showMap": false,
"timeout": 10000,
"connectRetry": 5,
"connectRetryInSeconds": 0,
"waitAfterConnectRetries": 10,
"telemetry": [
{
"key": "temperature",
"method": "notify",
"characteristicUUID": "226CAA55-6476-4566-7562-66734470666D",
"valueExpression": "[2]"
},
{
"key": "humidity",
"method": "notify",
"characteristicUUID": "226CAA55-6476-4566-7562-66734470666D",
"valueExpression": "[0]"
}
],
"attributes": [
{
"key": "name",
"method": "read",
"characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB",
"valueExpression": "[0:2]cm [2:]A"
},
{
"key": "values",
"method": "read",
"characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB",
"valueExpression": "All values: [:]"
}
],
"attributeUpdates": [
{
"attributeOnThingsBoard": "sharedName",
"characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB"
}
],
"serverSideRpc": [
{
"methodRPC": "rpcMethod1",
"withResponse": true,
"characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB",
"methodProcessing": "read"
},
{
"methodRPC": "rpcMethod2",
"withResponse": true,
"characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB",
"methodProcessing": "write"
},
{
"methodRPC": "rpcMethod3",
"withResponse": true,
"methodProcessing": "scan"
}
]
}
]
}

89
ui-ngx/src/assets/metadata/connector-default-configs/can.json

@ -0,0 +1,89 @@
{
"interface": "socketcan",
"channel": "vcan0",
"backend": {
"fd": true
},
"reconnectPeriod": 5,
"devices": [
{
"name": "Car",
"sendDataOnlyOnChange": false,
"enableUnknownRpc": true,
"strictEval": false,
"attributes": [
{
"key": "isDriverDoorOpened",
"nodeId": 41,
"command": "2:2:big:8717",
"value": "4:1:int",
"expression": "bool(value & 0b00000100)",
"polling": {
"type": "once",
"dataInHex": "AB CD AB CD"
}
}
],
"timeseries": [
{
"key": "rpm",
"nodeId": 1918,
"isExtendedId": true,
"command": "2:2:big:48059",
"value": "4:2:big:int",
"expression": "value / 4",
"polling": {
"type": "always",
"period": 5,
"dataInHex": "aaaa bbbb aaaa bbbb"
}
},
{
"key": "milliage",
"nodeId": 1918,
"isExtendedId": true,
"value": "4:2:little:int",
"expression": "value * 10",
"polling": {
"type": "always",
"period": 30,
"dataInHex": "aa bb cc dd ee ff aa bb"
}
}
],
"attributeUpdates": [
{
"attributeOnThingsBoard": "softwareVersion",
"nodeId": 64,
"isExtendedId": true,
"dataLength": 4,
"dataExpression": "value + 5",
"dataByteorder": "little"
}
],
"serverSideRpc": [
{
"method": "sendSameData",
"nodeId": 4,
"isExtendedId": true,
"isFd": true,
"bitrateSwitch": true,
"dataInHex": "aa bb cc dd ee ff aa bb aa bb cc d ee ff"
},
{
"method": "setLightLevel",
"nodeId": 5,
"dataLength": 2,
"dataByteorder": "little",
"dataBefore": "00AA"
},
{
"method": "setSpeed",
"nodeId": 16,
"dataAfter": "0102",
"dataExpression": "userSpeed if maxAllowedSpeed > userSpeed else maxAllowedSpeed"
}
]
}
]
}

65
ui-ngx/src/assets/metadata/connector-default-configs/ftp.json

@ -0,0 +1,65 @@
{
"host": "0.0.0.0",
"port": 21,
"TLSSupport": false,
"security": {
"type": "basic",
"username": "admin",
"password": "admin"
},
"paths": [
{
"devicePatternName": "asd",
"devicePatternType": "Device",
"delimiter": ",",
"path": "fol/*_hello*.txt",
"readMode": "FULL",
"maxFileSize": 5,
"pollPeriod": 500,
"txtFileDataView": "SLICED",
"withSortingFiles": true,
"attributes": [
{
"key": "temp",
"value": "[1:]"
},
{
"key": "tmp",
"value": "[0:1]"
}
],
"timeseries": [
{
"type": "int",
"key": "[0:1]",
"value": "[0:1]"
},
{
"type": "int",
"key": "temp",
"value": "[1:]"
}
]
}
],
"attributeUpdates": [
{
"path": "fol/hello.json",
"deviceNameFilter": ".*",
"writingMode": "WRITE",
"valueExpression": "{'${attributeKey}':'${attributeValue}'}"
}
],
"serverSideRpc": [
{
"deviceNameFilter": ".*",
"methodFilter": "read",
"valueExpression": "${params}"
},
{
"deviceNameFilter": ".*",
"methodFilter": "write",
"valueExpression": "${params}"
}
]
}

248
ui-ngx/src/assets/metadata/connector-default-configs/modbus.json

@ -0,0 +1,248 @@
{
"master": {
"slaves": [
{
"host": "127.0.0.1",
"port": 5021,
"type": "tcp",
"method": "socket",
"timeout": 35,
"byteOrder": "LITTLE",
"wordOrder": "LITTLE",
"retries": true,
"retryOnEmpty": true,
"retryOnInvalid": true,
"pollPeriod": 5000,
"unitId": 1,
"deviceName": "Temp Sensor",
"sendDataOnlyOnChange": true,
"connectAttemptTimeMs": 5000,
"connectAttemptCount": 5,
"waitAfterFailedAttemptsMs": 300000,
"attributes": [
{
"tag": "string_read",
"type": "string",
"functionCode": 4,
"objectsCount": 4,
"address": 1
},
{
"tag": "bits_read",
"type": "bits",
"functionCode": 4,
"objectsCount": 1,
"address": 5
},
{
"tag": "8int_read",
"type": "8int",
"functionCode": 4,
"objectsCount": 1,
"address": 6
},
{
"tag": "16int_read",
"type": "16int",
"functionCode": 4,
"objectsCount": 1,
"address": 7
},
{
"tag": "32int_read_divider",
"type": "32int",
"functionCode": 4,
"objectsCount": 2,
"address": 8,
"divider": 10
},
{
"tag": "8int_read_multiplier",
"type": "8int",
"functionCode": 4,
"objectsCount": 1,
"address": 10,
"multiplier": 10
},
{
"tag": "32int_read",
"type": "32int",
"functionCode": 4,
"objectsCount": 2,
"address": 11
},
{
"tag": "64int_read",
"type": "64int",
"functionCode": 4,
"objectsCount": 4,
"address": 13
}
],
"timeseries": [
{
"tag": "8uint_read",
"type": "8uint",
"functionCode": 4,
"objectsCount": 1,
"address": 17
},
{
"tag": "16uint_read",
"type": "16uint",
"functionCode": 4,
"objectsCount": 2,
"address": 18
},
{
"tag": "32uint_read",
"type": "32uint",
"functionCode": 4,
"objectsCount": 4,
"address": 20
},
{
"tag": "64uint_read",
"type": "64uint",
"functionCode": 4,
"objectsCount": 1,
"address": 24
},
{
"tag": "16float_read",
"type": "16float",
"functionCode": 4,
"objectsCount": 1,
"address": 25
},
{
"tag": "32float_read",
"type": "32float",
"functionCode": 4,
"objectsCount": 2,
"address": 26
},
{
"tag": "64float_read",
"type": "64float",
"functionCode": 4,
"objectsCount": 4,
"address": 28
}
],
"attributeUpdates": [
{
"tag": "shared_attribute_write",
"type": "32int",
"functionCode": 6,
"objectsCount": 2,
"address": 29
}
],
"rpc": [
{
"tag": "setValue",
"type": "bits",
"functionCode": 5,
"objectsCount": 1,
"address": 31
},
{
"tag": "getValue",
"type": "bits",
"functionCode": 1,
"objectsCount": 1,
"address": 31
},
{
"tag": "setCPUFanSpeed",
"type": "32int",
"functionCode": 16,
"objectsCount": 2,
"address": 33
},
{
"tag": "getCPULoad",
"type": "32int",
"functionCode": 4,
"objectsCount": 2,
"address": 35
}
]
}
]
},
"slave": {
"type": "tcp",
"host": "127.0.0.1",
"port": 5026,
"method": "socket",
"deviceName": "Modbus Slave Example",
"deviceType": "default",
"pollPeriod": 5000,
"sendDataToThingsBoard": false,
"byteOrder": "LITTLE",
"wordOrder": "LITTLE",
"unitId": 0,
"values": {
"holding_registers": [
{
"attributes": [
{
"address": 1,
"type": "string",
"tag": "sm",
"objectsCount": 1,
"value": "ON"
}
],
"timeseries": [
{
"address": 2,
"type": "int",
"tag": "smm",
"objectsCount": 1,
"value": "12334"
}
],
"attributeUpdates": [
{
"tag": "shared_attribute_write",
"type": "32int",
"functionCode": 6,
"objectsCount": 2,
"address": 29,
"value": 1243
}
],
"rpc": [
{
"tag": "setValue",
"type": "bits",
"functionCode": 5,
"objectsCount": 1,
"address": 31,
"value": 22
}
]
}
],
"coils_initializer": [
{
"attributes": [
{
"address": 5,
"type": "string",
"tag": "sm",
"objectsCount": 1,
"value": "12"
}
],
"timeseries": [],
"attributeUpdates": [],
"rpc": []
}
]
}
}
}

218
ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json

@ -0,0 +1,218 @@
{
"broker": {
"name": "Default Local Broker",
"host": "127.0.0.1",
"port": 1883,
"clientId": "ThingsBoard_gateway",
"version": 5,
"maxMessageNumberPerWorker": 10,
"maxNumberOfWorkers": 100,
"sendDataOnlyOnChange": false,
"security": {
"type": "anonymous"
}
},
"dataMapping": [
{
"topicFilter": "sensor/data",
"subscriptionQos": 1,
"converter": {
"type": "json",
"deviceInfo": {
"deviceNameExpressionSource": "message",
"deviceNameExpression": "${serialNumber}",
"deviceProfileExpressionSource": "message",
"deviceProfileExpression": "${sensorType}"
},
"sendDataOnlyOnChange": false,
"timeout": 60000,
"attributes": [
{
"type": "string",
"key": "model",
"value": "${sensorModel}"
},
{
"type": "string",
"key": "${sensorModel}",
"value": "on"
}
],
"timeseries": [
{
"type": "string",
"key": "temperature",
"value": "${temp}"
},
{
"type": "double",
"key": "humidity",
"value": "${hum}"
},
{
"type": "string",
"key": "combine",
"value": "${hum}:${temp}"
}
]
}
},
{
"topicFilter": "sensor/+/data",
"subscriptionQos": 1,
"converter": {
"type": "json",
"deviceInfo": {
"deviceNameExpressionSource": "topic",
"deviceNameExpression": "(?<=sensor\/)(.*?)(?=\/data)",
"deviceProfileExpressionSource": "constant",
"deviceProfileExpression": "Thermometer"
},
"sendDataOnlyOnChange": false,
"timeout": 60000,
"attributes": [
{
"type": "string",
"key": "model",
"value": "${sensorModel}"
}
],
"timeseries": [
{
"type": "double",
"key": "temperature",
"value": "${temp}"
},
{
"type": "string",
"key": "humidity",
"value": "${hum}"
}
]
}
},
{
"topicFilter": "sensor/raw_data",
"subscriptionQos": 1,
"converter": {
"type": "bytes",
"deviceInfo": {
"deviceNameExpressionSource": "message",
"deviceNameExpression": "[0:4]",
"deviceProfileExpressionSource": "constant",
"deviceProfileExpression": "default"
},
"sendDataOnlyOnChange": false,
"timeout": 60000,
"attributes": [
{
"type": "raw",
"key": "rawData",
"value": "[:]"
}
],
"timeseries": [
{
"type": "raw",
"key": "temp",
"value": "[4:]"
}
]
}
},
{
"topicFilter": "custom/sensors/+",
"subscriptionQos": 1,
"converter": {
"type": "custom",
"extension": "CustomMqttUplinkConverter",
"cached": true,
"extensionConfig": {
"temperature": 2,
"humidity": 2,
"batteryLevel": 1
}
}
}
],
"requestsMapping": {
"connectRequests": [
{
"topicFilter": "sensor/connect",
"deviceInfo": {
"deviceNameExpressionSource": "message",
"deviceNameExpression": "${serialNumber}",
"deviceProfileExpressionSource": "constant",
"deviceProfileExpression": "Thermometer"
}
},
{
"topicFilter": "sensor/+/connect",
"deviceInfo": {
"deviceNameExpressionSource": "topic",
"deviceNameExpression": "(?<=sensor\/)(.*?)(?=\/connect)",
"deviceProfileExpressionSource": "constant",
"deviceProfileExpression": "Thermometer"
}
}
],
"disconnectRequests": [
{
"topicFilter": "sensor/disconnect",
"deviceInfo": {
"deviceNameExpressionSource": "message",
"deviceNameExpression": "${serialNumber}"
}
},
{
"topicFilter": "sensor/+/disconnect",
"deviceInfo": {
"deviceNameExpressionSource": "topic",
"deviceNameExpression": "(?<=sensor\/)(.*?)(?=\/connect)"
}
}
],
"attributeRequests": [
{
"retain": false,
"topicFilter": "v1/devices/me/attributes/request",
"deviceInfo": {
"deviceNameExpressionSource": "message",
"deviceNameExpression": "${serialNumber}"
},
"attributeNameExpressionSource": "message",
"attributeNameExpression": "${versionAttribute}, ${pduAttribute}",
"topicExpression": "devices/${deviceName}/attrs",
"valueExpression": "${attributeKey}: ${attributeValue}"
}
],
"attributeUpdates": [
{
"retain": true,
"deviceNameFilter": ".*",
"attributeFilter": "firmwareVersion",
"topicExpression": "sensor/${deviceName}/${attributeKey}",
"valueExpression": "{\"${attributeKey}\":\"${attributeValue}\"}"
}
],
"serverSideRpc": [
{
"type": "twoWay",
"deviceNameFilter": ".*",
"methodFilter": "echo",
"requestTopicExpression": "sensor/${deviceName}/request/${methodName}/${requestId}",
"responseTopicExpression": "sensor/${deviceName}/response/${methodName}/${requestId}",
"responseTopicQoS": 1,
"responseTimeout": 10000,
"valueExpression": "${params}"
},
{
"type": "oneWay",
"deviceNameFilter": ".*",
"methodFilter": "no-reply",
"requestTopicExpression": "sensor/${deviceName}/request/${methodName}/${requestId}",
"valueExpression": "${params}"
}
]
}
}

66
ui-ngx/src/assets/metadata/connector-default-configs/ocpp.json

@ -0,0 +1,66 @@
{
"centralSystem": {
"name": "Central System",
"host": "127.0.0.1",
"port": 9000,
"connection": {
"type": "insecure"
},
"security": [
{
"type": "token",
"tokens": [
"Bearer ACCESS_TOKEN"
]
},
{
"type": "basic",
"credentials": [
{
"username": "admin",
"password": "admin"
}
]
}
]
},
"chargePoints": [
{
"idRegexpPattern": "bidon/hello/CP_1",
"deviceNameExpression": "${Vendor} ${Model}",
"deviceTypeExpression": "default",
"attributes": [
{
"messageTypeFilter": "MeterValues,",
"key": "temp1",
"value": "${meter_value[:].sampled_value[:].value}"
},
{
"messageTypeFilter": "MeterValues,",
"key": "vendorId",
"value": "${connector_id}"
}
],
"timeseries": [
{
"messageTypeFilter": "DataTransfer,",
"key": "temp",
"value": "${data.temp}"
}
],
"attributeUpdates": [
{
"attributeOnThingsBoard": "shared",
"valueExpression": "{\"${attributeKey}\":\"${attributeValue}\"}"
}
],
"serverSideRpc": [
{
"methodRPC": "rpc1",
"withResponse": true,
"valueExpression": "${params}"
}
]
}
]
}

58
ui-ngx/src/assets/metadata/connector-default-configs/odbc.json

@ -0,0 +1,58 @@
{
"connection": {
"str": "Driver={PostgreSQL};Server=localhost;Port=5432;Database=thingsboard;Uid=postgres;Pwd=postgres;",
"attributes": {
"autocommit": true,
"timeout": 0
},
"encoding": "utf-8",
"decoding": {
"char": "utf-8",
"wchar": "utf-8",
"metadata": "utf-16le"
},
"reconnect": true,
"reconnectPeriod": 60
},
"pyodbc": {
"pooling": false
},
"polling": {
"query": "SELECT bool_v, str_v, dbl_v, long_v, entity_id, ts FROM ts_kv WHERE ts > ? ORDER BY ts ASC LIMIT 10",
"period": 10,
"iterator": {
"column": "ts",
"query": "SELECT MIN(ts) - 1 FROM ts_kv",
"persistent": false
}
},
"mapping": {
"device": {
"type": "postgres",
"name": "'ODBC ' + entity_id"
},
"sendDataOnlyOnChange": false,
"attributes": "*",
"timeseries": [
{
"name": "value",
"value": "[i for i in [str_v, long_v, dbl_v,bool_v] if i is not None][0]"
}
]
},
"serverSideRpc": {
"enableUnknownRpc": false,
"overrideRpcConfig": true,
"methods": [
"procedureOne",
{
"name": "procedureTwo",
"args": [
"One",
2,
3.0
]
}
]
}
}

52
ui-ngx/src/assets/metadata/connector-default-configs/opcua.json

@ -0,0 +1,52 @@
{
"server": {
"name": "OPC-UA Default Server",
"url": "localhost:4840/freeopcua/server/",
"timeoutInMillis": 5000,
"scanPeriodInMillis": 5000,
"disableSubscriptions": false,
"subCheckPeriodInMillis": 100,
"showMap": false,
"security": "Basic128Rsa15",
"identity": {
"type": "anonymous"
},
"mapping": [
{
"deviceNodePattern": "Root\\.Objects\\.Device1",
"deviceNamePattern": "Device ${Root\\.Objects\\.Device1\\.serialNumber}",
"attributes": [
{
"key": "temperature °C",
"path": "${ns=2;i=5}"
}
],
"timeseries": [
{
"key": "humidity",
"path": "${Root\\.Objects\\.Device1\\.TemperatureAndHumiditySensor\\.Humidity}"
},
{
"key": "batteryLevel",
"path": "${Battery\\.batteryLevel}"
}
],
"rpc_methods": [
{
"method": "multiply",
"arguments": [
2,
4
]
}
],
"attributes_updates": [
{
"attributeOnThingsBoard": "deviceName",
"attributeOnDevice": "Root\\.Objects\\.Device1\\.serialNumber"
}
]
}
]
}
}

52
ui-ngx/src/assets/metadata/connector-default-configs/opcua_asyncio.json

@ -0,0 +1,52 @@
{
"server": {
"name": "OPC-UA Default Server",
"url": "localhost:4840/freeopcua/server/",
"timeoutInMillis": 5000,
"scanPeriodInMillis": 5000,
"disableSubscriptions": false,
"subCheckPeriodInMillis": 100,
"showMap": false,
"security": "Basic128Rsa15",
"identity": {
"type": "anonymous"
},
"mapping": [
{
"deviceNodePattern": "Root\\.Objects\\.Device1",
"deviceNamePattern": "Device ${Root\\.Objects\\.Device1\\.serialNumber}",
"attributes": [
{
"key": "temperature °C",
"path": "${ns=2;i=5}"
}
],
"timeseries": [
{
"key": "humidity",
"path": "${Root\\.Objects\\.Device1\\.TemperatureAndHumiditySensor\\.Humidity}"
},
{
"key": "batteryLevel",
"path": "${Battery\\.batteryLevel}"
}
],
"rpc_methods": [
{
"method": "multiply",
"arguments": [
2,
4
]
}
],
"attributes_updates": [
{
"attributeOnThingsBoard": "deviceName",
"attributeOnDevice": "Root\\.Objects\\.Device1\\.serialNumber"
}
]
}
]
}
}

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

Loading…
Cancel
Save