Browse Source

Megre with develop/3.6 to include flaky test fixes

pull/9155/head
Dmytro Skarzhynets 3 years ago
parent
commit
0cd6edfaae
  1. 267
      application/src/main/data/json/demo/dashboards/firmware.json
  2. 234
      application/src/main/data/json/demo/dashboards/gateways.json
  3. 82
      application/src/main/data/json/demo/dashboards/rule_engine_statistics.json
  4. 267
      application/src/main/data/json/demo/dashboards/software.json
  5. 229
      application/src/main/data/json/demo/dashboards/thermostats.json
  6. 2
      application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json
  7. 2
      application/src/main/data/json/tenant/device_profile/rule_chain_template.json
  8. 2
      application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
  9. 49
      application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
  10. 8
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  11. 54
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  12. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java
  13. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java
  14. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java
  15. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java
  16. 21
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  17. 68
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  18. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
  19. 4
      application/src/main/resources/thingsboard.yml
  20. 4
      application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java
  21. 144
      application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerWithDefaultPortTest.java
  22. 306
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java
  23. 92
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AbstractAssetProcessorTest.java
  24. 44
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorTest.java
  25. 43
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessorTest.java
  26. 100
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java
  27. 43
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessorTest.java
  28. 45
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessorTest.java
  29. 119
      application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java
  30. 23
      common/cluster-api/src/main/proto/queue.proto
  31. 9
      common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java
  32. 6
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java
  33. 4
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/importing/csv/BulkImportColumnType.java
  34. 18
      common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpCommunicationSpec.java
  35. 3
      common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpMethod.java
  36. 3
      common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java
  37. 29
      common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/ToServerRpcRequestSnmpCommunicationConfig.java
  38. 32
      common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java
  39. 2
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java
  40. 1
      common/edge-api/src/main/proto/edge.proto
  41. 50
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DateFormattingOptions.java
  42. 84
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java
  43. 182
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java
  44. 74
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java
  45. 97
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/PduService.java
  46. 221
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java
  47. 31
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java
  48. 23
      common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpDeviceSimulatorV2.java
  49. 43
      common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpTestV2.java
  50. 7
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
  51. 49
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  52. 7
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java
  53. 23
      dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java
  54. 3
      dao/src/test/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidatorTest.java
  55. 2
      docker/docker-compose.yml
  56. 1
      docker/tb-snmp-transport.env
  57. 4
      transport/snmp/src/main/resources/tb-snmp-transport.yml
  58. 13
      ui-ngx/src/app/core/translate/missing-translate-handler.ts
  59. 8
      ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts
  60. 4
      ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts
  61. 330
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html
  62. 72
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss
  63. 66
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts
  64. 31
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html
  65. 18
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts
  66. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html
  67. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts
  68. 2
      ui-ngx/src/app/shared/components/color-input.component.html
  69. 83
      ui-ngx/src/app/shared/components/color-input.component.ts
  70. 12
      ui-ngx/src/app/shared/models/device.models.ts
  71. 848
      ui-ngx/src/assets/dashboard/api_usage.json
  72. 45
      ui-ngx/src/assets/dashboard/customer_user_home_page.json
  73. 250
      ui-ngx/src/assets/dashboard/sys_admin_home_page.json
  74. 144
      ui-ngx/src/assets/dashboard/tenant_admin_home_page.json
  75. 15
      ui-ngx/src/assets/locale/locale.constant-en_US.json

267
application/src/main/data/json/demo/dashboards/firmware.json

@ -1,24 +1,34 @@
{ {
"title": "Firmware", "title": "Firmware",
"image": null, "image": null,
"mobileHide": false,
"mobileOrder": null,
"configuration": { "configuration": {
"description": "", "description": "",
"widgets": { "widgets": {
"cd03188e-cd9d-9601-fd57-da4cb95fc016": { "cd03188e-cd9d-9601-fd57-da4cb95fc016": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 6.5, "sizeY": 6.5,
"config": { "config": {
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000, "interval": 1000,
"timewindowMs": 86400000 "timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
}, },
"aggregation": { "aggregation": {
"type": "NONE", "type": "NONE",
@ -299,16 +309,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "cd03188e-cd9d-9601-fd57-da4cb95fc016" "id": "cd03188e-cd9d-9601-fd57-da4cb95fc016",
"typeFullFqn": "system.cards.entities_table"
}, },
"100b756c-0082-6505-3ae1-3603e6deea48": { "100b756c-0082-6505-3ae1-3603e6deea48": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "timeseries_table",
"type": "timeseries", "type": "timeseries",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 8, "sizeX": 8,
"sizeY": 6.5, "sizeY": 6.5,
"config": { "config": {
@ -461,14 +466,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "100b756c-0082-6505-3ae1-3603e6deea48" "id": "100b756c-0082-6505-3ae1-3603e6deea48",
"typeFullFqn": "system.cards.timeseries_table"
}, },
"17543c57-af4a-2c1e-bf12-53a7b46791e6": { "17543c57-af4a-2c1e-bf12-53a7b46791e6": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "html_value_card",
"type": "latest", "type": "latest",
"title": "New widget",
"sizeX": 8, "sizeX": 8,
"sizeY": 3, "sizeY": 3,
"config": { "config": {
@ -496,8 +498,27 @@
} }
], ],
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"timewindowMs": 60000 "realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
} }
}, },
"showTitle": false, "showTitle": false,
@ -545,14 +566,11 @@
"enableDataExport": false, "enableDataExport": false,
"displayTimewindow": true "displayTimewindow": true
}, },
"id": "17543c57-af4a-2c1e-bf12-53a7b46791e6" "id": "17543c57-af4a-2c1e-bf12-53a7b46791e6",
"typeFullFqn": "system.cards.html_value_card"
}, },
"6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6": { "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "html_value_card",
"type": "latest", "type": "latest",
"title": "New widget",
"sizeX": 8, "sizeX": 8,
"sizeY": 3, "sizeY": 3,
"config": { "config": {
@ -580,8 +598,27 @@
} }
], ],
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"timewindowMs": 60000 "realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
} }
}, },
"showTitle": false, "showTitle": false,
@ -629,14 +666,11 @@
"enableDataExport": false, "enableDataExport": false,
"displayTimewindow": true "displayTimewindow": true
}, },
"id": "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6" "id": "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6",
"typeFullFqn": "system.cards.html_value_card"
}, },
"e6674227-9cf3-a2f6-ecac-5ccfc38a3c81": { "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "html_value_card",
"type": "latest", "type": "latest",
"title": "New widget",
"sizeX": 8, "sizeX": 8,
"sizeY": 3, "sizeY": 3,
"config": { "config": {
@ -664,8 +698,27 @@
} }
], ],
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"timewindowMs": 60000 "realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
} }
}, },
"showTitle": false, "showTitle": false,
@ -713,14 +766,11 @@
"enableDataExport": false, "enableDataExport": false,
"displayTimewindow": true "displayTimewindow": true
}, },
"id": "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81" "id": "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81",
"typeFullFqn": "system.cards.html_value_card"
}, },
"77b10144-b904-edd5-8c7c-8fb75616c6d8": { "77b10144-b904-edd5-8c7c-8fb75616c6d8": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "html_value_card",
"type": "latest", "type": "latest",
"title": "New widget",
"sizeX": 8, "sizeX": 8,
"sizeY": 3, "sizeY": 3,
"config": { "config": {
@ -748,8 +798,27 @@
} }
], ],
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"timewindowMs": 60000 "realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
} }
}, },
"showTitle": false, "showTitle": false,
@ -797,23 +866,32 @@
"enableDataExport": false, "enableDataExport": false,
"displayTimewindow": true "displayTimewindow": true
}, },
"id": "77b10144-b904-edd5-8c7c-8fb75616c6d8" "id": "77b10144-b904-edd5-8c7c-8fb75616c6d8",
"typeFullFqn": "system.cards.html_value_card"
}, },
"21be08bb-ec90-f760-ad6f-e7678f12c401": { "21be08bb-ec90-f760-ad6f-e7678f12c401": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 6.5, "sizeY": 6.5,
"config": { "config": {
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"realtimeType": 1,
"interval": 1000, "interval": 1000,
"timewindowMs": 86400000 "timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
}, },
"aggregation": { "aggregation": {
"type": "NONE", "type": "NONE",
@ -1094,23 +1172,32 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "21be08bb-ec90-f760-ad6f-e7678f12c401" "id": "21be08bb-ec90-f760-ad6f-e7678f12c401",
"typeFullFqn": "system.cards.entities_table"
}, },
"e8280043-d3dc-7acb-c2ff-a4522972ff91": { "e8280043-d3dc-7acb-c2ff-a4522972ff91": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 6.5, "sizeY": 6.5,
"config": { "config": {
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000, "interval": 1000,
"timewindowMs": 86400000 "timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
}, },
"aggregation": { "aggregation": {
"type": "NONE", "type": "NONE",
@ -1391,23 +1478,32 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "e8280043-d3dc-7acb-c2ff-a4522972ff91" "id": "e8280043-d3dc-7acb-c2ff-a4522972ff91",
"typeFullFqn": "system.cards.entities_table"
}, },
"3624013b-378c-f110-5eba-ae95c25a4dcc": { "3624013b-378c-f110-5eba-ae95c25a4dcc": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 6.5, "sizeY": 6.5,
"config": { "config": {
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000, "interval": 1000,
"timewindowMs": 86400000 "timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
}, },
"aggregation": { "aggregation": {
"type": "NONE", "type": "NONE",
@ -1688,23 +1784,32 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "3624013b-378c-f110-5eba-ae95c25a4dcc" "id": "3624013b-378c-f110-5eba-ae95c25a4dcc",
"typeFullFqn": "system.cards.entities_table"
}, },
"d2d13e0d-4e71-889f-9343-ad2f0af9f176": { "d2d13e0d-4e71-889f-9343-ad2f0af9f176": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 6.5, "sizeY": 6.5,
"config": { "config": {
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"realtimeType": 1,
"interval": 1000, "interval": 1000,
"timewindowMs": 86400000 "timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
}, },
"aggregation": { "aggregation": {
"type": "NONE", "type": "NONE",
@ -1985,7 +2090,8 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "d2d13e0d-4e71-889f-9343-ad2f0af9f176" "id": "d2d13e0d-4e71-889f-9343-ad2f0af9f176",
"typeFullFqn": "system.cards.entities_table"
} }
}, },
"states": { "states": {
@ -2035,7 +2141,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"backgroundImageUrl": null, "backgroundImageUrl": null,
"mobileAutoFillHeight": true, "mobileAutoFillHeight": true,
"mobileRowHeight": 70 "mobileRowHeight": 70,
"outerMargin": true
} }
} }
} }
@ -2062,7 +2169,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"backgroundImageUrl": null, "backgroundImageUrl": null,
"mobileAutoFillHeight": false, "mobileAutoFillHeight": false,
"mobileRowHeight": 70 "mobileRowHeight": 70,
"outerMargin": true
} }
} }
} }
@ -2089,7 +2197,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"backgroundImageUrl": null, "backgroundImageUrl": null,
"mobileAutoFillHeight": false, "mobileAutoFillHeight": false,
"mobileRowHeight": 70 "mobileRowHeight": 70,
"outerMargin": true
} }
} }
} }
@ -2116,7 +2225,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"backgroundImageUrl": null, "backgroundImageUrl": null,
"mobileAutoFillHeight": false, "mobileAutoFillHeight": false,
"mobileRowHeight": 70 "mobileRowHeight": 70,
"outerMargin": true
} }
} }
} }
@ -2143,7 +2253,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"backgroundImageUrl": null, "backgroundImageUrl": null,
"mobileAutoFillHeight": false, "mobileAutoFillHeight": false,
"mobileRowHeight": 70 "mobileRowHeight": 70,
"outerMargin": true
} }
} }
} }
@ -2170,7 +2281,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"backgroundImageUrl": null, "backgroundImageUrl": null,
"mobileAutoFillHeight": false, "mobileAutoFillHeight": false,
"mobileRowHeight": 70 "mobileRowHeight": 70,
"outerMargin": true
} }
} }
} }
@ -2488,5 +2600,6 @@
"showUpdateDashboardImage": false "showUpdateDashboardImage": false
} }
}, },
"externalId": null,
"name": "Firmware" "name": "Firmware"
} }

234
application/src/main/data/json/demo/dashboards/gateways.json

@ -1,20 +1,33 @@
{ {
"title": "Gateways", "title": "Gateways",
"image": null,
"mobileHide": false,
"mobileOrder": null,
"configuration": { "configuration": {
"widgets": { "widgets": {
"94715984-ae74-76e4-20b7-2f956b01ed80": { "94715984-ae74-76e4-20b7-2f956b01ed80": {
"isSystemType": true,
"bundleAlias": "entity_admin_widgets",
"typeAlias": "device_admin_table",
"type": "latest", "type": "latest",
"title": "New widget",
"sizeX": 24, "sizeX": 24,
"sizeY": 12, "sizeY": 12,
"config": { "config": {
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"realtimeType": 1,
"interval": 1000, "interval": 1000,
"timewindowMs": 86400000 "timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085270425,
"endTimeMs": 1694171670425
},
"quickInterval": "CURRENT_DAY"
}, },
"aggregation": { "aggregation": {
"type": "NONE", "type": "NONE",
@ -187,14 +200,11 @@
] ]
} }
}, },
"id": "94715984-ae74-76e4-20b7-2f956b01ed80" "id": "94715984-ae74-76e4-20b7-2f956b01ed80",
"typeFullFqn": "system.entity_admin_widgets.device_admin_table"
}, },
"eadabbc7-519e-76fc-ba10-b3fe8c18da10": { "eadabbc7-519e-76fc-ba10-b3fe8c18da10": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "timeseries_table",
"type": "timeseries", "type": "timeseries",
"title": "New widget",
"sizeX": 14, "sizeX": 14,
"sizeY": 13, "sizeY": 13,
"config": { "config": {
@ -255,14 +265,11 @@
"titleTooltip": "", "titleTooltip": "",
"displayTimewindow": true "displayTimewindow": true
}, },
"id": "eadabbc7-519e-76fc-ba10-b3fe8c18da10" "id": "eadabbc7-519e-76fc-ba10-b3fe8c18da10",
"typeFullFqn": "system.cards.timeseries_table"
}, },
"f928afc4-30d1-8d0c-e3cf-777f9f9d1155": { "f928afc4-30d1-8d0c-e3cf-777f9f9d1155": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries", "type": "timeseries",
"title": "New widget",
"sizeX": 17, "sizeX": 17,
"sizeY": 4, "sizeY": 4,
"config": { "config": {
@ -375,6 +382,15 @@
"xaxisSecond": { "xaxisSecond": {
"axisPosition": "top", "axisPosition": "top",
"showLabels": true "showLabels": true
},
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "right",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": true
} }
}, },
"title": "Real time information", "title": "Real time information",
@ -393,25 +409,13 @@
"widgetStyle": {}, "widgetStyle": {},
"useDashboardTimewindow": false, "useDashboardTimewindow": false,
"displayTimewindow": true, "displayTimewindow": true,
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "right",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": true
},
"actions": {} "actions": {}
}, },
"id": "f928afc4-30d1-8d0c-e3cf-777f9f9d1155" "id": "f928afc4-30d1-8d0c-e3cf-777f9f9d1155",
"typeFullFqn": "system.charts.basic_timeseries"
}, },
"2a95b473-042d-59d0-2da2-40d0cccb6c8a": { "2a95b473-042d-59d0-2da2-40d0cccb6c8a": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "timeseries_table",
"type": "timeseries", "type": "timeseries",
"title": "New widget",
"sizeX": 7, "sizeX": 7,
"sizeY": 7, "sizeY": 7,
"config": { "config": {
@ -491,14 +495,11 @@
"showTotal": false "showTotal": false
} }
}, },
"id": "2a95b473-042d-59d0-2da2-40d0cccb6c8a" "id": "2a95b473-042d-59d0-2da2-40d0cccb6c8a",
"typeFullFqn": "system.cards.timeseries_table"
}, },
"aaa69366-aacc-9028-65aa-645c0f8533ec": { "aaa69366-aacc-9028-65aa-645c0f8533ec": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries", "type": "timeseries",
"title": "New widget",
"sizeX": 17, "sizeX": 17,
"sizeY": 4, "sizeY": 4,
"config": { "config": {
@ -604,6 +605,15 @@
"xaxisSecond": { "xaxisSecond": {
"axisPosition": "top", "axisPosition": "top",
"showLabels": true "showLabels": true
},
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "right",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": true
} }
}, },
"title": "History information", "title": "History information",
@ -622,25 +632,13 @@
"widgetStyle": {}, "widgetStyle": {},
"useDashboardTimewindow": true, "useDashboardTimewindow": true,
"displayTimewindow": true, "displayTimewindow": true,
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "right",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": true
},
"actions": {} "actions": {}
}, },
"id": "aaa69366-aacc-9028-65aa-645c0f8533ec" "id": "aaa69366-aacc-9028-65aa-645c0f8533ec",
"typeFullFqn": "system.charts.basic_timeseries"
}, },
"ce5c7d01-a3ef-5cf0-4578-8505135c23a0": { "ce5c7d01-a3ef-5cf0-4578-8505135c23a0": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries", "type": "timeseries",
"title": "New widget",
"sizeX": 17, "sizeX": 17,
"sizeY": 4, "sizeY": 4,
"config": { "config": {
@ -751,6 +749,15 @@
"xaxisSecond": { "xaxisSecond": {
"axisPosition": "top", "axisPosition": "top",
"showLabels": true "showLabels": true
},
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "right",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": true
} }
}, },
"title": "Real time information", "title": "Real time information",
@ -769,25 +776,13 @@
"widgetStyle": {}, "widgetStyle": {},
"useDashboardTimewindow": false, "useDashboardTimewindow": false,
"displayTimewindow": true, "displayTimewindow": true,
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "right",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": true
},
"actions": {} "actions": {}
}, },
"id": "ce5c7d01-a3ef-5cf0-4578-8505135c23a0" "id": "ce5c7d01-a3ef-5cf0-4578-8505135c23a0",
"typeFullFqn": "system.charts.basic_timeseries"
}, },
"466f046d-6005-a168-b107-60fcb2469cd5": { "466f046d-6005-a168-b107-60fcb2469cd5": {
"isSystemType": true,
"bundleAlias": "gateway_widgets",
"typeAlias": "attributes_card",
"type": "latest", "type": "latest",
"title": "New widget",
"sizeX": 7, "sizeX": 7,
"sizeY": 5, "sizeY": 5,
"config": { "config": {
@ -799,8 +794,27 @@
} }
], ],
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"timewindowMs": 60000 "realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085270425,
"endTimeMs": 1694171670425
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
} }
}, },
"showTitle": true, "showTitle": true,
@ -832,14 +846,11 @@
"showLegend": false, "showLegend": false,
"actions": {} "actions": {}
}, },
"id": "466f046d-6005-a168-b107-60fcb2469cd5" "id": "466f046d-6005-a168-b107-60fcb2469cd5",
"typeFullFqn": "system.gateway_widgets.attributes_card"
}, },
"8fc32225-164f-3258-73f7-e6b6d959cf0b": { "8fc32225-164f-3258-73f7-e6b6d959cf0b": {
"isSystemType": true,
"bundleAlias": "gateway_widgets",
"typeAlias": "config_form_latest",
"type": "latest", "type": "latest",
"title": "New widget",
"sizeX": 10, "sizeX": 10,
"sizeY": 9, "sizeY": 9,
"config": { "config": {
@ -851,8 +862,27 @@
} }
], ],
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"timewindowMs": 60000 "realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085270425,
"endTimeMs": 1694171670425
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
} }
}, },
"showTitle": true, "showTitle": true,
@ -881,14 +911,11 @@
"showLegend": false, "showLegend": false,
"actions": {} "actions": {}
}, },
"id": "8fc32225-164f-3258-73f7-e6b6d959cf0b" "id": "8fc32225-164f-3258-73f7-e6b6d959cf0b",
"typeFullFqn": "system.gateway_widgets.config_form_latest"
}, },
"063fc179-c9fd-f952-e714-f24e9c43c05c": { "063fc179-c9fd-f952-e714-f24e9c43c05c": {
"isSystemType": true,
"bundleAlias": "control_widgets",
"typeAlias": "rpcbutton",
"type": "rpc", "type": "rpc",
"title": "New widget",
"sizeX": 4, "sizeX": 4,
"sizeY": 2, "sizeY": 2,
"config": { "config": {
@ -930,14 +957,11 @@
"b2487e75-2fa4-f211-142c-434dfd50c70c" "b2487e75-2fa4-f211-142c-434dfd50c70c"
] ]
}, },
"id": "063fc179-c9fd-f952-e714-f24e9c43c05c" "id": "063fc179-c9fd-f952-e714-f24e9c43c05c",
"typeFullFqn": "system.control_widgets.rpcbutton"
}, },
"3c2134cc-27a0-93e1-dbe1-2fa7c1ce16b7": { "3c2134cc-27a0-93e1-dbe1-2fa7c1ce16b7": {
"isSystemType": true,
"bundleAlias": "control_widgets",
"typeAlias": "rpcbutton",
"type": "rpc", "type": "rpc",
"title": "New widget",
"sizeX": 4, "sizeX": 4,
"sizeY": 2, "sizeY": 2,
"config": { "config": {
@ -979,14 +1003,11 @@
"b2487e75-2fa4-f211-142c-434dfd50c70c" "b2487e75-2fa4-f211-142c-434dfd50c70c"
] ]
}, },
"id": "3c2134cc-27a0-93e1-dbe1-2fa7c1ce16b7" "id": "3c2134cc-27a0-93e1-dbe1-2fa7c1ce16b7",
"typeFullFqn": "system.control_widgets.rpcbutton"
}, },
"6770b6ba-eff8-df05-75f8-c1f9326d4842": { "6770b6ba-eff8-df05-75f8-c1f9326d4842": {
"isSystemType": true,
"bundleAlias": "input_widgets",
"typeAlias": "markers_placement_openstreetmap",
"type": "latest", "type": "latest",
"title": "New widget",
"sizeX": 6, "sizeX": 6,
"sizeY": 4, "sizeY": 4,
"config": { "config": {
@ -1015,8 +1036,27 @@
} }
], ],
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"timewindowMs": 60000 "realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085270425,
"endTimeMs": 1694171670425
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
} }
}, },
"showTitle": false, "showTitle": false,
@ -1083,7 +1123,8 @@
"titleTooltip": "", "titleTooltip": "",
"displayTimewindow": true "displayTimewindow": true
}, },
"id": "6770b6ba-eff8-df05-75f8-c1f9326d4842" "id": "6770b6ba-eff8-df05-75f8-c1f9326d4842",
"typeFullFqn": "system.input_widgets.markers_placement_openstreetmap"
} }
}, },
"states": { "states": {
@ -1108,7 +1149,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"mobileAutoFillHeight": false, "mobileAutoFillHeight": false,
"mobileRowHeight": 70, "mobileRowHeight": 70,
"margin": 10 "margin": 10,
"outerMargin": true
} }
} }
} }
@ -1158,7 +1200,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"mobileAutoFillHeight": false, "mobileAutoFillHeight": false,
"mobileRowHeight": 70, "mobileRowHeight": 70,
"margin": 10 "margin": 10,
"outerMargin": true
} }
} }
} }
@ -1211,7 +1254,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"mobileAutoFillHeight": true, "mobileAutoFillHeight": true,
"mobileRowHeight": 70, "mobileRowHeight": 70,
"margin": 10 "margin": 10,
"outerMargin": true
} }
} }
} }
@ -1224,8 +1268,10 @@
"filter": { "filter": {
"type": "deviceType", "type": "deviceType",
"resolveMultiple": true, "resolveMultiple": true,
"deviceType": "gateway", "deviceNameFilter": "",
"deviceNameFilter": "" "deviceTypes": [
"gateway"
]
} }
}, },
"b2487e75-2fa4-f211-142c-434dfd50c70c": { "b2487e75-2fa4-f211-142c-434dfd50c70c": {
@ -1261,7 +1307,9 @@
"showDashboardExport": true, "showDashboardExport": true,
"toolbarAlwaysOpen": true, "toolbarAlwaysOpen": true,
"titleColor": "rgba(0,0,0,0.870588)" "titleColor": "rgba(0,0,0,0.870588)"
} },
"filters": {}
}, },
"externalId": null,
"name": "Gateways" "name": "Gateways"
} }

82
application/src/main/data/json/demo/dashboards/rule_engine_statistics.json

@ -1,13 +1,12 @@
{ {
"title": "Rule Engine Statistics", "title": "Rule Engine Statistics",
"image": null,
"mobileHide": false,
"mobileOrder": null,
"configuration": { "configuration": {
"widgets": { "widgets": {
"81987f19-3eac-e4ce-b790-d96e9b54d9a0": { "81987f19-3eac-e4ce-b790-d96e9b54d9a0": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries", "type": "timeseries",
"title": "New widget",
"sizeX": 12, "sizeX": 12,
"sizeY": 7, "sizeY": 7,
"config": { "config": {
@ -151,6 +150,15 @@
"xaxisSecond": { "xaxisSecond": {
"axisPosition": "top", "axisPosition": "top",
"showLabels": true "showLabels": true
},
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "bottom",
"showMin": true,
"showMax": true,
"showAvg": false,
"showTotal": true
} }
}, },
"title": "Queue Stats", "title": "Queue Stats",
@ -169,25 +177,13 @@
"widgetStyle": {}, "widgetStyle": {},
"useDashboardTimewindow": false, "useDashboardTimewindow": false,
"displayTimewindow": true, "displayTimewindow": true,
"showLegend": true, "actions": {}
"actions": {},
"legendConfig": {
"direction": "column",
"position": "bottom",
"showMin": true,
"showMax": true,
"showAvg": false,
"showTotal": true
}
}, },
"id": "81987f19-3eac-e4ce-b790-d96e9b54d9a0" "id": "81987f19-3eac-e4ce-b790-d96e9b54d9a0",
"typeFullFqn": "system.charts.basic_timeseries"
}, },
"5eb79712-5c24-3060-7e4f-6af36b8f842d": { "5eb79712-5c24-3060-7e4f-6af36b8f842d": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "timeseries_table",
"type": "timeseries", "type": "timeseries",
"title": "New widget",
"sizeX": 24, "sizeX": 24,
"sizeY": 5, "sizeY": 5,
"config": { "config": {
@ -272,14 +268,11 @@
"titleTooltip": "", "titleTooltip": "",
"displayTimewindow": true "displayTimewindow": true
}, },
"id": "5eb79712-5c24-3060-7e4f-6af36b8f842d" "id": "5eb79712-5c24-3060-7e4f-6af36b8f842d",
"typeFullFqn": "system.cards.timeseries_table"
}, },
"ad3f1417-87a8-750e-fc67-49a2de1466d4": { "ad3f1417-87a8-750e-fc67-49a2de1466d4": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries", "type": "timeseries",
"title": "New widget",
"sizeX": 12, "sizeX": 12,
"sizeY": 7, "sizeY": 7,
"config": { "config": {
@ -393,6 +386,15 @@
"xaxisSecond": { "xaxisSecond": {
"axisPosition": "top", "axisPosition": "top",
"showLabels": true "showLabels": true
},
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "bottom",
"showMin": true,
"showMax": true,
"showAvg": false,
"showTotal": true
} }
}, },
"title": "Processing Failures and Timeouts", "title": "Processing Failures and Timeouts",
@ -411,18 +413,10 @@
"widgetStyle": {}, "widgetStyle": {},
"useDashboardTimewindow": false, "useDashboardTimewindow": false,
"displayTimewindow": true, "displayTimewindow": true,
"showLegend": true, "actions": {}
"actions": {},
"legendConfig": {
"direction": "column",
"position": "bottom",
"showMin": true,
"showMax": true,
"showAvg": false,
"showTotal": true
}
}, },
"id": "ad3f1417-87a8-750e-fc67-49a2de1466d4" "id": "ad3f1417-87a8-750e-fc67-49a2de1466d4",
"typeFullFqn": "system.charts.basic_timeseries"
} }
}, },
"states": { "states": {
@ -457,14 +451,12 @@
"backgroundColor": "#eeeeee", "backgroundColor": "#eeeeee",
"color": "rgba(0,0,0,0.870588)", "color": "rgba(0,0,0,0.870588)",
"columns": 24, "columns": 24,
"margins": [
10,
10
],
"backgroundSizeMode": "100%", "backgroundSizeMode": "100%",
"autoFillHeight": true, "autoFillHeight": true,
"mobileAutoFillHeight": false, "mobileAutoFillHeight": false,
"mobileRowHeight": 70 "mobileRowHeight": 70,
"margin": 10,
"outerMargin": true
} }
} }
} }
@ -477,8 +469,10 @@
"filter": { "filter": {
"type": "assetType", "type": "assetType",
"resolveMultiple": true, "resolveMultiple": true,
"assetType": "TbServiceQueue", "assetNameFilter": "",
"assetNameFilter": "" "assetTypes": [
"TbServiceQueue"
]
} }
} }
}, },
@ -514,7 +508,9 @@
"showDashboardTimewindow": true, "showDashboardTimewindow": true,
"showDashboardExport": true, "showDashboardExport": true,
"toolbarAlwaysOpen": true "toolbarAlwaysOpen": true
} },
"filters": {}
}, },
"externalId": null,
"name": "Rule Engine Statistics" "name": "Rule Engine Statistics"
} }

267
application/src/main/data/json/demo/dashboards/software.json

@ -1,24 +1,34 @@
{ {
"title": "Software", "title": "Software",
"image": null, "image": null,
"mobileHide": false,
"mobileOrder": null,
"configuration": { "configuration": {
"description": "", "description": "",
"widgets": { "widgets": {
"cd03188e-cd9d-9601-fd57-da4cb95fc016": { "cd03188e-cd9d-9601-fd57-da4cb95fc016": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 6.5, "sizeY": 6.5,
"config": { "config": {
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000, "interval": 1000,
"timewindowMs": 86400000 "timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343279,
"endTimeMs": 1694171743279
},
"quickInterval": "CURRENT_DAY"
}, },
"aggregation": { "aggregation": {
"type": "NONE", "type": "NONE",
@ -299,16 +309,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "cd03188e-cd9d-9601-fd57-da4cb95fc016" "id": "cd03188e-cd9d-9601-fd57-da4cb95fc016",
"typeFullFqn": "system.cards.entities_table"
}, },
"100b756c-0082-6505-3ae1-3603e6deea48": { "100b756c-0082-6505-3ae1-3603e6deea48": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "timeseries_table",
"type": "timeseries", "type": "timeseries",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 8, "sizeX": 8,
"sizeY": 6.5, "sizeY": 6.5,
"config": { "config": {
@ -461,14 +466,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "100b756c-0082-6505-3ae1-3603e6deea48" "id": "100b756c-0082-6505-3ae1-3603e6deea48",
"typeFullFqn": "system.cards.timeseries_table"
}, },
"17543c57-af4a-2c1e-bf12-53a7b46791e6": { "17543c57-af4a-2c1e-bf12-53a7b46791e6": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "html_value_card",
"type": "latest", "type": "latest",
"title": "New widget",
"sizeX": 8, "sizeX": 8,
"sizeY": 3, "sizeY": 3,
"config": { "config": {
@ -496,8 +498,27 @@
} }
], ],
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"timewindowMs": 60000 "realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343280,
"endTimeMs": 1694171743280
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
} }
}, },
"showTitle": false, "showTitle": false,
@ -545,14 +566,11 @@
"enableDataExport": false, "enableDataExport": false,
"displayTimewindow": true "displayTimewindow": true
}, },
"id": "17543c57-af4a-2c1e-bf12-53a7b46791e6" "id": "17543c57-af4a-2c1e-bf12-53a7b46791e6",
"typeFullFqn": "system.cards.html_value_card"
}, },
"6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6": { "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "html_value_card",
"type": "latest", "type": "latest",
"title": "New widget",
"sizeX": 8, "sizeX": 8,
"sizeY": 3, "sizeY": 3,
"config": { "config": {
@ -580,8 +598,27 @@
} }
], ],
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"timewindowMs": 60000 "realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343280,
"endTimeMs": 1694171743280
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
} }
}, },
"showTitle": false, "showTitle": false,
@ -629,14 +666,11 @@
"enableDataExport": false, "enableDataExport": false,
"displayTimewindow": true "displayTimewindow": true
}, },
"id": "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6" "id": "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6",
"typeFullFqn": "system.cards.html_value_card"
}, },
"e6674227-9cf3-a2f6-ecac-5ccfc38a3c81": { "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "html_value_card",
"type": "latest", "type": "latest",
"title": "New widget",
"sizeX": 8, "sizeX": 8,
"sizeY": 3, "sizeY": 3,
"config": { "config": {
@ -664,8 +698,27 @@
} }
], ],
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"timewindowMs": 60000 "realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343280,
"endTimeMs": 1694171743280
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
} }
}, },
"showTitle": false, "showTitle": false,
@ -713,14 +766,11 @@
"enableDataExport": false, "enableDataExport": false,
"displayTimewindow": true "displayTimewindow": true
}, },
"id": "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81" "id": "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81",
"typeFullFqn": "system.cards.html_value_card"
}, },
"77b10144-b904-edd5-8c7c-8fb75616c6d8": { "77b10144-b904-edd5-8c7c-8fb75616c6d8": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "html_value_card",
"type": "latest", "type": "latest",
"title": "New widget",
"sizeX": 8, "sizeX": 8,
"sizeY": 3, "sizeY": 3,
"config": { "config": {
@ -748,8 +798,27 @@
} }
], ],
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"timewindowMs": 60000 "realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343280,
"endTimeMs": 1694171743280
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
} }
}, },
"showTitle": false, "showTitle": false,
@ -797,23 +866,32 @@
"enableDataExport": false, "enableDataExport": false,
"displayTimewindow": true "displayTimewindow": true
}, },
"id": "77b10144-b904-edd5-8c7c-8fb75616c6d8" "id": "77b10144-b904-edd5-8c7c-8fb75616c6d8",
"typeFullFqn": "system.cards.html_value_card"
}, },
"21be08bb-ec90-f760-ad6f-e7678f12c401": { "21be08bb-ec90-f760-ad6f-e7678f12c401": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 6.5, "sizeY": 6.5,
"config": { "config": {
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"realtimeType": 1,
"interval": 1000, "interval": 1000,
"timewindowMs": 86400000 "timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343280,
"endTimeMs": 1694171743280
},
"quickInterval": "CURRENT_DAY"
}, },
"aggregation": { "aggregation": {
"type": "NONE", "type": "NONE",
@ -1094,23 +1172,32 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "21be08bb-ec90-f760-ad6f-e7678f12c401" "id": "21be08bb-ec90-f760-ad6f-e7678f12c401",
"typeFullFqn": "system.cards.entities_table"
}, },
"e8280043-d3dc-7acb-c2ff-a4522972ff91": { "e8280043-d3dc-7acb-c2ff-a4522972ff91": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 6.5, "sizeY": 6.5,
"config": { "config": {
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000, "interval": 1000,
"timewindowMs": 86400000 "timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343280,
"endTimeMs": 1694171743280
},
"quickInterval": "CURRENT_DAY"
}, },
"aggregation": { "aggregation": {
"type": "NONE", "type": "NONE",
@ -1391,23 +1478,32 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "e8280043-d3dc-7acb-c2ff-a4522972ff91" "id": "e8280043-d3dc-7acb-c2ff-a4522972ff91",
"typeFullFqn": "system.cards.entities_table"
}, },
"3624013b-378c-f110-5eba-ae95c25a4dcc": { "3624013b-378c-f110-5eba-ae95c25a4dcc": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 6.5, "sizeY": 6.5,
"config": { "config": {
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000, "interval": 1000,
"timewindowMs": 86400000 "timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343280,
"endTimeMs": 1694171743280
},
"quickInterval": "CURRENT_DAY"
}, },
"aggregation": { "aggregation": {
"type": "NONE", "type": "NONE",
@ -1688,23 +1784,32 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "3624013b-378c-f110-5eba-ae95c25a4dcc" "id": "3624013b-378c-f110-5eba-ae95c25a4dcc",
"typeFullFqn": "system.cards.entities_table"
}, },
"d2d13e0d-4e71-889f-9343-ad2f0af9f176": { "d2d13e0d-4e71-889f-9343-ad2f0af9f176": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 6.5, "sizeY": 6.5,
"config": { "config": {
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"realtimeType": 1,
"interval": 1000, "interval": 1000,
"timewindowMs": 86400000 "timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343280,
"endTimeMs": 1694171743280
},
"quickInterval": "CURRENT_DAY"
}, },
"aggregation": { "aggregation": {
"type": "NONE", "type": "NONE",
@ -1985,7 +2090,8 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "d2d13e0d-4e71-889f-9343-ad2f0af9f176" "id": "d2d13e0d-4e71-889f-9343-ad2f0af9f176",
"typeFullFqn": "system.cards.entities_table"
} }
}, },
"states": { "states": {
@ -2035,7 +2141,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"backgroundImageUrl": null, "backgroundImageUrl": null,
"mobileAutoFillHeight": true, "mobileAutoFillHeight": true,
"mobileRowHeight": 70 "mobileRowHeight": 70,
"outerMargin": true
} }
} }
} }
@ -2062,7 +2169,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"backgroundImageUrl": null, "backgroundImageUrl": null,
"mobileAutoFillHeight": false, "mobileAutoFillHeight": false,
"mobileRowHeight": 70 "mobileRowHeight": 70,
"outerMargin": true
} }
} }
} }
@ -2089,7 +2197,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"backgroundImageUrl": null, "backgroundImageUrl": null,
"mobileAutoFillHeight": false, "mobileAutoFillHeight": false,
"mobileRowHeight": 70 "mobileRowHeight": 70,
"outerMargin": true
} }
} }
} }
@ -2116,7 +2225,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"backgroundImageUrl": null, "backgroundImageUrl": null,
"mobileAutoFillHeight": false, "mobileAutoFillHeight": false,
"mobileRowHeight": 70 "mobileRowHeight": 70,
"outerMargin": true
} }
} }
} }
@ -2143,7 +2253,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"backgroundImageUrl": null, "backgroundImageUrl": null,
"mobileAutoFillHeight": false, "mobileAutoFillHeight": false,
"mobileRowHeight": 70 "mobileRowHeight": 70,
"outerMargin": true
} }
} }
} }
@ -2170,7 +2281,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"backgroundImageUrl": null, "backgroundImageUrl": null,
"mobileAutoFillHeight": false, "mobileAutoFillHeight": false,
"mobileRowHeight": 70 "mobileRowHeight": 70,
"outerMargin": true
} }
} }
} }
@ -2488,5 +2600,6 @@
"showUpdateDashboardImage": false "showUpdateDashboardImage": false
} }
}, },
"externalId": null,
"name": "Software" "name": "Software"
} }

229
application/src/main/data/json/demo/dashboards/thermostats.json

@ -1,20 +1,33 @@
{ {
"title": "Thermostats", "title": "Thermostats",
"image": null,
"mobileHide": false,
"mobileOrder": null,
"configuration": { "configuration": {
"widgets": { "widgets": {
"f33c746c-0dfc-c212-395b-b448c8a17209": { "f33c746c-0dfc-c212-395b-b448c8a17209": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest", "type": "latest",
"title": "New widget",
"sizeX": 11, "sizeX": 11,
"sizeY": 11, "sizeY": 11,
"config": { "config": {
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"realtimeType": 1,
"interval": 1000, "interval": 1000,
"timewindowMs": 86400000 "timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085177686,
"endTimeMs": 1694171577686
},
"quickInterval": "CURRENT_DAY"
}, },
"aggregation": { "aggregation": {
"type": "NONE", "type": "NONE",
@ -184,14 +197,11 @@
"rowClick": [] "rowClick": []
} }
}, },
"id": "f33c746c-0dfc-c212-395b-b448c8a17209" "id": "f33c746c-0dfc-c212-395b-b448c8a17209",
"typeFullFqn": "system.cards.entities_table"
}, },
"7943196b-eedb-d422-f9c3-b32d379ad172": { "7943196b-eedb-d422-f9c3-b32d379ad172": {
"isSystemType": true,
"bundleAlias": "alarm_widgets",
"typeAlias": "alarms_table",
"type": "alarm", "type": "alarm",
"title": "New widget",
"sizeX": 13, "sizeX": 13,
"sizeY": 5, "sizeY": 5,
"config": { "config": {
@ -230,7 +240,7 @@
"titleStyle": { "titleStyle": {
"fontSize": "16px", "fontSize": "16px",
"fontWeight": 400, "fontWeight": 400,
"padding": "5px 10px 5px 10px" "padding": "5px 10px 5px 2px"
}, },
"useDashboardTimewindow": false, "useDashboardTimewindow": false,
"showLegend": false, "showLegend": false,
@ -290,7 +300,6 @@
} }
] ]
}, },
"alarmSearchStatus": "ANY",
"alarmsPollingInterval": 5, "alarmsPollingInterval": 5,
"showTitleIcon": false, "showTitleIcon": false,
"titleIcon": null, "titleIcon": null,
@ -302,16 +311,22 @@
"actions": {}, "actions": {},
"datasources": [], "datasources": [],
"alarmsMaxCountLoad": 0, "alarmsMaxCountLoad": 0,
"alarmsFetchSize": 100 "alarmsFetchSize": 100,
"alarmFilterConfig": {
"statusList": [],
"severityList": [],
"typeList": [],
"searchPropagatedAlarms": true
},
"widgetCss": "",
"pageSize": 1024,
"noDataDisplayMessage": ""
}, },
"id": "7943196b-eedb-d422-f9c3-b32d379ad172" "id": "7943196b-eedb-d422-f9c3-b32d379ad172",
"typeFullFqn": "system.alarm_widgets.alarms_table"
}, },
"14a19183-f0b2-d6be-0f62-9863f0a51111": { "14a19183-f0b2-d6be-0f62-9863f0a51111": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries", "type": "timeseries",
"title": "New widget",
"sizeX": 18, "sizeX": 18,
"sizeY": 6, "sizeY": 6,
"config": { "config": {
@ -395,7 +410,16 @@
"axisPosition": "top", "axisPosition": "top",
"showLabels": true "showLabels": true
}, },
"smoothLines": true "smoothLines": true,
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "bottom",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": false
}
}, },
"title": "Temperature", "title": "Temperature",
"dropShadow": true, "dropShadow": true,
@ -413,25 +437,13 @@
"widgetStyle": {}, "widgetStyle": {},
"useDashboardTimewindow": false, "useDashboardTimewindow": false,
"displayTimewindow": true, "displayTimewindow": true,
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "bottom",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": false
},
"actions": {} "actions": {}
}, },
"id": "14a19183-f0b2-d6be-0f62-9863f0a51111" "id": "14a19183-f0b2-d6be-0f62-9863f0a51111",
"typeFullFqn": "system.charts.basic_timeseries"
}, },
"07f49fd5-a73b-d74c-c220-362c20af81f4": { "07f49fd5-a73b-d74c-c220-362c20af81f4": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries", "type": "timeseries",
"title": "New widget",
"sizeX": 18, "sizeX": 18,
"sizeY": 6, "sizeY": 6,
"config": { "config": {
@ -515,7 +527,16 @@
"axisPosition": "top", "axisPosition": "top",
"showLabels": true "showLabels": true
}, },
"smoothLines": true "smoothLines": true,
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "bottom",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": false
}
}, },
"title": "Humidity", "title": "Humidity",
"dropShadow": true, "dropShadow": true,
@ -533,25 +554,13 @@
"widgetStyle": {}, "widgetStyle": {},
"useDashboardTimewindow": false, "useDashboardTimewindow": false,
"displayTimewindow": true, "displayTimewindow": true,
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "bottom",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": false
},
"actions": {} "actions": {}
}, },
"id": "07f49fd5-a73b-d74c-c220-362c20af81f4" "id": "07f49fd5-a73b-d74c-c220-362c20af81f4",
"typeFullFqn": "system.charts.basic_timeseries"
}, },
"c4631f94-2db3-523b-4d09-2a1a0a75d93f": { "c4631f94-2db3-523b-4d09-2a1a0a75d93f": {
"isSystemType": true,
"bundleAlias": "input_widgets",
"typeAlias": "update_multiple_attributes",
"type": "latest", "type": "latest",
"title": "New widget",
"sizeX": 6, "sizeX": 6,
"sizeY": 6, "sizeY": 6,
"config": { "config": {
@ -626,8 +635,27 @@
} }
], ],
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"timewindowMs": 60000 "realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085177686,
"endTimeMs": 1694171577686
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
} }
}, },
"showTitle": true, "showTitle": true,
@ -661,14 +689,11 @@
"titleTooltip": "", "titleTooltip": "",
"displayTimewindow": true "displayTimewindow": true
}, },
"id": "c4631f94-2db3-523b-4d09-2a1a0a75d93f" "id": "c4631f94-2db3-523b-4d09-2a1a0a75d93f",
"typeFullFqn": "system.input_widgets.update_multiple_attributes"
}, },
"3da9a9a1-0b9a-2e1f-0dcb-0ff34a695abb": { "3da9a9a1-0b9a-2e1f-0dcb-0ff34a695abb": {
"isSystemType": true,
"bundleAlias": "maps_v2",
"typeAlias": "openstreetmap",
"type": "latest", "type": "latest",
"title": "New widget",
"sizeX": 13, "sizeX": 13,
"sizeY": 6, "sizeY": 6,
"config": { "config": {
@ -725,8 +750,27 @@
} }
], ],
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"timewindowMs": 60000 "realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085177686,
"endTimeMs": 1694171577686
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
} }
}, },
"showTitle": false, "showTitle": false,
@ -802,14 +846,11 @@
"titleTooltip": "", "titleTooltip": "",
"displayTimewindow": true "displayTimewindow": true
}, },
"id": "3da9a9a1-0b9a-2e1f-0dcb-0ff34a695abb" "id": "3da9a9a1-0b9a-2e1f-0dcb-0ff34a695abb",
"typeFullFqn": "system.maps_v2.openstreetmap"
}, },
"00fb2742-ba1f-7e43-673f-d6c08b72ed06": { "00fb2742-ba1f-7e43-673f-d6c08b72ed06": {
"isSystemType": true,
"bundleAlias": "input_widgets",
"typeAlias": "markers_placement_openstreetmap",
"type": "latest", "type": "latest",
"title": "New widget",
"sizeX": 24, "sizeX": 24,
"sizeY": 12, "sizeY": 12,
"config": { "config": {
@ -864,8 +905,27 @@
} }
], ],
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"timewindowMs": 60000 "realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085177686,
"endTimeMs": 1694171577686
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
} }
}, },
"showTitle": false, "showTitle": false,
@ -937,14 +997,11 @@
"titleTooltip": "", "titleTooltip": "",
"displayTimewindow": true "displayTimewindow": true
}, },
"id": "00fb2742-ba1f-7e43-673f-d6c08b72ed06" "id": "00fb2742-ba1f-7e43-673f-d6c08b72ed06",
"typeFullFqn": "system.input_widgets.markers_placement_openstreetmap"
}, },
"0a430429-9078-9ae6-2b67-e4a15a2bf8bf": { "0a430429-9078-9ae6-2b67-e4a15a2bf8bf": {
"isSystemType": true,
"bundleAlias": "input_widgets",
"typeAlias": "markers_placement_openstreetmap",
"type": "latest", "type": "latest",
"title": "New widget",
"sizeX": 6, "sizeX": 6,
"sizeY": 6, "sizeY": 6,
"config": { "config": {
@ -999,8 +1056,27 @@
} }
], ],
"timewindow": { "timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": { "realtime": {
"timewindowMs": 60000 "realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085177686,
"endTimeMs": 1694171577686
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
} }
}, },
"showTitle": false, "showTitle": false,
@ -1073,7 +1149,8 @@
"titleTooltip": "", "titleTooltip": "",
"displayTimewindow": true "displayTimewindow": true
}, },
"id": "0a430429-9078-9ae6-2b67-e4a15a2bf8bf" "id": "0a430429-9078-9ae6-2b67-e4a15a2bf8bf",
"typeFullFqn": "system.input_widgets.markers_placement_openstreetmap"
} }
}, },
"states": { "states": {
@ -1110,7 +1187,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"mobileAutoFillHeight": false, "mobileAutoFillHeight": false,
"mobileRowHeight": 70, "mobileRowHeight": 70,
"margin": 10 "margin": 10,
"outerMargin": true
} }
} }
} }
@ -1136,7 +1214,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"mobileAutoFillHeight": false, "mobileAutoFillHeight": false,
"mobileRowHeight": 70, "mobileRowHeight": 70,
"margin": 10 "margin": 10,
"outerMargin": true
} }
} }
} }
@ -1182,7 +1261,8 @@
"autoFillHeight": true, "autoFillHeight": true,
"mobileAutoFillHeight": false, "mobileAutoFillHeight": false,
"mobileRowHeight": 70, "mobileRowHeight": 70,
"margin": 10 "margin": 10,
"outerMargin": true
} }
} }
} }
@ -1195,8 +1275,10 @@
"filter": { "filter": {
"type": "deviceType", "type": "deviceType",
"resolveMultiple": true, "resolveMultiple": true,
"deviceType": "thermostat", "deviceNameFilter": "",
"deviceNameFilter": "" "deviceTypes": [
"thermostat"
]
} }
}, },
"12ae98c7-1ea2-52cf-64d5-763e9d993547": { "12ae98c7-1ea2-52cf-64d5-763e9d993547": {
@ -1245,5 +1327,6 @@
}, },
"filters": {} "filters": {}
}, },
"externalId": null,
"name": "Thermostats" "name": "Thermostats"
} }

2
application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json

@ -48,8 +48,8 @@
"type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
"name": "Save Client Attributes", "name": "Save Client Attributes",
"debugMode": false, "debugMode": false,
"configurationVersion": 1,
"configuration": { "configuration": {
"version": 1,
"scope": "CLIENT_SCOPE", "scope": "CLIENT_SCOPE",
"notifyDevice": "false", "notifyDevice": "false",
"sendAttributesUpdatedNotification": "false", "sendAttributesUpdatedNotification": "false",

2
application/src/main/data/json/tenant/device_profile/rule_chain_template.json

@ -32,8 +32,8 @@
"type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
"name": "Save Client Attributes", "name": "Save Client Attributes",
"debugMode": false, "debugMode": false,
"configurationVersion": 1,
"configuration": { "configuration": {
"version": 1,
"scope": "CLIENT_SCOPE", "scope": "CLIENT_SCOPE",
"notifyDevice": "false", "notifyDevice": "false",
"sendAttributesUpdatedNotification": "false", "sendAttributesUpdatedNotification": "false",

2
application/src/main/data/json/tenant/rule_chains/root_rule_chain.json

@ -31,8 +31,8 @@
"type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
"name": "Save Client Attributes", "name": "Save Client Attributes",
"debugMode": false, "debugMode": false,
"configurationVersion": 1,
"configuration": { "configuration": {
"version": 1,
"scope": "CLIENT_SCOPE", "scope": "CLIENT_SCOPE",
"notifyDevice": "false", "notifyDevice": "false",
"sendAttributesUpdatedNotification": "false", "sendAttributesUpdatedNotification": "false",

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

@ -33,7 +33,10 @@ import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; 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.LwM2MClientCredential;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode; 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.PowerMode;
import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
@ -45,6 +48,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumnType; import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumnType;
import org.thingsboard.server.common.data.transport.snmp.SnmpProtocolVersion;
import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
@ -76,18 +80,18 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
private final Lock findOrCreateDeviceProfileLock = new ReentrantLock(); private final Lock findOrCreateDeviceProfileLock = new ReentrantLock();
@Override @Override
protected void setEntityFields(Device entity, Map<BulkImportColumnType, String> fields) { protected void setEntityFields(Device device, Map<BulkImportColumnType, String> fields) {
ObjectNode additionalInfo = getOrCreateAdditionalInfoObj(entity); ObjectNode additionalInfo = getOrCreateAdditionalInfoObj(device);
fields.forEach((columnType, value) -> { fields.forEach((columnType, value) -> {
switch (columnType) { switch (columnType) {
case NAME: case NAME:
entity.setName(value); device.setName(value);
break; break;
case TYPE: case TYPE:
entity.setType(value); device.setType(value);
break; break;
case LABEL: case LABEL:
entity.setLabel(value); device.setLabel(value);
break; break;
case DESCRIPTION: case DESCRIPTION:
additionalInfo.set("description", new TextNode(value)); additionalInfo.set("description", new TextNode(value));
@ -96,16 +100,17 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
additionalInfo.set("gateway", BooleanNode.valueOf(Boolean.parseBoolean(value))); additionalInfo.set("gateway", BooleanNode.valueOf(Boolean.parseBoolean(value)));
break; break;
} }
entity.setAdditionalInfo(additionalInfo); device.setAdditionalInfo(additionalInfo);
}); });
setUpDeviceConfiguration(device, fields);
} }
@Override @Override
@SneakyThrows @SneakyThrows
protected Device saveEntity(SecurityUser user, Device entity, Map<BulkImportColumnType, String> fields) { protected Device saveEntity(SecurityUser user, Device device, Map<BulkImportColumnType, String> fields) {
DeviceCredentials deviceCredentials; DeviceCredentials deviceCredentials;
try { try {
deviceCredentials = createDeviceCredentials(entity.getTenantId(), entity.getId(), fields); deviceCredentials = createDeviceCredentials(device.getTenantId(), device.getId(), fields);
deviceCredentialsService.formatCredentials(deviceCredentials); deviceCredentialsService.formatCredentials(deviceCredentials);
} catch (Exception e) { } catch (Exception e) {
throw new DeviceCredentialsValidationException("Invalid device credentials: " + e.getMessage()); throw new DeviceCredentialsValidationException("Invalid device credentials: " + e.getMessage());
@ -113,15 +118,15 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
DeviceProfile deviceProfile; DeviceProfile deviceProfile;
if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.LWM2M_CREDENTIALS) { if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.LWM2M_CREDENTIALS) {
deviceProfile = setUpLwM2mDeviceProfile(entity.getTenantId(), entity); deviceProfile = setUpLwM2mDeviceProfile(device.getTenantId(), device);
} else if (StringUtils.isNotEmpty(entity.getType())) { } else if (StringUtils.isNotEmpty(device.getType())) {
deviceProfile = deviceProfileService.findOrCreateDeviceProfile(entity.getTenantId(), entity.getType()); deviceProfile = deviceProfileService.findOrCreateDeviceProfile(device.getTenantId(), device.getType());
} else { } else {
deviceProfile = deviceProfileService.findDefaultDeviceProfile(entity.getTenantId()); deviceProfile = deviceProfileService.findDefaultDeviceProfile(device.getTenantId());
} }
entity.setDeviceProfileId(deviceProfile.getId()); device.setDeviceProfileId(deviceProfile.getId());
return tbDeviceService.saveDeviceWithCredentials(entity, deviceCredentials, user); return tbDeviceService.saveDeviceWithCredentials(device, deviceCredentials, user);
} }
@Override @Override
@ -136,6 +141,22 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
entity.setCustomerId(user.getCustomerId()); entity.setCustomerId(user.getCustomerId());
} }
private void setUpDeviceConfiguration(Device device, Map<BulkImportColumnType, String> fields) {
if (fields.containsKey(BulkImportColumnType.SNMP_HOST)) {
SnmpDeviceTransportConfiguration transportConfiguration = new SnmpDeviceTransportConfiguration();
transportConfiguration.setHost(fields.get(BulkImportColumnType.SNMP_HOST));
transportConfiguration.setPort(Optional.ofNullable(fields.get(BulkImportColumnType.SNMP_PORT))
.map(Integer::parseInt).orElse(161));
transportConfiguration.setProtocolVersion(Optional.ofNullable(fields.get(BulkImportColumnType.SNMP_VERSION))
.map(version -> SnmpProtocolVersion.valueOf(version.toUpperCase())).orElse(SnmpProtocolVersion.V2C));
transportConfiguration.setCommunity(fields.getOrDefault(BulkImportColumnType.SNMP_COMMUNITY_STRING, "public"));
DeviceData deviceData = new DeviceData();
deviceData.setTransportConfiguration(transportConfiguration);
device.setDeviceData(deviceData);
}
}
@SneakyThrows @SneakyThrows
private DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceId deviceId, Map<BulkImportColumnType, String> fields) { private DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceId deviceId, Map<BulkImportColumnType, String> fields) {
DeviceCredentials credentials = new DeviceCredentials(); DeviceCredentials credentials = new DeviceCredentials();

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

@ -614,13 +614,13 @@ public final class EdgeGrpcSession implements Closeable {
case EDGE: case EDGE:
return ctx.getEdgeProcessor().convertEdgeEventToDownlink(edgeEvent); return ctx.getEdgeProcessor().convertEdgeEventToDownlink(edgeEvent);
case DEVICE: case DEVICE:
return ctx.getDeviceProcessor().convertDeviceEventToDownlink(edgeEvent); return ctx.getDeviceProcessor().convertDeviceEventToDownlink(edgeEvent, this.edge.getId(), this.edgeVersion);
case DEVICE_PROFILE: case DEVICE_PROFILE:
return ctx.getDeviceProfileProcessor().convertDeviceProfileEventToDownlink(edgeEvent); return ctx.getDeviceProfileProcessor().convertDeviceProfileEventToDownlink(edgeEvent, this.edge.getId(), this.edgeVersion);
case ASSET_PROFILE: case ASSET_PROFILE:
return ctx.getAssetProfileProcessor().convertAssetProfileEventToDownlink(edgeEvent); return ctx.getAssetProfileProcessor().convertAssetProfileEventToDownlink(edgeEvent, this.edge.getId(), this.edgeVersion);
case ASSET: case ASSET:
return ctx.getAssetProcessor().convertAssetEventToDownlink(edgeEvent); return ctx.getAssetProcessor().convertAssetEventToDownlink(edgeEvent, this.edge.getId(), this.edgeVersion);
case ENTITY_VIEW: case ENTITY_VIEW:
return ctx.getEntityViewProcessor().convertEntityViewEventToDownlink(edgeEvent); return ctx.getEntityViewProcessor().convertEntityViewEventToDownlink(edgeEvent);
case DASHBOARD: case DASHBOARD:

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

@ -79,6 +79,7 @@ import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueCallback;
@ -596,4 +597,57 @@ public abstract class BaseEdgeProcessor {
} }
}); });
} }
protected AssetProfile checkIfAssetProfileDefaultFieldsAssignedToEdge(TenantId tenantId, EdgeId edgeId, AssetProfile assetProfile, EdgeVersion edgeVersion) {
switch (edgeVersion) {
case V_3_3_3:
case V_3_3_0:
case V_3_4_0:
if (assetProfile.getDefaultDashboardId() != null
&& isEntityNotAssignedToEdge(tenantId, assetProfile.getDefaultDashboardId(), edgeId)) {
assetProfile.setDefaultDashboardId(null);
}
if (assetProfile.getDefaultEdgeRuleChainId() != null
&& isEntityNotAssignedToEdge(tenantId, assetProfile.getDefaultEdgeRuleChainId(), edgeId)) {
assetProfile.setDefaultEdgeRuleChainId(null);
}
break;
}
return assetProfile;
}
protected DeviceProfile checkIfDeviceProfileDefaultFieldsAssignedToEdge(TenantId tenantId, EdgeId edgeId, DeviceProfile deviceProfile, EdgeVersion edgeVersion) {
switch (edgeVersion) {
case V_3_3_3:
case V_3_3_0:
case V_3_4_0:
if (deviceProfile.getDefaultDashboardId() != null
&& isEntityNotAssignedToEdge(tenantId, deviceProfile.getDefaultDashboardId(), edgeId)) {
deviceProfile.setDefaultDashboardId(null);
}
if (deviceProfile.getDefaultEdgeRuleChainId() != null
&& isEntityNotAssignedToEdge(tenantId, deviceProfile.getDefaultEdgeRuleChainId(), edgeId)) {
deviceProfile.setDefaultEdgeRuleChainId(null);
}
break;
}
return deviceProfile;
}
private boolean isEntityNotAssignedToEdge(TenantId tenantId, EntityId entityId, EdgeId edgeId) {
PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE);
PageData<EdgeId> pageData;
do {
pageData = edgeService.findRelatedEdgeIdsByEntityId(tenantId, entityId, pageLink);
if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) {
if (pageData.getData().contains(edgeId)) {
return false;
}
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
}
}
} while (pageData != null && pageData.hasNext());
return true;
}
} }

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

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -37,6 +38,7 @@ import org.thingsboard.server.dao.asset.BaseAssetService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
@ -106,7 +108,7 @@ public class AssetEdgeProcessor extends BaseAssetProcessor {
} }
} }
public DownlinkMsg convertAssetEventToDownlink(EdgeEvent edgeEvent) { public DownlinkMsg convertAssetEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) {
AssetId assetId = new AssetId(edgeEvent.getEntityId()); AssetId assetId = new AssetId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null; DownlinkMsg downlinkMsg = null;
switch (edgeEvent.getAction()) { switch (edgeEvent.getAction()) {
@ -125,6 +127,7 @@ public class AssetEdgeProcessor extends BaseAssetProcessor {
.addAssetUpdateMsg(assetUpdateMsg); .addAssetUpdateMsg(assetUpdateMsg);
if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) {
AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId()); AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId());
assetProfile = checkIfAssetProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, assetProfile, edgeVersion);
builder.addAssetProfileUpdateMsg(assetProfileMsgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile)); builder.addAssetProfileUpdateMsg(assetProfileMsgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile));
} }
downlinkMsg = builder.build(); downlinkMsg = builder.build();

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

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
@ -36,6 +37,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
@ -94,7 +96,7 @@ public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor {
} }
} }
public DownlinkMsg convertAssetProfileEventToDownlink(EdgeEvent edgeEvent) { public DownlinkMsg convertAssetProfileEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) {
AssetProfileId assetProfileId = new AssetProfileId(edgeEvent.getEntityId()); AssetProfileId assetProfileId = new AssetProfileId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null; DownlinkMsg downlinkMsg = null;
switch (edgeEvent.getAction()) { switch (edgeEvent.getAction()) {
@ -103,6 +105,7 @@ public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor {
AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), assetProfileId); AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), assetProfileId);
if (assetProfile != null) { if (assetProfile != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
assetProfile = checkIfAssetProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, assetProfile, edgeVersion);
AssetProfileUpdateMsg assetProfileUpdateMsg = AssetProfileUpdateMsg assetProfileUpdateMsg =
assetProfileMsgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile); assetProfileMsgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile);
downlinkMsg = DownlinkMsg.newBuilder() downlinkMsg = DownlinkMsg.newBuilder()

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

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.data.rpc.RpcError;
@ -49,6 +50,7 @@ import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.TbQueueMsgMetadata;
@ -202,7 +204,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }
public DownlinkMsg convertDeviceEventToDownlink(EdgeEvent edgeEvent) { public DownlinkMsg convertDeviceEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) {
DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); DeviceId deviceId = new DeviceId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null; DownlinkMsg downlinkMsg = null;
switch (edgeEvent.getAction()) { switch (edgeEvent.getAction()) {
@ -221,6 +223,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor {
.addDeviceUpdateMsg(deviceUpdateMsg); .addDeviceUpdateMsg(deviceUpdateMsg);
if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) {
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId());
deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion);
builder.addDeviceProfileUpdateMsg(deviceProfileMsgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); builder.addDeviceProfileUpdateMsg(deviceProfileMsgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile));
} }
downlinkMsg = builder.build(); downlinkMsg = builder.build();

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

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
@ -36,6 +37,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
@ -94,7 +96,7 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor {
} }
} }
public DownlinkMsg convertDeviceProfileEventToDownlink(EdgeEvent edgeEvent) { public DownlinkMsg convertDeviceProfileEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) {
DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId()); DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null; DownlinkMsg downlinkMsg = null;
switch (edgeEvent.getAction()) { switch (edgeEvent.getAction()) {
@ -103,6 +105,7 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor {
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), deviceProfileId); DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), deviceProfileId);
if (deviceProfile != null) { if (deviceProfile != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion);
DeviceProfileUpdateMsg deviceProfileUpdateMsg = DeviceProfileUpdateMsg deviceProfileUpdateMsg =
deviceProfileMsgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile); deviceProfileMsgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile);
downlinkMsg = DownlinkMsg.newBuilder() downlinkMsg = DownlinkMsg.newBuilder()

21
application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

@ -28,7 +28,6 @@ import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainMetaData;
@ -104,15 +103,15 @@ public class InstallScripts {
@Autowired @Autowired
private ResourceService resourceService; private ResourceService resourceService;
private Path getTenantRuleChainsDir() { Path getTenantRuleChainsDir() {
return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR); return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR);
} }
private Path getDeviceProfileDefaultRuleChainTemplateFilePath() { Path getDeviceProfileDefaultRuleChainTemplateFilePath() {
return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DEVICE_PROFILE_DIR, "rule_chain_template.json"); return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DEVICE_PROFILE_DIR, "rule_chain_template.json");
} }
private Path getEdgeRuleChainsDir() { Path getEdgeRuleChainsDir() {
return Paths.get(getDataDir(), JSON_DIR, EDGE_DIR, RULE_CHAINS_DIR); return Paths.get(getDataDir(), JSON_DIR, EDGE_DIR, RULE_CHAINS_DIR);
} }
@ -148,8 +147,8 @@ public class InstallScripts {
} }
private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) throws IOException { private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) throws IOException {
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(ruleChainsPath, path -> path.toString().endsWith(InstallScripts.JSON_EXT))) { findRuleChainsFromPath(ruleChainsPath)
dirStream.forEach( .forEach(
path -> { path -> {
try { try {
createRuleChainFromFile(tenantId, path, null); createRuleChainFromFile(tenantId, path, null);
@ -157,9 +156,15 @@ public class InstallScripts {
log.error("Unable to load rule chain from json: [{}]", path.toString()); log.error("Unable to load rule chain from json: [{}]", path.toString());
throw new RuntimeException("Unable to load rule chain from json", e); throw new RuntimeException("Unable to load rule chain from json", e);
} }
} });
); }
List<Path> findRuleChainsFromPath(Path ruleChainsPath) throws IOException {
List<Path> paths = new ArrayList<>();
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(ruleChainsPath, path -> path.toString().endsWith(InstallScripts.JSON_EXT))) {
dirStream.forEach(paths::add);
} }
return paths;
} }
public RuleChain createDefaultRuleChain(TenantId tenantId, String ruleChainName) throws IOException { public RuleChain createDefaultRuleChain(TenantId tenantId, String ruleChainName) throws IOException {

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

@ -24,10 +24,14 @@ import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.event.ErrorEvent;
import org.thingsboard.server.common.data.event.Event;
import org.thingsboard.server.common.data.event.LifecycleEvent;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -45,7 +49,9 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.EdgeNotificationMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.EdgeNotificationMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.ErrorEventProto;
import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto; import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto;
import org.thingsboard.server.gen.transport.TransportProtos.LifecycleEventProto;
import org.thingsboard.server.gen.transport.TransportProtos.LocalSubscriptionServiceMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.LocalSubscriptionServiceMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionMgrMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionMgrMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmDeleteProto; import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmDeleteProto;
@ -274,6 +280,10 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
TransportProtos.NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = toCoreMsg.getNotificationSchedulerServiceMsg(); TransportProtos.NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = toCoreMsg.getNotificationSchedulerServiceMsg();
log.trace("[{}] Forwarding message to notification scheduler service {}", id, toCoreMsg.getNotificationSchedulerServiceMsg()); log.trace("[{}] Forwarding message to notification scheduler service {}", id, toCoreMsg.getNotificationSchedulerServiceMsg());
forwardToNotificationSchedulerService(notificationSchedulerServiceMsg, callback); forwardToNotificationSchedulerService(notificationSchedulerServiceMsg, callback);
} else if (toCoreMsg.hasErrorEventMsg()) {
forwardToEventService(toCoreMsg.getErrorEventMsg(), callback);
} else if (toCoreMsg.hasLifecycleEventMsg()) {
forwardToEventService(toCoreMsg.getLifecycleEventMsg(), callback);
} }
} catch (Throwable e) { } catch (Throwable e) {
log.warn("[{}] Failed to process message: {}", id, msg, e); log.warn("[{}] Failed to process message: {}", id, msg, e);
@ -519,50 +529,52 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
subscriptionManagerService.cancelSubscription(closeProto.getSessionId(), closeProto.getSubscriptionId(), callback); subscriptionManagerService.cancelSubscription(closeProto.getSessionId(), closeProto.getSubscriptionId(), callback);
} else if (msg.hasTsUpdate()) { } else if (msg.hasTsUpdate()) {
TbTimeSeriesUpdateProto proto = msg.getTsUpdate(); TbTimeSeriesUpdateProto proto = msg.getTsUpdate();
long tenantIdMSB = proto.getTenantIdMSB();
long tenantIdLSB = proto.getTenantIdLSB();
subscriptionManagerService.onTimeSeriesUpdate( subscriptionManagerService.onTimeSeriesUpdate(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), toTenantId(tenantIdMSB, tenantIdLSB),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()), TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
TbSubscriptionUtils.toTsKvEntityList(proto.getDataList()), callback); TbSubscriptionUtils.toTsKvEntityList(proto.getDataList()), callback);
} else if (msg.hasAttrUpdate()) { } else if (msg.hasAttrUpdate()) {
TbAttributeUpdateProto proto = msg.getAttrUpdate(); TbAttributeUpdateProto proto = msg.getAttrUpdate();
subscriptionManagerService.onAttributesUpdate( subscriptionManagerService.onAttributesUpdate(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()), TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
proto.getScope(), TbSubscriptionUtils.toAttributeKvList(proto.getDataList()), callback); proto.getScope(), TbSubscriptionUtils.toAttributeKvList(proto.getDataList()), callback);
} else if (msg.hasAttrDelete()) { } else if (msg.hasAttrDelete()) {
TbAttributeDeleteProto proto = msg.getAttrDelete(); TbAttributeDeleteProto proto = msg.getAttrDelete();
subscriptionManagerService.onAttributesDelete( subscriptionManagerService.onAttributesDelete(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()), TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
proto.getScope(), proto.getKeysList(), proto.getNotifyDevice(), callback); proto.getScope(), proto.getKeysList(), proto.getNotifyDevice(), callback);
} else if (msg.hasTsDelete()) { } else if (msg.hasTsDelete()) {
TbTimeSeriesDeleteProto proto = msg.getTsDelete(); TbTimeSeriesDeleteProto proto = msg.getTsDelete();
subscriptionManagerService.onTimeSeriesDelete( subscriptionManagerService.onTimeSeriesDelete(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()), TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
proto.getKeysList(), callback); proto.getKeysList(), callback);
} else if (msg.hasAlarmUpdate()) { } else if (msg.hasAlarmUpdate()) {
TbAlarmUpdateProto proto = msg.getAlarmUpdate(); TbAlarmUpdateProto proto = msg.getAlarmUpdate();
subscriptionManagerService.onAlarmUpdate( subscriptionManagerService.onAlarmUpdate(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()), TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
JacksonUtil.fromString(proto.getAlarm(), AlarmInfo.class), JacksonUtil.fromString(proto.getAlarm(), AlarmInfo.class),
callback); callback);
} else if (msg.hasAlarmDelete()) { } else if (msg.hasAlarmDelete()) {
TbAlarmDeleteProto proto = msg.getAlarmDelete(); TbAlarmDeleteProto proto = msg.getAlarmDelete();
subscriptionManagerService.onAlarmDeleted( subscriptionManagerService.onAlarmDeleted(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()), TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
JacksonUtil.fromString(proto.getAlarm(), AlarmInfo.class), callback); JacksonUtil.fromString(proto.getAlarm(), AlarmInfo.class), callback);
} else if (msg.hasNotificationUpdate()) { } else if (msg.hasNotificationUpdate()) {
TransportProtos.NotificationUpdateProto updateProto = msg.getNotificationUpdate(); TransportProtos.NotificationUpdateProto updateProto = msg.getNotificationUpdate();
TenantId tenantId = TenantId.fromUUID(new UUID(updateProto.getTenantIdMSB(), updateProto.getTenantIdLSB())); TenantId tenantId = toTenantId(updateProto.getTenantIdMSB(), updateProto.getTenantIdLSB());
UserId recipientId = new UserId(new UUID(updateProto.getRecipientIdMSB(), updateProto.getRecipientIdLSB())); UserId recipientId = new UserId(new UUID(updateProto.getRecipientIdMSB(), updateProto.getRecipientIdLSB()));
NotificationUpdate update = JacksonUtil.fromString(updateProto.getUpdate(), NotificationUpdate.class); NotificationUpdate update = JacksonUtil.fromString(updateProto.getUpdate(), NotificationUpdate.class);
subscriptionManagerService.onNotificationUpdate(tenantId, recipientId, update, callback); subscriptionManagerService.onNotificationUpdate(tenantId, recipientId, update, callback);
} else if (msg.hasNotificationRequestUpdate()) { } else if (msg.hasNotificationRequestUpdate()) {
TransportProtos.NotificationRequestUpdateProto updateProto = msg.getNotificationRequestUpdate(); TransportProtos.NotificationRequestUpdateProto updateProto = msg.getNotificationRequestUpdate();
TenantId tenantId = TenantId.fromUUID(new UUID(updateProto.getTenantIdMSB(), updateProto.getTenantIdLSB())); TenantId tenantId = toTenantId(updateProto.getTenantIdMSB(), updateProto.getTenantIdLSB());
NotificationRequestUpdate update = JacksonUtil.fromString(updateProto.getUpdate(), NotificationRequestUpdate.class); NotificationRequestUpdate update = JacksonUtil.fromString(updateProto.getUpdate(), NotificationRequestUpdate.class);
subscriptionManagerService.onNotificationRequestUpdate(tenantId, update, callback); subscriptionManagerService.onNotificationRequestUpdate(tenantId, update, callback);
} else { } else {
@ -584,7 +596,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
if (statsEnabled) { if (statsEnabled) {
stats.log(deviceActivityMsg); stats.log(deviceActivityMsg);
} }
TenantId tenantId = TenantId.fromUUID(new UUID(deviceActivityMsg.getTenantIdMSB(), deviceActivityMsg.getTenantIdLSB())); TenantId tenantId = toTenantId(deviceActivityMsg.getTenantIdMSB(), deviceActivityMsg.getTenantIdLSB());
DeviceId deviceId = new DeviceId(new UUID(deviceActivityMsg.getDeviceIdMSB(), deviceActivityMsg.getDeviceIdLSB())); DeviceId deviceId = new DeviceId(new UUID(deviceActivityMsg.getDeviceIdMSB(), deviceActivityMsg.getDeviceIdLSB()));
try { try {
stateService.onDeviceActivity(tenantId, deviceId, deviceActivityMsg.getLastActivityTime()); stateService.onDeviceActivity(tenantId, deviceId, deviceActivityMsg.getLastActivityTime());
@ -595,7 +607,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
} }
private void forwardToNotificationSchedulerService(TransportProtos.NotificationSchedulerServiceMsg msg, TbCallback callback) { private void forwardToNotificationSchedulerService(TransportProtos.NotificationSchedulerServiceMsg msg, TbCallback callback) {
TenantId tenantId = TenantId.fromUUID(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); TenantId tenantId = toTenantId(msg.getTenantIdMSB(), msg.getTenantIdLSB());
NotificationRequestId notificationRequestId = new NotificationRequestId(new UUID(msg.getRequestIdMSB(), msg.getRequestIdLSB())); NotificationRequestId notificationRequestId = new NotificationRequestId(new UUID(msg.getRequestIdMSB(), msg.getRequestIdLSB()));
try { try {
notificationSchedulerService.scheduleNotificationRequest(tenantId, notificationRequestId, msg.getTs()); notificationSchedulerService.scheduleNotificationRequest(tenantId, notificationRequestId, msg.getTs());
@ -627,11 +639,47 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
callback.onSuccess(); callback.onSuccess();
} }
private void forwardToEventService(ErrorEventProto eventProto, TbCallback callback) {
Event event = ErrorEvent.builder()
.tenantId(toTenantId(eventProto.getTenantIdMSB(), eventProto.getTenantIdLSB()))
.entityId(new UUID(eventProto.getEntityIdMSB(), eventProto.getEntityIdLSB()))
.serviceId(eventProto.getServiceId())
.ts(System.currentTimeMillis())
.method(eventProto.getMethod())
.error(eventProto.getError())
.build();
forwardToEventService(event, callback);
}
private void forwardToEventService(LifecycleEventProto eventProto, TbCallback callback) {
Event event = LifecycleEvent.builder()
.tenantId(toTenantId(eventProto.getTenantIdMSB(), eventProto.getTenantIdLSB()))
.entityId(new UUID(eventProto.getEntityIdMSB(), eventProto.getEntityIdLSB()))
.serviceId(eventProto.getServiceId())
.ts(System.currentTimeMillis())
.lcEventType(eventProto.getLcEventType())
.success(eventProto.getSuccess())
.error(StringUtils.isNotEmpty(eventProto.getError()) ? eventProto.getError() : null)
.build();
forwardToEventService(event, callback);
}
private void forwardToEventService(Event event, TbCallback callback) {
DonAsynchron.withCallback(actorContext.getEventService().saveAsync(event),
result -> callback.onSuccess(),
callback::onFailure,
actorContext.getDbCallbackExecutor());
}
private void throwNotHandled(Object msg, TbCallback callback) { private void throwNotHandled(Object msg, TbCallback callback) {
log.warn("Message not handled: {}", msg); log.warn("Message not handled: {}", msg);
callback.onFailure(new RuntimeException("Message not handled!")); callback.onFailure(new RuntimeException("Message not handled!"));
} }
private TenantId toTenantId(long tenantIdMSB, long tenantIdLSB) {
return TenantId.fromUUID(new UUID(tenantIdMSB, tenantIdLSB));
}
@Override @Override
protected void stopMainConsumers() { protected void stopMainConsumers() {
if (mainConsumer != null) { if (mainConsumer != null) {

5
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java

@ -22,6 +22,7 @@ import com.google.gson.JsonPrimitive;
import lombok.Data; import lombok.Data;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
@ -57,7 +58,7 @@ import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import org.thingsboard.server.utils.CsvUtils; import org.thingsboard.server.utils.CsvUtils;
import org.thingsboard.server.utils.TypeCastUtil; import org.thingsboard.server.common.data.util.TypeCastUtil;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
@ -269,7 +270,7 @@ public abstract class AbstractBulkImportService<E extends HasId<? extends Entity
if (!entry.getKey().getType().isKv()) { if (!entry.getKey().getType().isKv()) {
entityData.getFields().put(entry.getKey().getType(), entry.getValue()); entityData.getFields().put(entry.getKey().getType(), entry.getValue());
} else { } else {
Map.Entry<DataType, Object> castResult = TypeCastUtil.castValue(entry.getValue()); Pair<DataType, Object> castResult = TypeCastUtil.castValue(entry.getValue());
entityData.getKvs().put(entry.getKey(), new ParsedValue(castResult.getValue(), castResult.getKey())); entityData.getKvs().put(entry.getKey(), new ParsedValue(castResult.getValue(), castResult.getKey()));
} }
}); });

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

@ -991,11 +991,15 @@ transport:
# value: "${LWM2M_PROTOCOL_STAGE_THREAD_COUNT:4}" # value: "${LWM2M_PROTOCOL_STAGE_THREAD_COUNT:4}"
snmp: snmp:
enabled: "${SNMP_ENABLED:true}" enabled: "${SNMP_ENABLED:true}"
bind_port: "${SNMP_BIND_PORT:1620}"
response_processing: response_processing:
# parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices # parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices
parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:20}" parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:20}"
# to configure SNMP to work over UDP or TCP # to configure SNMP to work over UDP or TCP
underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}" underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}"
max_request_oids: "${SNMP_MAX_REQUEST_OIDS:100}"
response:
ignore_type_cast_errors: "${SNMP_RESPONSE_IGNORE_TYPE_CAST_ERRORS:false}"
stats: stats:
enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" enabled: "${TB_TRANSPORT_STATS_ENABLED:true}"
print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}"

4
application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java

@ -66,6 +66,8 @@ import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.PEM_CERT_FI
@TestPropertySource(properties = { @TestPropertySource(properties = {
"device.connectivity.https.enabled=true", "device.connectivity.https.enabled=true",
"device.connectivity.http.port=8080",
"device.connectivity.https.port=444",
"device.connectivity.mqtts.enabled=true", "device.connectivity.mqtts.enabled=true",
"device.connectivity.mqtts.pem_cert_file=/tmp/" + PEM_CERT_FILE_NAME, "device.connectivity.mqtts.pem_cert_file=/tmp/" + PEM_CERT_FILE_NAME,
"device.connectivity.coaps.enabled=true", "device.connectivity.coaps.enabled=true",
@ -191,7 +193,7 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://localhost:8080/api/v1/%s/telemetry " + assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://localhost:8080/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"", "--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId())); credentials.getCredentialsId()));
assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost:443/api/v1/%s/telemetry " + assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost:444/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"", "--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId())); credentials.getCredentialsId()));

144
application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerWithDefaultPortTest.java

@ -0,0 +1,144 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.device.DeviceDao;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAP;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.DOCKER;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.PEM_CERT_FILE_NAME;
@TestPropertySource(properties = {
"device.connectivity.https.enabled=true",
"device.connectivity.http.port=80",
"device.connectivity.mqtt.enabled=false",
"device.connectivity.mqtts.enabled=false",
"device.connectivity.coap.enabled=false",
"device.connectivity.coaps.enabled=false",
})
@ContextConfiguration(classes = {DeviceConnectivityControllerWithDefaultPortTest.Config.class})
@DaoSqlTest
public class DeviceConnectivityControllerWithDefaultPortTest extends AbstractControllerTest {
ListeningExecutorService executor;
private Tenant savedTenant;
static class Config {
@Bean
@Primary
public DeviceDao deviceDao(DeviceDao deviceDao) {
return Mockito.mock(DeviceDao.class, AdditionalAnswers.delegatesTo(deviceDao));
}
}
@Before
public void beforeTest() throws Exception {
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass()));
loginSysAdmin();
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
savedTenant = doPost("/api/tenant", tenant, Tenant.class);
Assert.assertNotNull(savedTenant);
User tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(savedTenant.getId());
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
createUserAndLogin(tenantAdmin, "testPassword1");
}
@After
public void afterTest() throws Exception {
executor.shutdownNow();
loginSysAdmin();
doDelete("/api/tenant/" + savedTenant.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testFetchPublishTelemetryCommandsForDefaultDevice() throws Exception {
Device device = new Device();
device.setName("My device");
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
assertThat(commands).hasSize(1);
JsonNode httpCommands = commands.get(HTTP);
assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://localhost/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
}
}

306
application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java

@ -0,0 +1,306 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor;
import org.junit.jupiter.params.provider.Arguments;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.context.annotation.Lazy;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeEventService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.edge.EdgeSynchronizationManager;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.service.edge.rpc.constructor.AdminSettingsMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.AlarmMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.AssetMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.AssetProfileMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.CustomerMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.DashboardMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.DeviceMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.DeviceProfileMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.EdgeMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.EntityDataMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.EntityViewMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.OtaPackageMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.QueueMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.RelationMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.RuleChainMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.TenantMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.TenantProfileMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.UserMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.WidgetTypeMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.WidgetsBundleMsgConstructor;
import org.thingsboard.server.service.entitiy.TbNotificationEntityService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.util.UUID;
import java.util.stream.Stream;
public abstract class BaseEdgeProcessorTest {
@MockBean
protected TelemetrySubscriptionService tsSubService;
@MockBean
protected TbNotificationEntityService notificationEntityService;
@MockBean
protected RuleChainService ruleChainService;
@MockBean
protected AlarmService alarmService;
@MockBean
protected DeviceService deviceService;
@MockBean
protected TbDeviceProfileCache deviceProfileCache;
@MockBean
protected TbAssetProfileCache assetProfileCache;
@MockBean
protected DashboardService dashboardService;
@MockBean
protected AssetService assetService;
@MockBean
protected EntityViewService entityViewService;
@MockBean
protected TenantService tenantService;
@MockBean
protected TenantProfileService tenantProfileService;
@MockBean
protected EdgeService edgeService;
@MockBean
protected CustomerService customerService;
@MockBean
protected UserService userService;
@MockBean
protected DeviceProfileService deviceProfileService;
@MockBean
protected AssetProfileService assetProfileService;
@MockBean
protected RelationService relationService;
@MockBean
protected DeviceCredentialsService deviceCredentialsService;
@MockBean
protected AttributesService attributesService;
@MockBean
protected TbClusterService tbClusterService;
@MockBean
protected DeviceStateService deviceStateService;
@MockBean
protected EdgeEventService edgeEventService;
@MockBean
protected WidgetsBundleService widgetsBundleService;
@MockBean
protected WidgetTypeService widgetTypeService;
@MockBean
protected OtaPackageService otaPackageService;
@MockBean
protected QueueService queueService;
@MockBean
protected PartitionService partitionService;
@MockBean
@Lazy
protected TbQueueProducerProvider producerProvider;
@MockBean
protected DataValidator<Device> deviceValidator;
@MockBean
protected DataValidator<DeviceProfile> deviceProfileValidator;
@MockBean
protected DataValidator<Asset> assetValidator;
@MockBean
protected DataValidator<AssetProfile> assetProfileValidator;
@MockBean
protected DataValidator<Dashboard> dashboardValidator;
@MockBean
protected DataValidator<EntityView> entityViewValidator;
@MockBean
protected EdgeMsgConstructor edgeMsgConstructor;
@MockBean
protected EntityDataMsgConstructor entityDataMsgConstructor;
@MockBean
protected RuleChainMsgConstructor ruleChainMsgConstructor;
@MockBean
protected AlarmMsgConstructor alarmMsgConstructor;
@SpyBean
protected DeviceMsgConstructor deviceMsgConstructor;
@SpyBean
protected AssetMsgConstructor assetMsgConstructor;
@MockBean
protected EntityViewMsgConstructor entityViewMsgConstructor;
@MockBean
protected DashboardMsgConstructor dashboardMsgConstructor;
@MockBean
protected RelationMsgConstructor relationMsgConstructor;
@MockBean
protected UserMsgConstructor userMsgConstructor;
@MockBean
protected CustomerMsgConstructor customerMsgConstructor;
@SpyBean
protected DeviceProfileMsgConstructor deviceProfileMsgConstructor;
@SpyBean
protected AssetProfileMsgConstructor assetProfileMsgConstructor;
@MockBean
protected TenantMsgConstructor tenantMsgConstructor;
@MockBean
protected TenantProfileMsgConstructor tenantProfileMsgConstructor;
@MockBean
protected WidgetsBundleMsgConstructor widgetsBundleMsgConstructor;
@MockBean
protected WidgetTypeMsgConstructor widgetTypeMsgConstructor;
@MockBean
protected AdminSettingsMsgConstructor adminSettingsMsgConstructor;
@MockBean
protected OtaPackageMsgConstructor otaPackageMsgConstructor;
@MockBean
protected QueueMsgConstructor queueMsgConstructor;
@MockBean
protected EdgeSynchronizationManager edgeSynchronizationManager;
@MockBean
protected DbCallbackExecutorService dbCallbackExecutorService;
@MockBean
protected DataDecodingEncodingService dataDecodingEncodingService;
protected EdgeId edgeId;
protected TenantId tenantId;
protected EdgeEvent edgeEvent;
protected DashboardId getDashboardId(long expectedDashboardIdMSB, long expectedDashboardIdLSB) {
DashboardId dashboardId;
if (expectedDashboardIdMSB != 0 && expectedDashboardIdLSB != 0) {
dashboardId = new DashboardId(new UUID(expectedDashboardIdMSB, expectedDashboardIdLSB));
} else {
dashboardId = new DashboardId(UUID.randomUUID());
}
return dashboardId;
}
protected RuleChainId getRuleChainId(long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
RuleChainId ruleChainId;
if (expectedRuleChainIdMSB != 0 && expectedRuleChainIdLSB != 0) {
ruleChainId = new RuleChainId(new UUID(expectedRuleChainIdMSB, expectedRuleChainIdLSB));
} else {
ruleChainId = new RuleChainId(UUID.randomUUID());
}
return ruleChainId;
}
protected static Stream<Arguments> provideParameters() {
UUID dashoboardUUID = UUID.randomUUID();
UUID ruleChaindUUID = UUID.randomUUID();
return Stream.of(
Arguments.of(EdgeVersion.V_3_3_0, 0, 0, 0, 0),
Arguments.of(EdgeVersion.V_3_3_3, 0, 0, 0, 0),
Arguments.of(EdgeVersion.V_3_4_0, 0, 0, 0, 0),
Arguments.of(EdgeVersion.V_3_6_0,
dashoboardUUID.getMostSignificantBits(),
dashoboardUUID.getLeastSignificantBits(),
ruleChaindUUID.getMostSignificantBits(),
ruleChaindUUID.getLeastSignificantBits())
);
}
}

92
application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AbstractAssetProcessorTest.java

@ -0,0 +1,92 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.asset;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessorTest;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.BDDMockito.willReturn;
public abstract class AbstractAssetProcessorTest extends BaseEdgeProcessorTest {
protected AssetId assetId;
protected AssetProfileId assetProfileId;
protected AssetProfile assetProfile;
@BeforeEach
public void setUp() {
edgeId = new EdgeId(UUID.randomUUID());
tenantId = new TenantId(UUID.randomUUID());
assetId = new AssetId(UUID.randomUUID());
assetProfileId = new AssetProfileId(UUID.randomUUID());
assetProfile = new AssetProfile();
assetProfile.setId(assetProfileId);
assetProfile.setName("AssetProfile");
assetProfile.setDefault(true);
Asset asset = new Asset();
asset.setAssetProfileId(assetProfileId);
asset.setId(assetId);
asset.setName("Asset");
asset.setType(assetProfile.getName());
edgeEvent = new EdgeEvent();
edgeEvent.setTenantId(tenantId);
edgeEvent.setAction(EdgeEventActionType.ADDED);
willReturn(asset).given(assetService).findAssetById(tenantId, assetId);
willReturn(assetProfile).given(assetProfileService).findAssetProfileById(tenantId, assetProfileId);
}
protected void updateAssetProfileDefaultFields(long expectedDashboardIdMSB, long expectedDashboardIdLSB,
long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
DashboardId dashboardId = getDashboardId(expectedDashboardIdMSB, expectedDashboardIdLSB);
RuleChainId ruleChainId = getRuleChainId(expectedRuleChainIdMSB, expectedRuleChainIdLSB);
assetProfile.setDefaultDashboardId(dashboardId);
assetProfile.setDefaultEdgeRuleChainId(ruleChainId);
}
protected void verify(DownlinkMsg downlinkMsg, long expectedDashboardIdMSB, long expectedDashboardIdLSB,
long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
AssetProfileUpdateMsg assetProfileUpdateMsg = downlinkMsg.getAssetProfileUpdateMsgList().get(0);
assertNotNull(assetProfileUpdateMsg);
Assertions.assertThat(assetProfileUpdateMsg.getDefaultDashboardIdMSB()).isEqualTo(expectedDashboardIdMSB);
Assertions.assertThat(assetProfileUpdateMsg.getDefaultDashboardIdLSB()).isEqualTo(expectedDashboardIdLSB);
Assertions.assertThat(assetProfileUpdateMsg.getDefaultRuleChainIdMSB()).isEqualTo(expectedRuleChainIdMSB);
Assertions.assertThat(assetProfileUpdateMsg.getDefaultRuleChainIdLSB()).isEqualTo(expectedRuleChainIdLSB);
}
}

44
application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorTest.java

@ -0,0 +1,44 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.asset;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
@SpringBootTest(classes = {AssetEdgeProcessor.class})
class AssetEdgeProcessorTest extends AbstractAssetProcessorTest {
@SpyBean
AssetEdgeProcessor assetEdgeProcessor;
@ParameterizedTest
@MethodSource("provideParameters")
public void testAssetProfileDefaultFields_notSendToEdgeOlder3_6_0IfNotAssigned(EdgeVersion edgeVersion, long expectedDashboardIdMSB, long expectedDashboardIdLSB,
long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
updateAssetProfileDefaultFields(expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB);
edgeEvent.setEntityId(assetId.getId());
DownlinkMsg downlinkMsg = assetEdgeProcessor.convertAssetEventToDownlink(edgeEvent, edgeId, edgeVersion);
verify(downlinkMsg, expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB);
}
}

43
application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessorTest.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.asset;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
@SpringBootTest(classes = {AssetProfileEdgeProcessor.class})
class AssetProfileEdgeProcessorTest extends AbstractAssetProcessorTest{
@SpyBean
AssetProfileEdgeProcessor assetProfileEdgeProcessor;
@ParameterizedTest
@MethodSource("provideParameters")
public void testAssetProfileDefaultFields_notSendToEdgeOlder3_6_0IfNotAssigned(EdgeVersion edgeVersion, long expectedDashboardIdMSB, long expectedDashboardIdLSB,
long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
updateAssetProfileDefaultFields(expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB);
edgeEvent.setEntityId(assetProfileId.getId());
DownlinkMsg downlinkMsg = assetProfileEdgeProcessor.convertAssetProfileEventToDownlink(edgeEvent, edgeId, edgeVersion);
verify(downlinkMsg, expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB);
}
}

100
application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java

@ -0,0 +1,100 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.device;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessorTest;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.BDDMockito.willReturn;
public abstract class AbstractDeviceProcessorTest extends BaseEdgeProcessorTest {
protected DeviceId deviceId;
protected DeviceProfileId deviceProfileId;
protected DeviceProfile deviceProfile;
@BeforeEach
public void setUp() {
edgeId = new EdgeId(UUID.randomUUID());
tenantId = new TenantId(UUID.randomUUID());
deviceId = new DeviceId(UUID.randomUUID());
deviceProfileId = new DeviceProfileId(UUID.randomUUID());
deviceProfile = new DeviceProfile();
deviceProfile.setId(deviceProfileId);
deviceProfile.setName("DeviceProfile");
deviceProfile.setDefault(true);
deviceProfile.setType(DeviceProfileType.DEFAULT);
DeviceProfileData deviceProfileData = new DeviceProfileData();
deviceProfile.setProfileData(deviceProfileData);
deviceProfile.setTransportType(DeviceTransportType.DEFAULT);
Device device = new Device();
device.setDeviceProfileId(deviceProfileId);
device.setId(deviceId);
device.setName("Device");
device.setType(deviceProfile.getName());
edgeEvent = new EdgeEvent();
edgeEvent.setTenantId(tenantId);
edgeEvent.setAction(EdgeEventActionType.ADDED);
willReturn(device).given(deviceService).findDeviceById(tenantId, deviceId);
willReturn(deviceProfile).given(deviceProfileService).findDeviceProfileById(tenantId, deviceProfileId);
willReturn(new byte[]{0x00}).given(dataDecodingEncodingService).encode(deviceProfileData);
}
protected void updateDeviceProfileDefaultFields(long expectedDashboardIdMSB, long expectedDashboardIdLSB,
long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
DashboardId dashboardId = getDashboardId(expectedDashboardIdMSB, expectedDashboardIdLSB);
RuleChainId ruleChainId = getRuleChainId(expectedRuleChainIdMSB, expectedRuleChainIdLSB);
deviceProfile.setDefaultDashboardId(dashboardId);
deviceProfile.setDefaultEdgeRuleChainId(ruleChainId);
}
protected void verify(DownlinkMsg downlinkMsg, long expectedDashboardIdMSB, long expectedDashboardIdLSB,
long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
DeviceProfileUpdateMsg deviceProfileUpdateMsg = downlinkMsg.getDeviceProfileUpdateMsgList().get(0);
assertNotNull(deviceProfileUpdateMsg);
Assertions.assertThat(deviceProfileUpdateMsg.getDefaultDashboardIdMSB()).isEqualTo(expectedDashboardIdMSB);
Assertions.assertThat(deviceProfileUpdateMsg.getDefaultDashboardIdLSB()).isEqualTo(expectedDashboardIdLSB);
Assertions.assertThat(deviceProfileUpdateMsg.getDefaultRuleChainIdMSB()).isEqualTo(expectedRuleChainIdMSB);
Assertions.assertThat(deviceProfileUpdateMsg.getDefaultRuleChainIdLSB()).isEqualTo(expectedRuleChainIdLSB);
}
}

43
application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessorTest.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.device;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
@SpringBootTest(classes = {DeviceEdgeProcessor.class})
class DeviceEdgeProcessorTest extends AbstractDeviceProcessorTest {
@SpyBean
DeviceEdgeProcessor deviceEdgeProcessor;
@ParameterizedTest
@MethodSource("provideParameters")
public void testDeviceProfileDefaultFields_notSendToEdgeOlder3_6_0IfNotAssigned(EdgeVersion edgeVersion, long expectedDashboardIdMSB, long expectedDashboardIdLSB,
long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
updateDeviceProfileDefaultFields(expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB);
edgeEvent.setEntityId(deviceId.getId());
DownlinkMsg downlinkMsg = deviceEdgeProcessor.convertDeviceEventToDownlink(edgeEvent, edgeId, edgeVersion);
verify(downlinkMsg, expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB);
}
}

45
application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessorTest.java

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.device;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
@SpringBootTest(classes = {DeviceProfileEdgeProcessor.class})
class DeviceProfileEdgeProcessorTest extends AbstractDeviceProcessorTest {
@SpyBean
DeviceProfileEdgeProcessor deviceProfileEdgeProcessor;
@ParameterizedTest
@MethodSource("provideParameters")
public void testDeviceProfileDefaultFields_notSendToEdgeOlder3_6_0IfNotAssigned(EdgeVersion edgeVersion, long expectedDashboardIdMSB, long expectedDashboardIdLSB,
long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
updateDeviceProfileDefaultFields(expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB);
edgeEvent.setEntityId(deviceProfileId.getId());
DownlinkMsg downlinkMsg = deviceProfileEdgeProcessor.convertDeviceProfileEventToDownlink(edgeEvent, edgeId, edgeVersion);
verify(downlinkMsg, expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB);
}
}

119
application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java

@ -0,0 +1,119 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.install;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.validator.RuleChainDataValidator;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.willReturn;
@Slf4j
@SpringBootTest(classes = {InstallScripts.class, RuleChainDataValidator.class})
class InstallScriptsTest {
@MockBean
RuleChainService ruleChainService;
@MockBean
DashboardService dashboardService;
@MockBean
WidgetTypeService widgetTypeService;
@MockBean
WidgetsBundleService widgetsBundleService;
@MockBean
OAuth2ConfigTemplateService oAuth2TemplateService;
@MockBean
ResourceService resourceService;
@SpyBean
InstallScripts installScripts;
@MockBean
TenantService tenantService;
@MockBean
ApiLimitService apiLimitService;
@SpyBean
RuleChainDataValidator ruleChainValidator;
TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da"));
@BeforeEach
void setUp() {
willReturn(true).given(tenantService).tenantExists(tenantId);
willReturn(true).given(apiLimitService).checkEntitiesLimit(any(), any());
}
@Test
void testDefaultRuleChainsTemplates() throws IOException {
Path dir = installScripts.getTenantRuleChainsDir();
installScripts.findRuleChainsFromPath(dir)
.forEach(this::validateRuleChainTemplate);
}
@Test
void testDefaultEdgeRuleChainsTemplates() throws IOException {
Path dir = installScripts.getEdgeRuleChainsDir();
installScripts.findRuleChainsFromPath(dir)
.forEach(this::validateRuleChainTemplate);
}
@Test
void testDeviceProfileDefaultRuleChainTemplate() {
validateRuleChainTemplate(installScripts.getDeviceProfileDefaultRuleChainTemplateFilePath());
}
private void validateRuleChainTemplate(Path templateFilePath) {
log.warn("validateRuleChainTemplate {}", templateFilePath);
JsonNode ruleChainJson = JacksonUtil.toJsonNode(templateFilePath.toFile());
RuleChain ruleChain = JacksonUtil.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class);
ruleChain.setTenantId(tenantId);
ruleChainValidator.validate(ruleChain, RuleChain::getTenantId);
ruleChain.setId(new RuleChainId(UUID.randomUUID()));
RuleChainMetaData ruleChainMetaData = JacksonUtil.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class);
ruleChainMetaData.setRuleChainId(ruleChain.getId());
List<Throwable> throwables = RuleChainDataValidator.validateMetaData(ruleChainMetaData);
assertThat(throwables).as("templateFilePath " + templateFilePath)
.containsExactlyInAnyOrderElementsOf(Collections.emptyList());
}
}

23
common/cluster-api/src/main/proto/queue.proto

@ -975,6 +975,8 @@ message ToCoreMsg {
EdgeNotificationMsgProto edgeNotificationMsg = 5; EdgeNotificationMsgProto edgeNotificationMsg = 5;
DeviceActivityProto deviceActivityMsg = 6; DeviceActivityProto deviceActivityMsg = 6;
NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = 7; NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = 7;
LifecycleEventProto lifecycleEventMsg = 8;
ErrorEventProto errorEventMsg = 9;
} }
/* High priority messages with low latency are handled by ThingsBoard Core Service separately */ /* High priority messages with low latency are handled by ThingsBoard Core Service separately */
@ -1066,3 +1068,24 @@ message NotificationSchedulerServiceMsg {
message NotificationRuleProcessorMsg { message NotificationRuleProcessorMsg {
bytes trigger = 1; bytes trigger = 1;
} }
message ErrorEventProto {
int64 tenantIdMSB = 1;
int64 tenantIdLSB = 2;
int64 entityIdMSB = 3;
int64 entityIdLSB = 4;
string serviceId = 5;
string method = 6;
string error = 7;
}
message LifecycleEventProto {
int64 tenantIdMSB = 1;
int64 tenantIdLSB = 2;
int64 entityIdMSB = 3;
int64 entityIdLSB = 4;
string serviceId = 5;
string lcEventType = 6;
bool success = 7;
string error = 8;
}

9
common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java

@ -163,6 +163,15 @@ public class StringUtils {
return false; return false;
} }
public static boolean equalsAnyIgnoreCase(String string, String... otherStrings) {
for (String otherString : otherStrings) {
if (equalsIgnoreCase(string, otherString)) {
return true;
}
}
return false;
}
public static String substringAfterLast(String str, String sep) { public static String substringAfterLast(String str, String sep) {
return org.apache.commons.lang3.StringUtils.substringAfterLast(str, sep); return org.apache.commons.lang3.StringUtils.substringAfterLast(str, sep);
} }

6
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java

@ -18,7 +18,6 @@ package org.thingsboard.server.common.data.device.profile;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.transport.snmp.SnmpMapping;
import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig; import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig;
import java.util.List; import java.util.List;
@ -45,8 +44,7 @@ public class SnmpDeviceProfileTransportConfiguration implements DeviceProfileTra
private boolean isValid() { private boolean isValid() {
return timeoutMs != null && timeoutMs >= 0 && retries != null && retries >= 0 return timeoutMs != null && timeoutMs >= 0 && retries != null && retries >= 0
&& communicationConfigs != null && communicationConfigs != null
&& communicationConfigs.stream().allMatch(config -> config != null && config.isValid()) && communicationConfigs.stream().allMatch(config -> config != null && config.isValid());
&& communicationConfigs.stream().flatMap(config -> config.getAllMappings().stream()).map(SnmpMapping::getOid)
.distinct().count() == communicationConfigs.stream().mapToInt(config -> config.getAllMappings().size()).sum();
} }
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/importing/csv/BulkImportColumnType.java

@ -43,6 +43,10 @@ public enum BulkImportColumnType {
LWM2M_SERVER_SECURITY_MODE("securityMode", LwM2MSecurityMode.NO_SEC.name()), LWM2M_SERVER_SECURITY_MODE("securityMode", LwM2MSecurityMode.NO_SEC.name()),
LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID("clientPublicKeyOrId"), LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID("clientPublicKeyOrId"),
LWM2M_SERVER_CLIENT_SECRET_KEY("clientSecretKey"), LWM2M_SERVER_CLIENT_SECRET_KEY("clientSecretKey"),
SNMP_HOST,
SNMP_PORT,
SNMP_VERSION,
SNMP_COMMUNITY_STRING,
IS_GATEWAY, IS_GATEWAY,
DESCRIPTION, DESCRIPTION,
ROUTING_KEY, ROUTING_KEY,

18
common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpCommunicationSpec.java

@ -15,11 +15,21 @@
*/ */
package org.thingsboard.server.common.data.transport.snmp; package org.thingsboard.server.common.data.transport.snmp;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum SnmpCommunicationSpec { public enum SnmpCommunicationSpec {
TELEMETRY_QUERYING,
CLIENT_ATTRIBUTES_QUERYING, TELEMETRY_QUERYING("telemetryQuerying"),
SHARED_ATTRIBUTES_SETTING, CLIENT_ATTRIBUTES_QUERYING("clientAttributesQuerying"),
SHARED_ATTRIBUTES_SETTING("sharedAttributesSetting"),
TO_DEVICE_RPC_REQUEST("toDeviceRpcRequest"),
TO_SERVER_RPC_REQUEST("toServerRpcRequest");
private final String label;
TO_DEVICE_RPC_REQUEST,
} }

3
common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpMethod.java

@ -17,7 +17,8 @@ package org.thingsboard.server.common.data.transport.snmp;
public enum SnmpMethod { public enum SnmpMethod {
GET(-96), GET(-96),
SET(-93); SET(-93),
TRAP(-89);
// codes taken from org.snmp4j.PDU class // codes taken from org.snmp4j.PDU class
private final int code; private final int code;

3
common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java

@ -37,7 +37,8 @@ import java.util.List;
@Type(value = TelemetryQueryingSnmpCommunicationConfig.class, name = "TELEMETRY_QUERYING"), @Type(value = TelemetryQueryingSnmpCommunicationConfig.class, name = "TELEMETRY_QUERYING"),
@Type(value = ClientAttributesQueryingSnmpCommunicationConfig.class, name = "CLIENT_ATTRIBUTES_QUERYING"), @Type(value = ClientAttributesQueryingSnmpCommunicationConfig.class, name = "CLIENT_ATTRIBUTES_QUERYING"),
@Type(value = SharedAttributesSettingSnmpCommunicationConfig.class, name = "SHARED_ATTRIBUTES_SETTING"), @Type(value = SharedAttributesSettingSnmpCommunicationConfig.class, name = "SHARED_ATTRIBUTES_SETTING"),
@Type(value = ToDeviceRpcRequestSnmpCommunicationConfig.class, name = "TO_DEVICE_RPC_REQUEST") @Type(value = ToDeviceRpcRequestSnmpCommunicationConfig.class, name = "TO_DEVICE_RPC_REQUEST"),
@Type(value = ToServerRpcRequestSnmpCommunicationConfig.class, name = "TO_SERVER_RPC_REQUEST")
}) })
public interface SnmpCommunicationConfig extends Serializable { public interface SnmpCommunicationConfig extends Serializable {

29
common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/ToServerRpcRequestSnmpCommunicationConfig.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.transport.snmp.config;
import org.thingsboard.server.common.data.transport.snmp.SnmpCommunicationSpec;
public class ToServerRpcRequestSnmpCommunicationConfig extends MultipleMappingsSnmpCommunicationConfig {
private static final long serialVersionUID = 4851028734093214L;
@Override
public SnmpCommunicationSpec getSpec() {
return SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST;
}
}

32
application/src/main/java/org/thingsboard/server/utils/TypeCastUtil.java → common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java

@ -13,35 +13,53 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.utils; package org.thingsboard.server.common.data.util;
import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DataType;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Map;
public class TypeCastUtil { public class TypeCastUtil {
private TypeCastUtil() {} private TypeCastUtil() {}
public static Map.Entry<DataType, Object> castValue(String value) { public static Pair<DataType, Object> castValue(String value) {
if (isNumber(value)) { if (isNumber(value)) {
String formattedValue = value.replace(',', '.'); String formattedValue = value.replace(',', '.');
try { try {
BigDecimal bd = new BigDecimal(formattedValue); BigDecimal bd = new BigDecimal(formattedValue);
if (bd.stripTrailingZeros().scale() > 0 || isSimpleDouble(formattedValue)) { if (bd.stripTrailingZeros().scale() > 0 || isSimpleDouble(formattedValue)) {
if (bd.scale() <= 16) { if (bd.scale() <= 16) {
return Map.entry(DataType.DOUBLE, bd.doubleValue()); return Pair.of(DataType.DOUBLE, bd.doubleValue());
} }
} else { } else {
return Map.entry(DataType.LONG, bd.longValueExact()); return Pair.of(DataType.LONG, bd.longValueExact());
} }
} catch (RuntimeException ignored) {} } catch (RuntimeException ignored) {}
} else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { } else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) {
return Map.entry(DataType.BOOLEAN, Boolean.parseBoolean(value)); return Pair.of(DataType.BOOLEAN, Boolean.parseBoolean(value));
}
return Pair.of(DataType.STRING, value);
}
public static Pair<DataType, Number> castToNumber(String value) {
if (isNumber(value)) {
String formattedValue = value.replace(',', '.');
BigDecimal bd = new BigDecimal(formattedValue);
if (bd.stripTrailingZeros().scale() > 0 || isSimpleDouble(formattedValue)) {
if (bd.scale() <= 16) {
return Pair.of(DataType.DOUBLE, bd.doubleValue());
} else {
return Pair.of(DataType.DOUBLE, bd);
}
} else {
return Pair.of(DataType.LONG, bd.longValueExact());
}
} else {
throw new IllegalArgumentException("'" + value + "' can't be parsed as number");
} }
return Map.entry(DataType.STRING, value);
} }
private static boolean isNumber(String value) { private static boolean isNumber(String value) {

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

@ -111,7 +111,7 @@ public class EdgeGrpcClient implements EdgeRpcClient {
.setConnectRequestMsg(ConnectRequestMsg.newBuilder() .setConnectRequestMsg(ConnectRequestMsg.newBuilder()
.setEdgeRoutingKey(edgeKey) .setEdgeRoutingKey(edgeKey)
.setEdgeSecret(edgeSecret) .setEdgeSecret(edgeSecret)
.setEdgeVersion(EdgeVersion.V_3_4_0) .setEdgeVersion(EdgeVersion.V_3_6_0)
.setMaxInboundMessageSize(maxInboundMessageSize) .setMaxInboundMessageSize(maxInboundMessageSize)
.build()) .build())
.build()); .build());

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

@ -34,6 +34,7 @@ enum EdgeVersion {
V_3_3_0 = 0; V_3_3_0 = 0;
V_3_3_3 = 1; V_3_3_3 = 1;
V_3_4_0 = 2; V_3_4_0 = 2;
V_3_6_0 = 3;
} }
/** /**

50
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DateFormattingOptions.java

@ -0,0 +1,50 @@
package org.thingsboard.script.api.tbel;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.StringUtils;
import java.time.format.FormatStyle;
import java.util.TimeZone;
@NoArgsConstructor
@Data
class DateFormattingOptions {
private static final TimeZone DEFAULT_TZ = TimeZone.getDefault();
private String timeZone;
private String dateStyle;
private String timeStyle;
@Getter
private String pattern;
public DateFormattingOptions(String timeZone) {
this.timeZone = timeZone;
}
TimeZone getTimeZone() {
return StringUtils.isNotEmpty(timeZone) ? TimeZone.getTimeZone(timeZone) : TimeZone.getDefault();
}
FormatStyle getDateStyle() {
return getFormatStyle(dateStyle, FormatStyle.SHORT);
}
FormatStyle getTimeStyle() {
return getFormatStyle(timeStyle, FormatStyle.MEDIUM);
}
private static FormatStyle getFormatStyle(String style, FormatStyle defaultStyle) {
if (StringUtils.isNotEmpty(style)) {
try {
return FormatStyle.valueOf(style.toUpperCase());
} catch (IllegalArgumentException e) {
return defaultStyle;
}
} else {
return defaultStyle;
}
}
}

84
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java

@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -15,6 +15,9 @@
*/ */
package org.thingsboard.script.api.tbel; package org.thingsboard.script.api.tbel;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.Instant; import java.time.Instant;
@ -22,13 +25,16 @@ import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.chrono.IsoChronology;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalAccessor;
import java.util.Date; import java.util.Date;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.Locale; import java.util.Locale;
import java.util.TimeZone; import java.util.function.BiFunction;
import java.util.function.Function;
public class TbDate extends Date { public class TbDate extends Date {
@ -77,15 +83,77 @@ public class TbDate extends Date {
return isoDateFormat.get().format(this); return isoDateFormat.get().format(this);
} }
public String toLocaleDateString() {
return toLocaleDateString(null, null);
}
public String toLocaleDateString(String locale) {
return toLocaleDateString(locale, null);
}
public String toLocaleDateString(String localeStr, String optionsStr) {
return toLocaleString(localeStr, optionsStr, (locale, options) -> DateTimeFormatter.ofLocalizedDate(options.getDateStyle()).withLocale(locale));
}
public String toLocaleTimeString() {
return toLocaleTimeString(null, null);
}
public String toLocaleTimeString(String locale) {
return toLocaleTimeString(locale, null);
}
public String toLocaleTimeString(String localeStr, String optionsStr) {
return toLocaleString(localeStr, optionsStr, (locale, options) -> DateTimeFormatter.ofLocalizedTime(options.getTimeStyle()).withLocale(locale));
}
@Override
public String toLocaleString() {
return toLocaleString(null, null);
}
public String toLocaleString(String locale) { public String toLocaleString(String locale) {
DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.forLanguageTag(locale)); return toLocaleString(locale, null);
return formatter.format(this);
} }
public String toLocaleString(String locale, String tz) { public String toLocaleString(String localeStr, String optionsStr) {
DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.forLanguageTag(locale)); return toLocaleString(localeStr, optionsStr, (locale, options) -> {
formatter.setTimeZone(TimeZone.getTimeZone(tz)); String formatPattern =
return formatter.format(this); DateTimeFormatterBuilder.getLocalizedDateTimePattern(
options.getDateStyle(),
options.getTimeStyle(),
IsoChronology.INSTANCE,
locale);
return DateTimeFormatter.ofPattern(formatPattern, locale);
});
}
public String toLocaleString(String localeStr, String optionsStr, BiFunction<Locale, DateFormattingOptions, DateTimeFormatter> formatterBuilder) {
Locale locale = StringUtils.isNotEmpty(localeStr) ? Locale.forLanguageTag(localeStr) : Locale.getDefault();
DateFormattingOptions options = getDateFormattingOptions(optionsStr);
ZonedDateTime zdt = this.toInstant().atZone(options.getTimeZone().toZoneId());
DateTimeFormatter formatter;
if (StringUtils.isNotEmpty(options.getPattern())) {
formatter = new DateTimeFormatterBuilder().appendPattern(options.getPattern()).toFormatter(locale);
} else {
formatter = formatterBuilder.apply(locale, options);
}
return formatter.format(zdt);
}
private static DateFormattingOptions getDateFormattingOptions(String options) {
DateFormattingOptions opt = null;
if (StringUtils.isNotEmpty(options)) {
try {
opt = JacksonUtil.fromString(options, DateFormattingOptions.class);
} catch (IllegalArgumentException iae) {
opt = new DateFormattingOptions(options);
}
}
if (opt == null) {
opt = new DateFormattingOptions();
}
return opt;
} }
public static long now() { public static long now() {

182
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java

@ -21,12 +21,24 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.thingsboard.common.util.JacksonUtil;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.chrono.IsoChronology;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.FormatStyle;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -124,4 +136,174 @@ class TbDateTest {
.startsWith(datePrefix); .startsWith(datePrefix);
} }
@Test
void testToLocaleDateString() {
TbDate d = new TbDate(1693962245000L);
// Depends on time zone, so we just check it works;
Assert.assertNotNull(d.toLocaleDateString());
Assert.assertNotNull(d.toLocaleDateString("en-US"));
Assert.assertEquals("9/5/23", d.toLocaleDateString("en-US", "America/New_York"));
Assert.assertEquals("23. 9. 5.", d.toLocaleDateString("ko-KR", "America/New_York"));
Assert.assertEquals("06.09.23", d.toLocaleDateString( "uk-UA", "Europe/Kiev"));
Assert.assertEquals("5\u200F/9\u200F/2023", d.toLocaleDateString( "ar-EG", "America/New_York"));
Assert.assertEquals("Tuesday, September 5, 2023", d.toLocaleDateString("en-US", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("dateStyle", "full")
.toString()));
Assert.assertEquals("2023년 9월 5일 화요일", d.toLocaleDateString("ko-KR", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("dateStyle", "full")
.toString()));
Assert.assertEquals("середа, 6 вересня 2023 р.", d.toLocaleDateString("uk-UA", JacksonUtil.newObjectNode()
.put("timeZone", "Europe/Kiev")
.put("dateStyle", "full")
.toString()));
Assert.assertEquals("الثلاثاء، 5 سبتمبر 2023", d.toLocaleDateString("ar-EG", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("dateStyle", "full")
.toString()));
Assert.assertEquals("Tuesday 9/5/2023", d.toLocaleDateString("en-US", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "EEEE M/d/yyyy")
.toString()));
Assert.assertEquals("화요일 9/5/2023", d.toLocaleDateString("ko-KR", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "EEEE M/d/yyyy")
.toString()));
Assert.assertEquals("середа 9/6/2023", d.toLocaleDateString("uk-UA", JacksonUtil.newObjectNode()
.put("timeZone", "Europe/Kiev")
.put("pattern", "EEEE M/d/yyyy")
.toString()));
Assert.assertEquals("الثلاثاء 9/5/2023", d.toLocaleDateString("ar-EG", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "EEEE M/d/yyyy")
.toString()));
}
@Test
void testToLocaleTimeString() {
TbDate d = new TbDate(1693962245000L);
// Depends on time zone, so we just check it works;
Assert.assertNotNull(d.toLocaleTimeString());
Assert.assertNotNull(d.toLocaleTimeString("en-US"));
Assert.assertEquals("9:04:05 PM", d.toLocaleTimeString("en-US", "America/New_York"));
Assert.assertEquals("오후 9:04:05", d.toLocaleTimeString("ko-KR", "America/New_York"));
Assert.assertEquals("04:04:05", d.toLocaleTimeString( "uk-UA", "Europe/Kiev"));
Assert.assertEquals("9:04:05 م", d.toLocaleTimeString( "ar-EG", "America/New_York"));
Assert.assertEquals("9:04:05 PM Eastern Daylight Time", d.toLocaleTimeString("en-US", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("timeStyle", "full")
.toString()));
Assert.assertEquals("오후 9시 4분 5초 미 동부 하계 표준시", d.toLocaleTimeString("ko-KR", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("timeStyle", "full")
.toString()));
Assert.assertEquals("04:04:05 за східноєвропейським літнім часом", d.toLocaleTimeString("uk-UA", JacksonUtil.newObjectNode()
.put("timeZone", "Europe/Kiev")
.put("timeStyle", "full")
.toString()));
Assert.assertEquals("9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية", d.toLocaleTimeString("ar-EG", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("timeStyle", "full")
.toString()));
Assert.assertEquals("9:04:05 PM", d.toLocaleTimeString("en-US", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "h:mm:ss a")
.toString()));
Assert.assertEquals("9:04:05 오후", d.toLocaleTimeString("ko-KR", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "h:mm:ss a")
.toString()));
Assert.assertEquals("4:04:05 дп", d.toLocaleTimeString("uk-UA", JacksonUtil.newObjectNode()
.put("timeZone", "Europe/Kiev")
.put("pattern", "h:mm:ss a")
.toString()));
Assert.assertEquals("9:04:05 م", d.toLocaleTimeString("ar-EG", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "h:mm:ss a")
.toString()));
}
@Test
void testToLocaleString() {
TbDate d = new TbDate(1693962245000L);
// Depends on time zone, so we just check it works;
Assert.assertNotNull(d.toLocaleString());
Assert.assertNotNull(d.toLocaleString("en-US"));
Assert.assertEquals("9/5/23, 9:04:05 PM", d.toLocaleString("en-US", "America/New_York"));
Assert.assertEquals("23. 9. 5. 오후 9:04:05", d.toLocaleString("ko-KR", "America/New_York"));
Assert.assertEquals("06.09.23, 04:04:05", d.toLocaleString( "uk-UA", "Europe/Kiev"));
Assert.assertEquals("5\u200F/9\u200F/2023 9:04:05 م", d.toLocaleString( "ar-EG", "America/New_York"));
Assert.assertEquals("Tuesday, September 5, 2023 at 9:04:05 PM Eastern Daylight Time", d.toLocaleString("en-US", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("dateStyle", "full")
.put("timeStyle", "full")
.toString()));
Assert.assertEquals("2023년 9월 5일 화요일 오후 9시 4분 5초 미 동부 하계 표준시", d.toLocaleString("ko-KR", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("dateStyle", "full")
.put("timeStyle", "full")
.toString()));
Assert.assertEquals("середа, 6 вересня 2023 р. о 04:04:05 за східноєвропейським літнім часом", d.toLocaleString("uk-UA", JacksonUtil.newObjectNode()
.put("timeZone", "Europe/Kiev")
.put("dateStyle", "full")
.put("timeStyle", "full")
.toString()));
Assert.assertEquals("الثلاثاء، 5 سبتمبر 2023 9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية", d.toLocaleString("ar-EG", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("dateStyle", "full")
.put("timeStyle", "full")
.toString()));
Assert.assertEquals("9/5/2023, 9:04:05 PM", d.toLocaleString("en-US", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "M/d/yyyy, h:mm:ss a")
.toString()));
Assert.assertEquals("9/5/2023, 9:04:05 오후", d.toLocaleString("ko-KR", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "M/d/yyyy, h:mm:ss a")
.toString()));
Assert.assertEquals("9/6/2023, 4:04:05 дп", d.toLocaleString("uk-UA", JacksonUtil.newObjectNode()
.put("timeZone", "Europe/Kiev")
.put("pattern", "M/d/yyyy, h:mm:ss a")
.toString()));
Assert.assertEquals("9/5/2023, 9:04:05 م", d.toLocaleString("ar-EG", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "M/d/yyyy, h:mm:ss a")
.toString()));
}
private static String toLocalString(TbDate d, Locale locale, ZoneId tz) {
LocalDateTime ldt = d.toInstant().atZone(tz).toLocalDateTime();
// new DateTimeFormatterBuilder().appendPattern(pattern).toFormatter(locale)
String formatPattern =
DateTimeFormatterBuilder.getLocalizedDateTimePattern(
FormatStyle.SHORT,
FormatStyle.MEDIUM,
IsoChronology.INSTANCE,
locale);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatPattern, locale);
return formatter.format(ldt);
}
private static String toLocalString2(TbDate d, Locale locale, ZoneId tz) {
DateFormat dateFormat = SimpleDateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, locale);
dateFormat.setTimeZone(TimeZone.getTimeZone(tz));
return dateFormat.format(d);
}
} }

74
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfigu
import org.thingsboard.server.common.data.device.profile.SnmpDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.SnmpDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.transport.DeviceUpdatedEvent; import org.thingsboard.server.common.transport.DeviceUpdatedEvent;
@ -113,19 +114,25 @@ public class SnmpTransportContext extends TransportContext {
SnmpDeviceProfileTransportConfiguration profileTransportConfiguration = (SnmpDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration(); SnmpDeviceProfileTransportConfiguration profileTransportConfiguration = (SnmpDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration();
SnmpDeviceTransportConfiguration deviceTransportConfiguration = (SnmpDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration(); SnmpDeviceTransportConfiguration deviceTransportConfiguration = (SnmpDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration();
DeviceSessionContext deviceSessionContext; DeviceSessionContext sessionContext;
try { try {
deviceSessionContext = new DeviceSessionContext( sessionContext = DeviceSessionContext.builder()
device, deviceProfile, credentials.getCredentialsId(), .tenantId(deviceProfile.getTenantId())
profileTransportConfiguration, deviceTransportConfiguration, this .device(device)
); .deviceProfile(deviceProfile)
registerSessionMsgListener(deviceSessionContext); .token(credentials.getCredentialsId())
.profileTransportConfiguration(profileTransportConfiguration)
.deviceTransportConfiguration(deviceTransportConfiguration)
.snmpTransportContext(this)
.build();
registerSessionMsgListener(sessionContext);
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to establish session for SNMP device {}: {}", device.getId(), e.toString()); log.error("Failed to establish session for SNMP device {}: {}", device.getId(), e.toString());
transportService.errorEvent(device.getTenantId(), device.getId(), "sessionEstablishing", e);
return; return;
} }
sessions.put(device.getId(), deviceSessionContext); sessions.put(device.getId(), sessionContext);
snmpTransportService.createQueryingTasks(deviceSessionContext); snmpTransportService.createQueryingTasks(sessionContext);
log.info("Established SNMP device session for device {}", device.getId()); log.info("Established SNMP device session for device {}", device.getId());
} }
@ -148,14 +155,17 @@ public class SnmpTransportContext extends TransportContext {
sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration); sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration);
snmpTransportService.cancelQueryingTasks(sessionContext); snmpTransportService.cancelQueryingTasks(sessionContext);
snmpTransportService.createQueryingTasks(sessionContext); snmpTransportService.createQueryingTasks(sessionContext);
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null);
} else if (!newDeviceTransportConfiguration.equals(sessionContext.getDeviceTransportConfiguration())) { } else if (!newDeviceTransportConfiguration.equals(sessionContext.getDeviceTransportConfiguration())) {
sessionContext.setDeviceTransportConfiguration(newDeviceTransportConfiguration); sessionContext.setDeviceTransportConfiguration(newDeviceTransportConfiguration);
sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration); sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration);
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null);
} else { } else {
log.trace("Configuration of the device {} was not updated", device); log.trace("Configuration of the device {} was not updated", device);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to update session for SNMP device {}: {}", sessionContext.getDeviceId(), e.getMessage()); log.error("Failed to update session for SNMP device {}: {}", sessionContext.getDeviceId(), e.getMessage());
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, false, e);
destroyDeviceSession(sessionContext); destroyDeviceSession(sessionContext);
} }
} }
@ -168,43 +178,58 @@ public class SnmpTransportContext extends TransportContext {
transportService.deregisterSession(sessionContext.getSessionInfo()); transportService.deregisterSession(sessionContext.getSessionInfo());
snmpTransportService.cancelQueryingTasks(sessionContext); snmpTransportService.cancelQueryingTasks(sessionContext);
sessions.remove(sessionContext.getDeviceId()); sessions.remove(sessionContext.getDeviceId());
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.STOPPED, true, null);
log.trace("Unregistered and removed session"); log.trace("Unregistered and removed session");
} }
private void registerSessionMsgListener(DeviceSessionContext deviceSessionContext) { private void registerSessionMsgListener(DeviceSessionContext sessionContext) {
transportService.process(DeviceTransportType.SNMP, transportService.process(DeviceTransportType.SNMP,
TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceSessionContext.getToken()).build(), TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(sessionContext.getToken()).build(),
new TransportServiceCallback<>() { new TransportServiceCallback<>() {
@Override @Override
public void onSuccess(ValidateDeviceCredentialsResponse msg) { public void onSuccess(ValidateDeviceCredentialsResponse msg) {
if (msg.hasDeviceInfo()) { if (msg.hasDeviceInfo()) {
SessionInfoProto sessionInfo = SessionInfoCreator.create( registerTransportSession(sessionContext, msg);
msg, SnmpTransportContext.this, UUID.randomUUID() sessionContext.setSessionTimeoutHandler(() -> {
); registerTransportSession(sessionContext, msg);
});
transportService.registerAsyncSession(sessionInfo, deviceSessionContext); transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.STARTED, true, null);
transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build(), TransportServiceCallback.EMPTY);
transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.newBuilder().build(), TransportServiceCallback.EMPTY);
deviceSessionContext.setSessionInfo(sessionInfo);
deviceSessionContext.setDeviceInfo(msg.getDeviceInfo());
deviceSessionContext.setConnected(true);
} else { } else {
log.warn("[{}] Failed to process device auth", deviceSessionContext.getDeviceId()); log.warn("[{}] Failed to process device auth", sessionContext.getDeviceId());
} }
} }
@Override @Override
public void onError(Throwable e) { public void onError(Throwable e) {
log.warn("[{}] Failed to process device auth: {}", deviceSessionContext.getDeviceId(), e); log.warn("[{}] Failed to process device auth: {}", sessionContext.getDeviceId(), e);
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.STARTED, false, e);
} }
}); });
} }
private void registerTransportSession(DeviceSessionContext deviceSessionContext, ValidateDeviceCredentialsResponse msg) {
SessionInfoProto sessionInfo = SessionInfoCreator.create(
msg, SnmpTransportContext.this, UUID.randomUUID()
);
log.debug("Registering transport session: {}", sessionInfo);
transportService.registerAsyncSession(sessionInfo, deviceSessionContext);
transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder()
.setSessionType(TransportProtos.SessionType.ASYNC)
.build(), TransportServiceCallback.EMPTY);
transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.newBuilder()
.setSessionType(TransportProtos.SessionType.ASYNC)
.build(), TransportServiceCallback.EMPTY);
deviceSessionContext.setSessionInfo(sessionInfo);
deviceSessionContext.setDeviceInfo(msg.getDeviceInfo());
deviceSessionContext.setConnected(true);
}
@EventListener(DeviceUpdatedEvent.class) @EventListener(DeviceUpdatedEvent.class)
public void onDeviceUpdatedOrCreated(DeviceUpdatedEvent deviceUpdatedEvent) { public void onDeviceUpdatedOrCreated(DeviceUpdatedEvent deviceUpdatedEvent) {
Device device = deviceUpdatedEvent.getDevice(); Device device = deviceUpdatedEvent.getDevice();
log.trace("Got creating or updating device event for device {}", device); log.debug("Got creating or updating device event for device {}", device);
DeviceTransportType transportType = Optional.ofNullable(device.getDeviceData().getTransportConfiguration()) DeviceTransportType transportType = Optional.ofNullable(device.getDeviceData().getTransportConfiguration())
.map(DeviceTransportConfiguration::getType) .map(DeviceTransportConfiguration::getType)
.orElse(null); .orElse(null);
@ -238,6 +263,7 @@ public class SnmpTransportContext extends TransportContext {
} }
public void onDeviceProfileUpdated(DeviceProfile deviceProfile, DeviceSessionContext sessionContext) { public void onDeviceProfileUpdated(DeviceProfile deviceProfile, DeviceSessionContext sessionContext) {
log.debug("Handling device profile {} update event for device {}", deviceProfile.getId(), sessionContext.getDeviceId());
updateDeviceSession(sessionContext, sessionContext.getDevice(), deviceProfile); updateDeviceSession(sessionContext, sessionContext.getDevice(), deviceProfile);
} }

97
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/PduService.java

@ -15,7 +15,9 @@
*/ */
package org.thingsboard.server.transport.snmp.service; package org.thingsboard.server.transport.snmp.service;
import com.google.common.collect.Lists;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.snmp4j.PDU; import org.snmp4j.PDU;
import org.snmp4j.ScopedPDU; import org.snmp4j.ScopedPDU;
@ -25,43 +27,61 @@ import org.snmp4j.smi.OID;
import org.snmp4j.smi.OctetString; import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.Variable; import org.snmp4j.smi.Variable;
import org.snmp4j.smi.VariableBinding; import org.snmp4j.smi.VariableBinding;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration;
import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.transport.snmp.SnmpMapping; import org.thingsboard.server.common.data.transport.snmp.SnmpMapping;
import org.thingsboard.server.common.data.transport.snmp.SnmpMethod; import org.thingsboard.server.common.data.transport.snmp.SnmpMethod;
import org.thingsboard.server.common.data.transport.snmp.SnmpProtocolVersion; import org.thingsboard.server.common.data.transport.snmp.SnmpProtocolVersion;
import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig; import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig;
import org.thingsboard.server.common.data.util.TypeCastUtil;
import org.thingsboard.server.queue.util.TbSnmpTransportComponent; import org.thingsboard.server.queue.util.TbSnmpTransportComponent;
import org.thingsboard.server.transport.snmp.session.DeviceSessionContext; import org.thingsboard.server.transport.snmp.session.DeviceSessionContext;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream;
@TbSnmpTransportComponent @TbSnmpTransportComponent
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor
public class PduService { public class PduService {
public PDU createPdu(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig, Map<String, String> values) {
PDU pdu = setUpPdu(sessionContext);
pdu.setType(communicationConfig.getMethod().getCode()); @Value("${transport.snmp.max_request_oids:100}")
pdu.addAll(communicationConfig.getAllMappings().stream() private int maxRequestOids;
.filter(mapping -> values.isEmpty() || values.containsKey(mapping.getKey()))
.map(mapping -> Optional.ofNullable(values.get(mapping.getKey())) @Value("${transport.snmp.response.ignore_type_cast_errors:false}")
.map(value -> { private boolean ignoreTypeCastErrors;
Variable variable = toSnmpVariable(value, mapping.getDataType());
return new VariableBinding(new OID(mapping.getOid()), variable); public List<PDU> createPdus(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig, Map<String, String> values) {
}) List<PDU> pdus = new ArrayList<>();
.orElseGet(() -> new VariableBinding(new OID(mapping.getOid())))) List<SnmpMapping> allMappings = communicationConfig.getAllMappings();
.collect(Collectors.toList()));
for (List<SnmpMapping> mappings : Lists.partition(allMappings, maxRequestOids)) {
PDU pdu = setUpPdu(sessionContext);
pdu.setType(communicationConfig.getMethod().getCode());
pdu.addAll(mappings.stream()
.filter(mapping -> values.isEmpty() || values.containsKey(mapping.getKey()))
.map(mapping -> Optional.ofNullable(values.get(mapping.getKey()))
.map(value -> {
Variable variable = toSnmpVariable(value, mapping.getDataType());
return new VariableBinding(new OID(mapping.getOid()), variable);
})
.orElseGet(() -> new VariableBinding(new OID(mapping.getOid()))))
.collect(Collectors.toList()));
if (pdu.size() > 0) {
pdus.add(pdu);
}
}
return pdu; return pdus;
} }
public PDU createSingleVariablePdu(DeviceSessionContext sessionContext, SnmpMethod snmpMethod, String oid, String value, DataType dataType) { public PDU createSingleVariablePdu(DeviceSessionContext sessionContext, SnmpMethod snmpMethod, String oid, String value, DataType dataType) {
@ -116,8 +136,8 @@ public class PduService {
} }
public JsonObject processPdu(PDU pdu, List<SnmpMapping> responseMappings) { public JsonObject processPdus(List<PDU> pdus, List<SnmpMapping> responseMappings) {
Map<OID, String> values = processPdu(pdu); Map<OID, String> values = processPdus(pdus);
Map<OID, SnmpMapping> mappings = new HashMap<>(); Map<OID, SnmpMapping> mappings = new HashMap<>();
if (responseMappings != null) { if (responseMappings != null) {
@ -143,29 +163,40 @@ public class PduService {
return data; return data;
} }
public Map<OID, String> processPdu(PDU pdu) { public Map<OID, String> processPdus(List<PDU> pdus) {
return IntStream.range(0, pdu.size()) return pdus.stream()
.mapToObj(pdu::get) .flatMap(pdu -> pdu.getVariableBindings().stream())
.filter(Objects::nonNull) .filter(Objects::nonNull)
.filter(variableBinding -> !(variableBinding.getVariable() instanceof Null)) .filter(variableBinding -> !(variableBinding.getVariable() instanceof Null))
.collect(Collectors.toMap(VariableBinding::getOid, VariableBinding::toValueString)); .collect(Collectors.toMap(VariableBinding::getOid, VariableBinding::toValueString));
} }
public void processValue(String key, DataType dataType, String value, JsonObject result) { public void processValue(String key, DataType dataType, String value, JsonObject result) {
switch (dataType) { try {
case LONG: switch (dataType) {
result.addProperty(key, Long.parseLong(value)); case STRING:
break; case JSON:
case BOOLEAN: result.addProperty(key, value);
result.addProperty(key, Boolean.parseBoolean(value)); break;
break; case LONG:
case DOUBLE: case DOUBLE:
result.addProperty(key, Double.parseDouble(value)); result.addProperty(key, TypeCastUtil.castToNumber(value).getValue());
break; break;
case STRING: case BOOLEAN:
case JSON: if (StringUtils.equalsAnyIgnoreCase(value, "true", "false")) {
default: result.addProperty(key, Boolean.parseBoolean(value));
result.addProperty(key, value); } else {
throw new IllegalArgumentException("Can't parse '" + value + "' as boolean");
}
break;
}
} catch (IllegalArgumentException e) {
if (ignoreTypeCastErrors) {
log.debug("Ignoring value '{}' for key '{}' because of data type mismatch ({} required)", value, key, dataType);
} else {
throw e;
}
} }
} }
} }

221
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java

@ -17,10 +17,13 @@ package org.thingsboard.server.transport.snmp.service;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.snmp4j.CommandResponder;
import org.snmp4j.CommandResponderEvent;
import org.snmp4j.PDU; import org.snmp4j.PDU;
import org.snmp4j.Snmp; import org.snmp4j.Snmp;
import org.snmp4j.TransportMapping; import org.snmp4j.TransportMapping;
@ -29,10 +32,15 @@ import org.snmp4j.mp.MPv3;
import org.snmp4j.security.SecurityModels; import org.snmp4j.security.SecurityModels;
import org.snmp4j.security.SecurityProtocols; import org.snmp4j.security.SecurityProtocols;
import org.snmp4j.security.USM; import org.snmp4j.security.USM;
import org.snmp4j.smi.Address;
import org.snmp4j.smi.OctetString; import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.TcpAddress;
import org.snmp4j.smi.UdpAddress;
import org.snmp4j.transport.DefaultTcpTransportMapping; import org.snmp4j.transport.DefaultTcpTransportMapping;
import org.snmp4j.transport.DefaultUdpTransportMapping; import org.snmp4j.transport.DefaultUdpTransportMapping;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
@ -45,15 +53,16 @@ import org.thingsboard.server.common.data.transport.snmp.SnmpMethod;
import org.thingsboard.server.common.data.transport.snmp.config.RepeatingQueryingSnmpCommunicationConfig; import org.thingsboard.server.common.data.transport.snmp.config.RepeatingQueryingSnmpCommunicationConfig;
import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig; import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig;
import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.util.TbSnmpTransportComponent; import org.thingsboard.server.queue.util.TbSnmpTransportComponent;
import org.thingsboard.server.transport.snmp.SnmpTransportContext;
import org.thingsboard.server.transport.snmp.session.DeviceSessionContext; import org.thingsboard.server.transport.snmp.session.DeviceSessionContext;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.EnumMap; import java.util.EnumMap;
@ -71,9 +80,11 @@ import java.util.stream.Collectors;
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class SnmpTransportService implements TbTransportService { public class SnmpTransportService implements TbTransportService, CommandResponder {
private final TransportService transportService; private final TransportService transportService;
private final PduService pduService; private final PduService pduService;
@Autowired @Lazy
private SnmpTransportContext transportContext;
@Getter @Getter
private Snmp snmp; private Snmp snmp;
@ -83,6 +94,8 @@ public class SnmpTransportService implements TbTransportService {
private final Map<SnmpCommunicationSpec, ResponseDataMapper> responseDataMappers = new EnumMap<>(SnmpCommunicationSpec.class); private final Map<SnmpCommunicationSpec, ResponseDataMapper> responseDataMappers = new EnumMap<>(SnmpCommunicationSpec.class);
private final Map<SnmpCommunicationSpec, ResponseProcessor> responseProcessors = new EnumMap<>(SnmpCommunicationSpec.class); private final Map<SnmpCommunicationSpec, ResponseProcessor> responseProcessors = new EnumMap<>(SnmpCommunicationSpec.class);
@Value("${transport.snmp.bind_port:1620}")
private Integer snmpBindPort;
@Value("${transport.snmp.response_processing.parallelism_level}") @Value("${transport.snmp.response_processing.parallelism_level}")
private Integer responseProcessingParallelismLevel; private Integer responseProcessingParallelismLevel;
@Value("${transport.snmp.underlying_protocol}") @Value("${transport.snmp.underlying_protocol}")
@ -114,15 +127,16 @@ public class SnmpTransportService implements TbTransportService {
TransportMapping<?> transportMapping; TransportMapping<?> transportMapping;
switch (snmpUnderlyingProtocol) { switch (snmpUnderlyingProtocol) {
case "udp": case "udp":
transportMapping = new DefaultUdpTransportMapping(); transportMapping = new DefaultUdpTransportMapping(new UdpAddress(snmpBindPort));
break; break;
case "tcp": case "tcp":
transportMapping = new DefaultTcpTransportMapping(); transportMapping = new DefaultTcpTransportMapping(new TcpAddress(snmpBindPort));
break; break;
default: default:
throw new IllegalArgumentException("Underlying protocol " + snmpUnderlyingProtocol + " for SNMP is not supported"); throw new IllegalArgumentException("Underlying protocol " + snmpUnderlyingProtocol + " for SNMP is not supported");
} }
snmp = new Snmp(transportMapping); snmp = new Snmp(transportMapping);
snmp.addNotificationListener(transportMapping, transportMapping.getListenAddress(), this);
snmp.listen(); snmp.listen();
USM usm = new USM(SecurityProtocols.getInstance(), new OctetString(MPv3.createLocalEngineID()), 0); USM usm = new USM(SecurityProtocols.getInstance(), new OctetString(MPv3.createLocalEngineID()), 0);
@ -143,6 +157,7 @@ public class SnmpTransportService implements TbTransportService {
} }
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to send SNMP request for device {}: {}", sessionContext.getDeviceId(), e.toString()); log.error("Failed to send SNMP request for device {}: {}", sessionContext.getDeviceId(), e.toString());
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), config.getSpec().getLabel(), e);
} }
}, queryingFrequency, queryingFrequency, TimeUnit.MILLISECONDS); }, queryingFrequency, queryingFrequency, TimeUnit.MILLISECONDS);
}) })
@ -161,18 +176,24 @@ public class SnmpTransportService implements TbTransportService {
} }
private void sendRequest(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig, Map<String, String> values) { private void sendRequest(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig, Map<String, String> values) {
PDU request = pduService.createPdu(sessionContext, communicationConfig, values); List<PDU> request = pduService.createPdus(sessionContext, communicationConfig, values);
RequestInfo requestInfo = new RequestInfo(communicationConfig.getSpec(), communicationConfig.getAllMappings()); RequestContext requestContext = RequestContext.builder()
sendRequest(sessionContext, request, requestInfo); .communicationSpec(communicationConfig.getSpec())
.method(communicationConfig.getMethod())
.responseMappings(communicationConfig.getAllMappings())
.requestSize(request.size())
.build();
sendRequest(sessionContext, request, requestContext);
} }
private void sendRequest(DeviceSessionContext sessionContext, PDU request, RequestInfo requestInfo) { private void sendRequest(DeviceSessionContext sessionContext, List<PDU> request, RequestContext requestContext) {
if (request.size() > 0) { for (PDU pdu : request) {
log.trace("Executing SNMP request for device {}. Variables bindings: {}", sessionContext.getDeviceId(), request.getVariableBindings()); log.debug("Executing SNMP request for device {} with {} variable bindings", sessionContext.getDeviceId(), pdu.size());
try { try {
snmp.send(request, sessionContext.getTarget(), requestInfo, sessionContext); snmp.send(pdu, sessionContext.getTarget(), requestContext, sessionContext);
} catch (IOException e) { } catch (IOException e) {
log.error("Failed to send SNMP request to device {}: {}", sessionContext.getDeviceId(), e.toString()); log.error("Failed to send SNMP request to device {}: {}", sessionContext.getDeviceId(), e.toString());
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), e);
} }
} }
} }
@ -215,51 +236,135 @@ public class SnmpTransportService implements TbTransportService {
DataType dataType = snmpMapping.getDataType(); DataType dataType = snmpMapping.getDataType();
PDU request = pduService.createSingleVariablePdu(sessionContext, snmpMethod, oid, value, dataType); PDU request = pduService.createSingleVariablePdu(sessionContext, snmpMethod, oid, value, dataType);
RequestInfo requestInfo = new RequestInfo(toDeviceRpcRequestMsg.getRequestId(), communicationConfig.getSpec(), communicationConfig.getAllMappings()); RequestContext requestContext = RequestContext.builder()
sendRequest(sessionContext, request, requestInfo); .requestId(toDeviceRpcRequestMsg.getRequestId())
.communicationSpec(communicationConfig.getSpec())
.method(snmpMethod)
.responseMappings(communicationConfig.getAllMappings())
.requestSize(1)
.build();
sendRequest(sessionContext, List.of(request), requestContext);
} }
public void processResponseEvent(DeviceSessionContext sessionContext, ResponseEvent event) { public void processResponseEvent(DeviceSessionContext sessionContext, ResponseEvent event) {
((Snmp) event.getSource()).cancel(event.getRequest(), sessionContext); ((Snmp) event.getSource()).cancel(event.getRequest(), sessionContext);
RequestContext requestContext = (RequestContext) event.getUserObject();
if (event.getError() != null) { if (event.getError() != null) {
log.warn("SNMP response error: {}", event.getError().toString()); log.warn("SNMP response error: {}", event.getError().toString());
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), new RuntimeException(event.getError()));
return; return;
} }
PDU response = event.getResponse(); PDU responsePdu = event.getResponse();
if (response == null) { if (log.isTraceEnabled()) {
log.debug("No response from SNMP device {}, requestId: {}", sessionContext.getDeviceId(), event.getRequest().getRequestID()); log.trace("Received PDU for device {}: {}", sessionContext.getDeviceId(), responsePdu);
}
List<PDU> response;
if (requestContext.getRequestSize() == 1) {
if (responsePdu == null) {
log.debug("No response from SNMP device {}, requestId: {}", sessionContext.getDeviceId(), event.getRequest().getRequestID());
if (requestContext.getMethod() == SnmpMethod.GET) {
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), new RuntimeException("No response from device"));
}
return;
}
response = List.of(responsePdu);
} else {
List<PDU> responseParts = requestContext.getResponseParts();
responseParts.add(responsePdu);
if (responseParts.size() == requestContext.getRequestSize()) {
response = new ArrayList<>();
for (PDU responsePart : responseParts) {
if (responsePart != null) {
response.add(responsePart);
}
}
log.debug("All response parts are collected for request to device {}", sessionContext.getDeviceId());
} else {
log.trace("Awaiting other response parts for request to device {}", sessionContext.getDeviceId());
return;
}
}
responseProcessingExecutor.execute(() -> {
try {
processResponse(sessionContext, response, requestContext);
} catch (Exception e) {
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), e);
}
});
}
/*
* SNMP notifications handler
*
* TODO: add check for host uniqueness when saving device (for backward compatibility - only for the ones using from-device RPC requests)
*
* NOTE: SNMP TRAPs support won't work properly when there is more than one SNMP transport,
* due to load-balancing of requests from devices: session might not be on this instance
* */
@Override
public void processPdu(CommandResponderEvent event) {
Address sourceAddress = event.getPeerAddress();
DeviceSessionContext sessionContext = transportContext.getSessions().stream()
.filter(session -> session.getTarget().getAddress().equals(sourceAddress))
.findFirst().orElse(null);
if (sessionContext == null) {
log.warn("SNMP TRAP processing failed: couldn't find device session for address {}", sourceAddress);
return; return;
} }
RequestInfo requestInfo = (RequestInfo) event.getUserObject(); try {
processIncomingTrap(sessionContext, event);
} catch (Throwable e) {
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(),
SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST.getLabel(), e);
}
}
private void processIncomingTrap(DeviceSessionContext sessionContext, CommandResponderEvent event) {
PDU pdu = event.getPDU();
if (pdu == null) {
log.warn("Got empty trap from device {}", sessionContext.getDeviceId());
throw new IllegalArgumentException("Received TRAP with no data");
}
log.debug("Processing SNMP trap from device {} (PDU: {}}", sessionContext.getDeviceId(), pdu);
SnmpCommunicationConfig communicationConfig = sessionContext.getProfileTransportConfiguration().getCommunicationConfigs().stream()
.filter(config -> config.getSpec() == SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST).findFirst()
.orElseThrow(() -> new IllegalArgumentException("No config found for to-server RPC requests"));
RequestContext requestContext = RequestContext.builder()
.communicationSpec(communicationConfig.getSpec())
.responseMappings(communicationConfig.getAllMappings())
.method(SnmpMethod.TRAP)
.build();
responseProcessingExecutor.execute(() -> { responseProcessingExecutor.execute(() -> {
processResponse(sessionContext, response, requestInfo); processResponse(sessionContext, List.of(pdu), requestContext);
}); });
} }
private void processResponse(DeviceSessionContext sessionContext, PDU response, RequestInfo requestInfo) { private void processResponse(DeviceSessionContext sessionContext, List<PDU> response, RequestContext requestContext) {
ResponseProcessor responseProcessor = responseProcessors.get(requestInfo.getCommunicationSpec()); ResponseProcessor responseProcessor = responseProcessors.get(requestContext.getCommunicationSpec());
if (responseProcessor == null) return; if (responseProcessor == null) return;
JsonObject responseData = responseDataMappers.get(requestInfo.getCommunicationSpec()).map(response, requestInfo); JsonObject responseData = responseDataMappers.get(requestContext.getCommunicationSpec()).map(response, requestContext);
if (responseData.size() == 0) {
if (responseData.entrySet().isEmpty()) { log.warn("No values in the SNMP response for device {}", sessionContext.getDeviceId());
log.debug("No values is the SNMP response for device {}. Request id: {}", sessionContext.getDeviceId(), response.getRequestID()); throw new IllegalArgumentException("No values in the response");
return;
} }
responseProcessor.process(responseData, requestInfo, sessionContext); responseProcessor.process(responseData, requestContext, sessionContext);
reportActivity(sessionContext.getSessionInfo()); reportActivity(sessionContext.getSessionInfo());
} }
private void configureResponseDataMappers() { private void configureResponseDataMappers() {
responseDataMappers.put(SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST, (pdu, requestInfo) -> { responseDataMappers.put(SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST, (pdus, requestContext) -> {
JsonObject responseData = new JsonObject(); JsonObject responseData = new JsonObject();
pduService.processPdu(pdu).forEach((oid, value) -> { pduService.processPdus(pdus).forEach((oid, value) -> {
requestInfo.getResponseMappings().stream() requestContext.getResponseMappings().stream()
.filter(snmpMapping -> snmpMapping.getOid().equals(oid.toDottedString())) .filter(snmpMapping -> snmpMapping.getOid().equals(oid.toDottedString()))
.findFirst() .findFirst()
.ifPresent(snmpMapping -> { .ifPresent(snmpMapping -> {
@ -269,8 +374,8 @@ public class SnmpTransportService implements TbTransportService {
return responseData; return responseData;
}); });
ResponseDataMapper defaultResponseDataMapper = (pdu, requestInfo) -> { ResponseDataMapper defaultResponseDataMapper = (pdus, requestContext) -> {
return pduService.processPdu(pdu, requestInfo.getResponseMappings()); return pduService.processPdus(pdus, requestContext.getResponseMappings());
}; };
Arrays.stream(SnmpCommunicationSpec.values()) Arrays.stream(SnmpCommunicationSpec.values())
.forEach(communicationSpec -> { .forEach(communicationSpec -> {
@ -279,34 +384,39 @@ public class SnmpTransportService implements TbTransportService {
} }
private void configureResponseProcessors() { private void configureResponseProcessors() {
responseProcessors.put(SnmpCommunicationSpec.TELEMETRY_QUERYING, (responseData, requestInfo, sessionContext) -> { responseProcessors.put(SnmpCommunicationSpec.TELEMETRY_QUERYING, (responseData, requestContext, sessionContext) -> {
TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(responseData); TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(responseData);
transportService.process(sessionContext.getSessionInfo(), postTelemetryMsg, null); transportService.process(sessionContext.getSessionInfo(), postTelemetryMsg, null);
log.debug("Posted telemetry for SNMP device {}: {}", sessionContext.getDeviceId(), responseData); log.debug("Posted telemetry for SNMP device {}: {}", sessionContext.getDeviceId(), responseData);
}); });
responseProcessors.put(SnmpCommunicationSpec.CLIENT_ATTRIBUTES_QUERYING, (responseData, requestInfo, sessionContext) -> { responseProcessors.put(SnmpCommunicationSpec.CLIENT_ATTRIBUTES_QUERYING, (responseData, requestContext, sessionContext) -> {
TransportProtos.PostAttributeMsg postAttributesMsg = JsonConverter.convertToAttributesProto(responseData); TransportProtos.PostAttributeMsg postAttributesMsg = JsonConverter.convertToAttributesProto(responseData);
transportService.process(sessionContext.getSessionInfo(), postAttributesMsg, null); transportService.process(sessionContext.getSessionInfo(), postAttributesMsg, null);
log.debug("Posted attributes for SNMP device {}: {}", sessionContext.getDeviceId(), responseData); log.debug("Posted attributes for SNMP device {}: {}", sessionContext.getDeviceId(), responseData);
}); });
responseProcessors.put(SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST, (responseData, requestInfo, sessionContext) -> { responseProcessors.put(SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST, (responseData, requestContext, sessionContext) -> {
TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder()
.setRequestId(requestInfo.getRequestId()) .setRequestId(requestContext.getRequestId())
.setPayload(JsonConverter.toJson(responseData)) .setPayload(JsonConverter.toJson(responseData))
.build(); .build();
transportService.process(sessionContext.getSessionInfo(), rpcResponseMsg, null); transportService.process(sessionContext.getSessionInfo(), rpcResponseMsg, null);
log.debug("Posted RPC response {} for device {}", responseData, sessionContext.getDeviceId()); log.debug("Posted RPC response {} for device {}", responseData, sessionContext.getDeviceId());
}); });
responseProcessors.put(SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST, (responseData, requestContext, sessionContext) -> {
TransportProtos.ToServerRpcRequestMsg toServerRpcRequestMsg = TransportProtos.ToServerRpcRequestMsg.newBuilder()
.setRequestId(0)
.setMethodName(requestContext.getMethod().name())
.setParams(JsonConverter.toJson(responseData))
.build();
transportService.process(sessionContext.getSessionInfo(), toServerRpcRequestMsg, null);
});
} }
private void reportActivity(TransportProtos.SessionInfoProto sessionInfo) { private void reportActivity(TransportProtos.SessionInfoProto sessionInfo) {
transportService.process(sessionInfo, TransportProtos.SubscriptionInfoProto.newBuilder() transportService.reportActivity(sessionInfo);
.setAttributeSubscription(true)
.setRpcSubscription(true)
.setLastActivityTime(System.currentTimeMillis())
.build(), TransportServiceCallback.EMPTY);
} }
@ -335,29 +445,34 @@ public class SnmpTransportService implements TbTransportService {
} }
@Data @Data
private static class RequestInfo { private static class RequestContext {
private Integer requestId; private final Integer requestId;
private SnmpCommunicationSpec communicationSpec; private final SnmpCommunicationSpec communicationSpec;
private List<SnmpMapping> responseMappings; private final SnmpMethod method;
private final List<SnmpMapping> responseMappings;
public RequestInfo(Integer requestId, SnmpCommunicationSpec communicationSpec, List<SnmpMapping> responseMappings) { private final int requestSize;
this.requestId = requestId; private List<PDU> responseParts;
this.communicationSpec = communicationSpec;
this.responseMappings = responseMappings;
}
public RequestInfo(SnmpCommunicationSpec communicationSpec, List<SnmpMapping> responseMappings) { @Builder
public RequestContext(Integer requestId, SnmpCommunicationSpec communicationSpec, SnmpMethod method, List<SnmpMapping> responseMappings, int requestSize) {
this.requestId = requestId;
this.communicationSpec = communicationSpec; this.communicationSpec = communicationSpec;
this.method = method;
this.responseMappings = responseMappings; this.responseMappings = responseMappings;
this.requestSize = requestSize;
if (requestSize > 1) {
this.responseParts = Collections.synchronizedList(new ArrayList<>());
}
} }
} }
private interface ResponseDataMapper { private interface ResponseDataMapper {
JsonObject map(PDU pdu, RequestInfo requestInfo); JsonObject map(List<PDU> pdus, RequestContext requestContext);
} }
private interface ResponseProcessor { private interface ResponseProcessor {
void process(JsonObject responseData, RequestInfo requestInfo, DeviceSessionContext sessionContext); void process(JsonObject responseData, RequestContext requestContext, DeviceSessionContext sessionContext);
} }
} }

31
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.transport.snmp.session; package org.thingsboard.server.transport.snmp.session;
import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -26,9 +27,12 @@ import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.SnmpDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.SnmpDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.data.transport.snmp.SnmpCommunicationSpec;
import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.SessionMsgListener;
import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.service.DefaultTransportService;
import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext; import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg;
@ -57,17 +61,22 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S
private SnmpDeviceTransportConfiguration deviceTransportConfiguration; private SnmpDeviceTransportConfiguration deviceTransportConfiguration;
@Getter @Getter
private final Device device; private final Device device;
@Getter
private final TenantId tenantId;
private final SnmpTransportContext snmpTransportContext; private final SnmpTransportContext snmpTransportContext;
private final AtomicInteger msgIdSeq = new AtomicInteger(0); private final AtomicInteger msgIdSeq = new AtomicInteger(0);
@Getter @Getter
private boolean isActive = true; private boolean isActive = true;
@Setter
private Runnable sessionTimeoutHandler;
@Getter @Getter
private final List<ScheduledFuture<?>> queryingTasks = new LinkedList<>(); private final List<ScheduledFuture<?>> queryingTasks = new LinkedList<>();
public DeviceSessionContext(Device device, DeviceProfile deviceProfile, String token, @Builder
public DeviceSessionContext(TenantId tenantId, Device device, DeviceProfile deviceProfile, String token,
SnmpDeviceProfileTransportConfiguration profileTransportConfiguration, SnmpDeviceProfileTransportConfiguration profileTransportConfiguration,
SnmpDeviceTransportConfiguration deviceTransportConfiguration, SnmpDeviceTransportConfiguration deviceTransportConfiguration,
SnmpTransportContext snmpTransportContext) throws Exception { SnmpTransportContext snmpTransportContext) throws Exception {
@ -75,6 +84,7 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S
super.setDeviceId(device.getId()); super.setDeviceId(device.getId());
super.setDeviceProfile(deviceProfile); super.setDeviceProfile(deviceProfile);
this.device = device; this.device = device;
this.tenantId = tenantId;
this.token = token; this.token = token;
this.snmpTransportContext = snmpTransportContext; this.snmpTransportContext = snmpTransportContext;
@ -131,19 +141,32 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S
@Override @Override
public void onAttributeUpdate(UUID sessionId, AttributeUpdateNotificationMsg attributeUpdateNotification) { public void onAttributeUpdate(UUID sessionId, AttributeUpdateNotificationMsg attributeUpdateNotification) {
log.trace("[{}] Received attributes update notification to device", sessionId); log.trace("[{}] Received attributes update notification to device", sessionId);
snmpTransportContext.getSnmpTransportService().onAttributeUpdate(this, attributeUpdateNotification); try {
snmpTransportContext.getSnmpTransportService().onAttributeUpdate(this, attributeUpdateNotification);
} catch (Exception e) {
snmpTransportContext.getTransportService().errorEvent(getTenantId(), getDeviceId(), SnmpCommunicationSpec.SHARED_ATTRIBUTES_SETTING.getLabel(), e);
}
} }
@Override @Override
public void onRemoteSessionCloseCommand(UUID sessionId, SessionCloseNotificationProto sessionCloseNotification) { public void onRemoteSessionCloseCommand(UUID sessionId, SessionCloseNotificationProto sessionCloseNotification) {
log.trace("[{}] Received the remote command to close the session: {}", sessionId, sessionCloseNotification.getMessage()); log.trace("[{}] Received the remote command to close the session: {}", sessionId, sessionCloseNotification.getMessage());
if (sessionCloseNotification.getMessage().equals(DefaultTransportService.SESSION_EXPIRED_MESSAGE)) {
if (sessionTimeoutHandler != null) {
sessionTimeoutHandler.run();
}
}
} }
@Override @Override
public void onToDeviceRpcRequest(UUID sessionId, ToDeviceRpcRequestMsg toDeviceRequest) { public void onToDeviceRpcRequest(UUID sessionId, ToDeviceRpcRequestMsg toDeviceRequest) {
log.trace("[{}] Received RPC command to device", sessionId); log.trace("[{}] Received RPC command to device", sessionId);
snmpTransportContext.getSnmpTransportService().onToDeviceRpcRequest(this, toDeviceRequest); try {
snmpTransportContext.getTransportService().process(getSessionInfo(), toDeviceRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY); snmpTransportContext.getSnmpTransportService().onToDeviceRpcRequest(this, toDeviceRequest);
snmpTransportContext.getTransportService().process(getSessionInfo(), toDeviceRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY);
} catch (Exception e) {
snmpTransportContext.getTransportService().errorEvent(getTenantId(), getDeviceId(), SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST.getLabel(), e);
}
} }
@Override @Override

23
common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpDeviceSimulatorV2.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.transport.snmp; package org.thingsboard.server.transport.snmp;
import org.snmp4j.CommandResponderEvent;
import org.snmp4j.CommunityTarget; import org.snmp4j.CommunityTarget;
import org.snmp4j.PDU; import org.snmp4j.PDU;
import org.snmp4j.Snmp; import org.snmp4j.Snmp;
@ -35,7 +34,6 @@ import org.snmp4j.agent.mo.snmp.SnmpTargetMIB;
import org.snmp4j.agent.mo.snmp.StorageType; import org.snmp4j.agent.mo.snmp.StorageType;
import org.snmp4j.agent.mo.snmp.VacmMIB; import org.snmp4j.agent.mo.snmp.VacmMIB;
import org.snmp4j.agent.security.MutableVACM; import org.snmp4j.agent.security.MutableVACM;
import org.snmp4j.mp.MPv3;
import org.snmp4j.mp.SnmpConstants; import org.snmp4j.mp.SnmpConstants;
import org.snmp4j.security.SecurityLevel; import org.snmp4j.security.SecurityLevel;
import org.snmp4j.security.SecurityModel; import org.snmp4j.security.SecurityModel;
@ -53,27 +51,11 @@ import org.snmp4j.transport.TransportMappings;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public class SnmpDeviceSimulatorV2 extends BaseAgent { public class SnmpDeviceSimulatorV2 extends BaseAgent {
public static class RequestProcessor extends CommandProcessor {
private final Consumer<CommandResponderEvent> processor;
public RequestProcessor(Consumer<CommandResponderEvent> processor) {
super(new OctetString(MPv3.createLocalEngineID()));
this.processor = processor;
}
@Override
public void processPdu(CommandResponderEvent event) {
processor.accept(event);
}
}
private final Target target; private final Target target;
private final Address address; private final Address address;
private Snmp snmp; private Snmp snmp;
@ -81,10 +63,7 @@ public class SnmpDeviceSimulatorV2 extends BaseAgent {
private final String password; private final String password;
public SnmpDeviceSimulatorV2(int port, String password) throws IOException { public SnmpDeviceSimulatorV2(int port, String password) throws IOException {
super(new File("conf.agent"), new File("bootCounter.agent"), new RequestProcessor(event -> { super(new File("conf.agent"), new File("bootCounter.agent"), new CommandProcessor(new OctetString("12312")));
System.out.println("aboba");
((Snmp) event.getSource()).cancel(event.getPDU(), event1 -> System.out.println("canceled"));
}));
CommunityTarget target = new CommunityTarget(); CommunityTarget target = new CommunityTarget();
target.setCommunity(new OctetString(password)); target.setCommunity(new OctetString(password));
this.address = GenericAddress.parse("udp:0.0.0.0/" + port); this.address = GenericAddress.parse("udp:0.0.0.0/" + port);

43
common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpTestV2.java

@ -16,34 +16,41 @@
package org.thingsboard.server.transport.snmp; package org.thingsboard.server.transport.snmp;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Scanner; import java.util.Scanner;
public class SnmpTestV2 { public class SnmpTestV2 {
public static void main(String[] args) throws IOException {
SnmpDeviceSimulatorV2 device = new SnmpDeviceSimulatorV2(1610, "public");
device.start(); private static final Scanner scanner = new Scanner(System.in);
device.setUpMappings(Map.of(
".1.3.6.1.2.1.1.1.50", "12",
".1.3.6.1.2.1.2.1.52", "56",
".1.3.6.1.2.1.3.1.54", "yes",
".1.3.6.1.2.1.7.1.58", ""
));
public static void main(String[] args) throws IOException {
SnmpDeviceSimulatorV2 client = new SnmpDeviceSimulatorV2(1610, "public");
// while (true) { client.start();
// new Scanner(System.in).nextLine(); Map<String, String> mappings = new HashMap<>();
// device.sendTrap("127.0.0.1", 1062, Map.of(".1.3.6.1.2.87.1.56", "12")); // for (int i = 1; i <= 500; i++) {
// System.out.println("sent"); // String oid = String.format(".1.3.6.1.2.1.%s.1.52", i);
// mappings.put(oid, "value_" + i);
// } // }
mappings.put("1.3.6.1.2.1.266.1.52", "****");
// Snmp snmp = new Snmp(device.transportMappings[0]); client.setUpMappings(mappings);
// device.snmp.addCommandResponder(event -> { inputTraps(client);
// System.out.println(event);
// }); scanner.nextLine();
}
new Scanner(System.in).nextLine(); private static void inputTraps(SnmpDeviceSimulatorV2 client) throws IOException {
while (true) {
String data = scanner.nextLine();
if (!data.isEmpty()) {
client.sendTrap("127.0.0.1", 1620, Map.of(
"1.3.6.1.2.1.266.1.52", data + " (266)",
"1.3.6.1.2.1.267.1.52", data + " (267)"
));
}
}
} }
} }

7
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java

@ -17,6 +17,9 @@ package org.thingsboard.server.common.transport;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse;
@ -144,6 +147,10 @@ public interface TransportService {
void reportActivity(SessionInfoProto sessionInfo); void reportActivity(SessionInfoProto sessionInfo);
void lifecycleEvent(TenantId tenantId, DeviceId deviceId, ComponentLifecycleEvent eventType, boolean success, Throwable error);
void errorEvent(TenantId tenantId, DeviceId deviceId, String method, Throwable error);
void deregisterSession(SessionInfoProto sessionInfo); void deregisterSession(SessionInfoProto sessionInfo);
void log(SessionInfoProto sessionInfo, String msg); void log(SessionInfoProto sessionInfo, String msg);

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

@ -22,6 +22,7 @@ import com.google.gson.Gson;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
@ -49,6 +50,8 @@ import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger; import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.data.rpc.RpcStatus;
@ -827,6 +830,37 @@ public class DefaultTransportService implements TransportService {
sessionsToRemove.forEach(sessionsActivity::remove); sessionsToRemove.forEach(sessionsActivity::remove);
} }
@Override
public void lifecycleEvent(TenantId tenantId, DeviceId deviceId, ComponentLifecycleEvent eventType, boolean success, Throwable error) {
ToCoreMsg msg = ToCoreMsg.newBuilder()
.setLifecycleEventMsg(TransportProtos.LifecycleEventProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setEntityIdMSB(deviceId.getId().getMostSignificantBits())
.setEntityIdLSB(deviceId.getId().getLeastSignificantBits())
.setServiceId(serviceInfoProvider.getServiceId())
.setLcEventType(eventType.name())
.setSuccess(success)
.setError(error != null ? ExceptionUtils.getStackTrace(error) : ""))
.build();
sendToCore(tenantId, deviceId, msg, deviceId.getId(), TransportServiceCallback.EMPTY);
}
@Override
public void errorEvent(TenantId tenantId, DeviceId deviceId, String method, Throwable error) {
ToCoreMsg msg = ToCoreMsg.newBuilder()
.setErrorEventMsg(TransportProtos.ErrorEventProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setEntityIdMSB(deviceId.getId().getMostSignificantBits())
.setEntityIdLSB(deviceId.getId().getLeastSignificantBits())
.setServiceId(serviceInfoProvider.getServiceId())
.setMethod(method)
.setError(ExceptionUtils.getStackTrace(error)))
.build();
sendToCore(tenantId, deviceId, msg, deviceId.getId(), TransportServiceCallback.EMPTY);
}
@Override @Override
public SessionMetaData registerSyncSession(TransportProtos.SessionInfoProto sessionInfo, SessionMsgListener listener, long timeout) { public SessionMetaData registerSyncSession(TransportProtos.SessionInfoProto sessionInfo, SessionMsgListener listener, long timeout) {
SessionMetaData currentSession = new SessionMetaData(sessionInfo, TransportProtos.SessionType.SYNC, listener); SessionMetaData currentSession = new SessionMetaData(sessionInfo, TransportProtos.SessionType.SYNC, listener);
@ -1118,18 +1152,21 @@ public class DefaultTransportService implements TransportService {
} }
protected void sendToDeviceActor(TransportProtos.SessionInfoProto sessionInfo, TransportToDeviceActorMsg toDeviceActorMsg, TransportServiceCallback<Void> callback) { protected void sendToDeviceActor(TransportProtos.SessionInfoProto sessionInfo, TransportToDeviceActorMsg toDeviceActorMsg, TransportServiceCallback<Void> callback) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, getTenantId(sessionInfo), getDeviceId(sessionInfo)); ToCoreMsg toCoreMsg = ToCoreMsg.newBuilder().setToDeviceActorMsg(toDeviceActorMsg).build();
sendToCore(getTenantId(sessionInfo), getDeviceId(sessionInfo), toCoreMsg, getRoutingKey(sessionInfo), callback);
}
private void sendToCore(TenantId tenantId, EntityId entityId, ToCoreMsg msg, UUID routingKey, TransportServiceCallback<Void> callback) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
if (log.isTraceEnabled()) { if (log.isTraceEnabled()) {
log.trace("[{}][{}] Pushing to topic {} message {}", getTenantId(sessionInfo), getDeviceId(sessionInfo), tpi.getFullTopicName(), toDeviceActorMsg); log.trace("[{}][{}] Pushing to topic {} message {}", tenantId, entityId, tpi.getFullTopicName(), msg);
} }
TransportTbQueueCallback transportTbQueueCallback = callback != null ? TransportTbQueueCallback transportTbQueueCallback = callback != null ?
new TransportTbQueueCallback(callback) : null; new TransportTbQueueCallback(callback) : null;
tbCoreProducerStats.incrementTotal(); tbCoreProducerStats.incrementTotal();
StatsCallback wrappedCallback = new StatsCallback(transportTbQueueCallback, tbCoreProducerStats); StatsCallback wrappedCallback = new StatsCallback(transportTbQueueCallback, tbCoreProducerStats);
tbCoreMsgProducer.send(tpi, tbCoreMsgProducer.send(tpi, new TbProtoQueueMsg<>(routingKey, msg), wrappedCallback);
new TbProtoQueueMsg<>(getRoutingKey(sessionInfo),
ToCoreMsg.newBuilder().setToDeviceActorMsg(toDeviceActorMsg).build()),
wrappedCallback);
} }
private void sendToRuleEngine(TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) { private void sendToRuleEngine(TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) {

7
dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java

@ -67,6 +67,8 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_DEVICE_ID = "Incorrect deviceId "; public static final String INCORRECT_DEVICE_ID = "Incorrect deviceId ";
public static final String DEFAULT_DEVICE_TELEMETRY_TOPIC = "v1/devices/me/telemetry"; public static final String DEFAULT_DEVICE_TELEMETRY_TOPIC = "v1/devices/me/telemetry";
public static final String HTTP_DEFAULT_PORT = "80";
public static final String HTTPS_DEFAULT_PORT = "443";
private final Map<String, Resource> certs = new ConcurrentHashMap<>(); private final Map<String, Resource> certs = new ConcurrentHashMap<>();
@ -203,8 +205,9 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
return null; return null;
} }
String hostName = getHost(baseUrl, properties); String hostName = getHost(baseUrl, properties);
String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort(); String propertiesPort = properties.getPort();
String port = (propertiesPort.isEmpty() || HTTP_DEFAULT_PORT.equals(propertiesPort) || HTTPS_DEFAULT_PORT.equals(propertiesPort))
? "" : ":" + propertiesPort;
return DeviceConnectivityUtil.getHttpPublishCommand(protocol, hostName, port, deviceCredentials); return DeviceConnectivityUtil.getHttpPublishCommand(protocol, hostName, port, deviceCredentials);
} }

23
dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java

@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.util.ReflectionUtils; import org.thingsboard.server.common.data.util.ReflectionUtils;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.rule.RuleChainDao;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
@ -41,15 +40,14 @@ import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
@Component @Component
@Slf4j @Slf4j
public class RuleChainDataValidator extends DataValidator<RuleChain> { public class RuleChainDataValidator extends DataValidator<RuleChain> {
@Autowired
private RuleChainDao ruleChainDao;
@Autowired @Autowired
@Lazy @Lazy
private RuleChainService ruleChainService; private RuleChainService ruleChainService;
@ -88,15 +86,19 @@ public class RuleChainDataValidator extends DataValidator<RuleChain> {
} }
} }
public static void validateMetaData(RuleChainMetaData ruleChainMetaData) { public static List<Throwable> validateMetaData(RuleChainMetaData ruleChainMetaData) {
ConstraintValidator.validateFields(ruleChainMetaData); ConstraintValidator.validateFields(ruleChainMetaData);
ruleChainMetaData.getNodes().forEach(RuleChainDataValidator::validateRuleNode); List<Throwable> throwables = ruleChainMetaData.getNodes().stream()
.map(RuleChainDataValidator::validateRuleNode)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(ruleChainMetaData.getConnections())) { if (CollectionUtils.isNotEmpty(ruleChainMetaData.getConnections())) {
validateCircles(ruleChainMetaData.getConnections()); validateCircles(ruleChainMetaData.getConnections());
} }
return throwables;
} }
public static void validateRuleNode(RuleNode ruleNode) { public static Throwable validateRuleNode(RuleNode ruleNode) {
String errorPrefix = "'" + ruleNode.getName() + "' node configuration is invalid: "; String errorPrefix = "'" + ruleNode.getName() + "' node configuration is invalid: ";
ConstraintValidator.validateFields(ruleNode, errorPrefix); ConstraintValidator.validateFields(ruleNode, errorPrefix);
Object nodeConfig; Object nodeConfig;
@ -104,11 +106,12 @@ public class RuleChainDataValidator extends DataValidator<RuleChain> {
Class<Object> nodeConfigType = ReflectionUtils.getAnnotationProperty(ruleNode.getType(), Class<Object> nodeConfigType = ReflectionUtils.getAnnotationProperty(ruleNode.getType(),
"org.thingsboard.rule.engine.api.RuleNode", "configClazz"); "org.thingsboard.rule.engine.api.RuleNode", "configClazz");
nodeConfig = JacksonUtil.treeToValue(ruleNode.getConfiguration(), nodeConfigType); nodeConfig = JacksonUtil.treeToValue(ruleNode.getConfiguration(), nodeConfigType);
} catch (Exception e) { } catch (Throwable t) {
log.warn("Failed to validate node configuration: {}", ExceptionUtils.getRootCauseMessage(e)); log.warn("Failed to validate node configuration: {}", ExceptionUtils.getRootCauseMessage(t));
return; return t;
} }
ConstraintValidator.validateFields(nodeConfig, errorPrefix); ConstraintValidator.validateFields(nodeConfig, errorPrefix);
return null;
} }
private static void validateCircles(List<NodeConnectionInfo> connectionInfos) { private static void validateCircles(List<NodeConnectionInfo> connectionInfos) {

3
dao/src/test/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidatorTest.java

@ -23,7 +23,6 @@ import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.dao.rule.RuleChainDao;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
@ -35,8 +34,6 @@ import static org.mockito.Mockito.verify;
@SpringBootTest(classes = RuleChainDataValidator.class) @SpringBootTest(classes = RuleChainDataValidator.class)
class RuleChainDataValidatorTest { class RuleChainDataValidatorTest {
@MockBean
RuleChainDao ruleChainDao;
@MockBean @MockBean
RuleChainService ruleChainService; RuleChainService ruleChainService;
@MockBean @MockBean

2
docker/docker-compose.yml

@ -235,6 +235,8 @@ services:
tb-snmp-transport: tb-snmp-transport:
restart: always restart: always
image: "${DOCKER_REPO}/${SNMP_TRANSPORT_DOCKER_NAME}:${TB_VERSION}" image: "${DOCKER_REPO}/${SNMP_TRANSPORT_DOCKER_NAME}:${TB_VERSION}"
ports:
- "1620:1620/udp"
environment: environment:
TB_SERVICE_ID: tb-snmp-transport TB_SERVICE_ID: tb-snmp-transport
JAVA_OPTS: "${JAVA_OPTS}" JAVA_OPTS: "${JAVA_OPTS}"

1
docker/tb-snmp-transport.env

@ -1,6 +1,7 @@
ZOOKEEPER_ENABLED=true ZOOKEEPER_ENABLED=true
ZOOKEEPER_URL=zookeeper:2181 ZOOKEEPER_URL=zookeeper:2181
SNMP_BIND_PORT=1620
METRICS_ENABLED=true METRICS_ENABLED=true
METRICS_ENDPOINTS_EXPOSE=prometheus METRICS_ENDPOINTS_EXPOSE=prometheus
WEB_APPLICATION_ENABLE=true WEB_APPLICATION_ENABLE=true

4
transport/snmp/src/main/resources/tb-snmp-transport.yml

@ -99,11 +99,15 @@ redis:
transport: transport:
snmp: snmp:
enabled: "${SNMP_ENABLED:true}" enabled: "${SNMP_ENABLED:true}"
bind_port: "${SNMP_BIND_PORT:1620}"
response_processing: response_processing:
# parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices # parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices
parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:20}" parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:20}"
# to configure SNMP to work over UDP or TCP # to configure SNMP to work over UDP or TCP
underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}" underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}"
max_request_oids: "${SNMP_MAX_REQUEST_OIDS:100}"
response:
ignore_type_cast_errors: "${SNMP_RESPONSE_IGNORE_TYPE_CAST_ERRORS:false}"
sessions: sessions:
inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}"
report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:3000}" report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:3000}"

13
ui-ngx/src/app/core/translate/missing-translate-handler.ts

@ -21,7 +21,18 @@ export class TbMissingTranslationHandler implements MissingTranslationHandler {
handle(params: MissingTranslationHandlerParams) { handle(params: MissingTranslationHandlerParams) {
if (params.key && !params.key.startsWith(customTranslationsPrefix)) { if (params.key && !params.key.startsWith(customTranslationsPrefix)) {
console.warn('Translation for \'' + params.key + '\' doesn\'t exist'); console.warn('Translation for \'' + params.key + '\' doesn\'t exist');
params.translateService.set(params.key, params.key); let translations: any;
const parts = params.key.split('.');
for (let i=parts.length-1; i>=0; i--) {
const newTranslations = {};
if (i === parts.length-1) {
newTranslations[parts[i]] = params.key;
} else {
newTranslations[parts[i]] = translations;
}
translations = newTranslations;
}
params.translateService.setTranslation(params.translateService.currentLang, translations, true);
} }
} }
} }

8
ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts

@ -65,6 +65,10 @@ export enum ImportEntityColumnType {
lwm2mServerSecurityMode = 'LWM2M_SERVER_SECURITY_MODE', lwm2mServerSecurityMode = 'LWM2M_SERVER_SECURITY_MODE',
lwm2mServerClientPublicKeyOrId = 'LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID', lwm2mServerClientPublicKeyOrId = 'LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID',
lwm2mServerClientSecretKey = 'LWM2M_SERVER_CLIENT_SECRET_KEY', lwm2mServerClientSecretKey = 'LWM2M_SERVER_CLIENT_SECRET_KEY',
snmpHost = 'SNMP_HOST',
snmpPort = 'SNMP_PORT',
snmpVersion = 'SNMP_VERSION',
snmpCommunityString = 'SNMP_COMMUNITY_STRING',
isGateway = 'IS_GATEWAY', isGateway = 'IS_GATEWAY',
description = 'DESCRIPTION', description = 'DESCRIPTION',
routingKey = 'ROUTING_KEY', routingKey = 'ROUTING_KEY',
@ -96,6 +100,10 @@ export const importEntityColumnTypeTranslations = new Map<ImportEntityColumnType
[ImportEntityColumnType.lwm2mServerSecurityMode, 'import.column-type.lwm2m.lwm2m-server-security-mode'], [ImportEntityColumnType.lwm2mServerSecurityMode, 'import.column-type.lwm2m.lwm2m-server-security-mode'],
[ImportEntityColumnType.lwm2mServerClientPublicKeyOrId, 'import.column-type.lwm2m.lwm2m-server-public-key-id'], [ImportEntityColumnType.lwm2mServerClientPublicKeyOrId, 'import.column-type.lwm2m.lwm2m-server-public-key-id'],
[ImportEntityColumnType.lwm2mServerClientSecretKey, 'import.column-type.lwm2m.lwm2m-server-secret-key'], [ImportEntityColumnType.lwm2mServerClientSecretKey, 'import.column-type.lwm2m.lwm2m-server-secret-key'],
[ImportEntityColumnType.snmpHost, 'import.column-type.snmp.host'],
[ImportEntityColumnType.snmpPort, 'import.column-type.snmp.port'],
[ImportEntityColumnType.snmpVersion, 'import.column-type.snmp.version'],
[ImportEntityColumnType.snmpCommunityString, 'import.column-type.snmp.community-string'],
[ImportEntityColumnType.isGateway, 'import.column-type.isgateway'], [ImportEntityColumnType.isGateway, 'import.column-type.isgateway'],
[ImportEntityColumnType.description, 'import.column-type.description'], [ImportEntityColumnType.description, 'import.column-type.description'],
[ImportEntityColumnType.routingKey, 'import.column-type.routing-key'], [ImportEntityColumnType.routingKey, 'import.column-type.routing-key'],

4
ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts

@ -105,6 +105,10 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
{ value: ImportEntityColumnType.lwm2mServerSecurityMode }, { value: ImportEntityColumnType.lwm2mServerSecurityMode },
{ value: ImportEntityColumnType.lwm2mServerClientPublicKeyOrId }, { value: ImportEntityColumnType.lwm2mServerClientPublicKeyOrId },
{ value: ImportEntityColumnType.lwm2mServerClientSecretKey }, { value: ImportEntityColumnType.lwm2mServerClientSecretKey },
{ value: ImportEntityColumnType.snmpHost },
{ value: ImportEntityColumnType.snmpPort },
{ value: ImportEntityColumnType.snmpVersion },
{ value: ImportEntityColumnType.snmpCommunityString },
); );
break; break;
case EntityType.ASSET: case EntityType.ASSET:

330
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html

@ -23,174 +23,164 @@
<fieldset *ngFor="let source of sources" [ngClass]="{'fields-group': settings.showGroupTitle}"> <fieldset *ngFor="let source of sources" [ngClass]="{'fields-group': settings.showGroupTitle}">
<legend class="group-title" *ngIf="settings.showGroupTitle">{{ getGroupTitle(source.datasource) }} <legend class="group-title" *ngIf="settings.showGroupTitle">{{ getGroupTitle(source.datasource) }}
</legend> </legend>
<div class="tb-multiple-input-layout layout-wrap" <div class="tb-multiple-input-layout"
[ngClass]="{'vertical-alignment': isVerticalAlignment || changeAlignment}"> [style]="{'grid-template-columns': 'repeat(' + columns + ', 1fr)', 'column-gap': settings.columnGap + 'px', 'row-gap': settings.rowGap + 'px'}">
<div *ngFor="let key of visibleKeys(source)" <ng-container *ngFor="let key of visibleKeys(source)">
[ngStyle]="{width: (isVerticalAlignment || changeAlignment) ? '100%' : inputWidthSettings}"> <mat-form-field *ngIf="key.settings.dataKeyValueType === 'string'"
<div class="input-field" *ngIf="key.settings.dataKeyValueType === 'string'"> [appearance]="key.settings.appearance" [subscriptSizing]="key.settings.subscriptSizing">
<mat-form-field class="mat-block" appearance="outline"> <mat-label>{{key.label}}</mat-label>
<mat-label>{{key.label}}</mat-label> <input matInput
<input matInput formControlName="{{key.formId}}"
formControlName="{{key.formId}}" [required]="key.settings.required"
[required]="key.settings.required" [readonly]="key.settings.isEditable === 'readonly'"
[readonly]="key.settings.isEditable === 'readonly'" type="text"
type="text" (focus)="key.isFocused = true; focusInputElement($event)"
(focus)="key.isFocused = true; focusInputElement($event)" (blur)="key.isFocused = false; inputChanged(source, key)">
(blur)="key.isFocused = false; inputChanged(source, key)"> <ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matIconPrefix>
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matPrefix> <ng-container *ngTemplateOutlet="iconPrefix; context: {key: key}"></ng-container>
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon> </ng-container>
<ng-template #customToggleIcon> <mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon"> {{ getErrorMessageText(key.settings, 'required') }}
</ng-template> </mat-error>
</ng-container> </mat-form-field>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')"> <mat-form-field *ngIf="['double', 'integer'].includes(key.settings.dataKeyValueType)"
{{ getErrorMessageText(key.settings, 'required') }} [appearance]="key.settings.appearance" [subscriptSizing]="key.settings.subscriptSizing">
</mat-error> <mat-label>{{key.label}}</mat-label>
</mat-form-field> <input matInput
</div> formControlName="{{key.formId}}"
<div class="input-field" *ngIf="key.settings.dataKeyValueType === 'double' || [required]="key.settings.required"
key.settings.dataKeyValueType === 'integer'"> [readonly]="key.settings.isEditable === 'readonly'"
<mat-form-field class="mat-block" appearance="outline"> type="number"
<mat-label>{{key.label}}</mat-label> step="{{key.settings.step}}"
<input matInput min="{{key.settings.minValue}}"
formControlName="{{key.formId}}" max="{{key.settings.maxValue}}"
[required]="key.settings.required" (focus)="key.isFocused = true; focusInputElement($event)"
[readonly]="key.settings.isEditable === 'readonly'" (blur)="key.isFocused = false; inputChanged(source, key)">
type="number" <ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matIconPrefix>
step="{{key.settings.step}}" <ng-container *ngTemplateOutlet="iconPrefix; context: {key: key}"></ng-container>
min="{{key.settings.minValue}}" </ng-container>
max="{{key.settings.maxValue}}" <mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
(focus)="key.isFocused = true; focusInputElement($event)" {{ getErrorMessageText(key.settings,'required') }}
(blur)="key.isFocused = false; inputChanged(source, key)"> </mat-error>
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matPrefix> <mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('min')">
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon> {{ getErrorMessageText(key.settings,'min') }}
<ng-template #customToggleIcon> </mat-error>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon"> <mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('max')">
</ng-template> {{ getErrorMessageText(key.settings,'max') }}
</ng-container> </mat-error>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')"> </mat-form-field>
{{ getErrorMessageText(key.settings,'required') }} <mat-form-field *ngIf="key.settings.dataKeyValueType === 'JSON'"
</mat-error> [appearance]="key.settings.appearance" [subscriptSizing]="key.settings.subscriptSizing">
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('min')"> <mat-label>{{key.label}}</mat-label>
{{ getErrorMessageText(key.settings,'min') }} <input
</mat-error> matInput
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('max')"> type="text"
{{ getErrorMessageText(key.settings,'max') }} formControlName="{{key.formId}}"
</mat-error> tb-json-to-string
</mat-form-field> [readonly]="key.settings.isEditable === 'readonly'"
</div> [required]="key.settings.required"
<div class="input-field mat-block" *ngIf="key.settings.dataKeyValueType === 'booleanCheckbox'"> (focus)="key.isFocused = true; focusInputElement($event)"
<mat-checkbox formControlName="{{key.formId}}" (blur)="key.isFocused = false; inputChanged(source, key)"
(change)="inputChanged(source, key)"> />
<span class="label-wrapper">{{key.label}}</span> <ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matIconPrefix>
</mat-checkbox> <ng-container *ngTemplateOutlet="iconPrefix; context: {key: key}"></ng-container>
</div> </ng-container>
<div class="input-field" *ngIf="key.settings.dataKeyValueType === 'JSON'"> <button [disabled]="key.settings.isEditable === 'disabled' || key.settings.isEditable === 'readonly'"
<mat-form-field class="mat-block" appearance="outline"> type="button"
<mat-label>{{key.label}}</mat-label> matSuffix mat-icon-button
<input (click)="openEditJSONDialog($event, key, source)">
matInput <mat-icon>open_in_new</mat-icon>
type="text" </button>
formControlName="{{key.formId}}" <mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
tb-json-to-string {{ getErrorMessageText(key.settings,'required') }}
[readonly]="key.settings.isEditable === 'readonly'" </mat-error>
[required]="key.settings.required" <mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('invalidJSON')">
(focus)="key.isFocused = true; focusInputElement($event)" {{ getErrorMessageText(key.settings,'invalidJSON') | translate }}
(blur)="key.isFocused = false; inputChanged(source, key)" </mat-error>
/> </mat-form-field>
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matPrefix> <mat-form-field *ngIf="['dateTime','date', 'time'].includes(key.settings.dataKeyValueType)"
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon> [appearance]="key.settings.appearance" [subscriptSizing]="key.settings.subscriptSizing">
<ng-template #customToggleIcon> <mat-label>{{key.label}}</mat-label>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon"> <mat-datetimepicker-toggle [for]="datePicker" matPrefix></mat-datetimepicker-toggle>
</ng-template> <mat-datetimepicker #datePicker type="{{datePickerType(key.settings.dataKeyValueType)}}"
</ng-container> openOnFocus="true"></mat-datetimepicker>
<button [disabled]="key.settings.isEditable === 'disabled' || key.settings.isEditable === 'readonly'" <input matInput formControlName="{{key.formId}}"
type="button" [required]="key.settings.required"
matSuffix mat-icon-button [readonly]="key.settings.isEditable === 'readonly'"
(click)="openEditJSONDialog($event, key, source)"> [matDatetimepicker]="datePicker"
<mat-icon>open_in_new</mat-icon> (focus)="key.isFocused = true;"
</button> (blur)="key.isFocused = false;"
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')"> (dateChange)="inputChanged(source, key)">
{{ getErrorMessageText(key.settings,'required') }} <mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
</mat-error> {{ getErrorMessageText(key.settings, 'required') }}
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('invalidJSON')"> </mat-error>
{{ getErrorMessageText(key.settings,'invalidJSON') | translate }} <mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('matDatepickerParse')">
</mat-error> {{ getErrorMessageText(key.settings, 'invalidDate') }}
</mat-form-field> </mat-error>
</div> </mat-form-field>
<div class="input-field mat-block" *ngIf="key.settings.dataKeyValueType === 'booleanSwitch'"> <mat-form-field *ngIf="key.settings.dataKeyValueType === 'select'"
<mat-slide-toggle formControlName="{{key.formId}}" [appearance]="key.settings.appearance" [subscriptSizing]="key.settings.subscriptSizing">
[labelPosition]="key.settings.slideToggleLabelPosition" <mat-label>{{key.label}}</mat-label>
(change)="inputChanged(source, key)"> <mat-select formControlName="{{key.formId}}"
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon"> [required]="key.settings.required"
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon> (focus)="key.isFocused = true;"
<ng-template #customToggleIcon> (selectionChange)="key.isFocused = false; inputChanged(source, key)">
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon"> <mat-option *ngFor="let option of key.settings.selectOptions"
</ng-template> [value]="option.value"
</ng-container> [disabled]="key.settings.isEditable === 'readonly'">
<span class="label-wrapper">{{key.label}}</span> {{ getCustomTranslationText(option.label ? option.label : option.value) }}
</mat-slide-toggle> </mat-option>
</div> </mat-select>
<div class="input-field mat-block date-time-input" *ngIf="(key.settings.dataKeyValueType === 'dateTime') || <ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matIconPrefix>
(key.settings.dataKeyValueType === 'date') || <ng-container *ngTemplateOutlet="iconPrefix; context: {key: key}"></ng-container>
(key.settings.dataKeyValueType === 'time')" fxLayout="column"> </ng-container>
<mat-form-field appearance="outline"> <mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
<mat-label>{{key.label}}</mat-label> {{ getErrorMessageText(key.settings, 'required') }}
<mat-datetimepicker-toggle [for]="datePicker" matPrefix></mat-datetimepicker-toggle> </mat-error>
<mat-datetimepicker #datePicker type="{{datePickerType(key.settings.dataKeyValueType)}}" </mat-form-field>
openOnFocus="true"></mat-datetimepicker> <mat-form-field *ngIf="key.settings.dataKeyValueType === 'color'"
<input matInput formControlName="{{key.formId}}" class="color-input" [appearance]="key.settings.appearance" [subscriptSizing]="key.settings.subscriptSizing"
[required]="key.settings.required" (click)="colorInput.openColorPickerPopup($event)">
[readonly]="key.settings.isEditable === 'readonly'" <mat-label>{{key.label}}</mat-label>
[matDatetimepicker]="datePicker" <input matInput
(focus)="key.isFocused = true;" formControlName="{{key.formId}}"
(blur)="key.isFocused = false;" [required]="key.settings.required"
(dateChange)="inputChanged(source, key)"> [readonly]="key.settings.isEditable === 'readonly'"
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')"> type="text"
{{ getErrorMessageText(key.settings, 'required') }} (keydown)="$event.preventDefault();">
</mat-error> <ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matIconPrefix>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('matDatepickerParse')"> <ng-container *ngTemplateOutlet="iconPrefix; context: {key: key}"></ng-container>
{{ getErrorMessageText(key.settings, 'invalidDate') }} </ng-container>
</mat-error> <tb-color-input #colorInput asBoxInput matSuffix
</mat-form-field> colorClearButton
</div> [disabled]="multipleInputFormGroup.get(key.formId).disabled"
<div class="input-field" *ngIf="key.settings.dataKeyValueType === 'select'"> [readonly]="key.settings.isEditable === 'readonly'"
<mat-form-field class="mat-block" appearance="outline"> [ngModel]="multipleInputFormGroup.get(key.formId).value"
<mat-label>{{key.label}}</mat-label> (ngModelChange)="colorChanged(source, key, $event)"
<mat-select formControlName="{{key.formId}}" [ngModelOptions]="{ standalone: true }">
[required]="key.settings.required"
(focus)="key.isFocused = true;"
(selectionChange)="key.isFocused = false; inputChanged(source, key)">
<mat-option *ngFor="let option of key.settings.selectOptions"
[value]="option.value"
[disabled]="key.settings.isEditable === 'readonly'">
{{ getCustomTranslationText(option.label ? option.label : option.value) }}
</mat-option>
</mat-select>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{ getErrorMessageText(key.settings, 'required') }}
</mat-error>
</mat-form-field>
</div>
<div class="tb-form-row space-between color-picker-input" *ngIf="key.settings.dataKeyValueType === 'color'">
<div class="label-container">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon">
<mat-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</mat-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>
</ng-container>
{{key.label}}
</div>
<tb-color-input asBoxInput
[required]="key.settings.required"
[requiredText]="getErrorMessageText(key.settings, 'required')"
openOnInput
formControlName="{{key.formId}}">
</tb-color-input> </tb-color-input>
</div> <mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
</div> {{ getErrorMessageText(key.settings, 'required') }}
</mat-error>
</mat-form-field>
<mat-checkbox *ngIf="key.settings.dataKeyValueType === 'booleanCheckbox'"
formControlName="{{key.formId}}"
(change)="inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon">
<ng-container *ngTemplateOutlet="iconPrefix; context: {key: key}"></ng-container>
</ng-container>
<span class="label-wrapper">{{key.label}}</span>
</mat-checkbox>
<mat-slide-toggle *ngIf="key.settings.dataKeyValueType === 'booleanSwitch'"
formControlName="{{key.formId}}"
[labelPosition]="key.settings.slideToggleLabelPosition"
(change)="inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon">
<ng-container *ngTemplateOutlet="iconPrefix; context: {key: key}"></ng-container>
</ng-container>
<span class="label-wrapper">{{key.label}}</span>
</mat-slide-toggle>
</ng-container>
</div> </div>
</fieldset> </fieldset>
</div> </div>
@ -216,3 +206,9 @@
</div> </div>
</div> </div>
</form> </form>
<ng-template #iconPrefix let-key="key">
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>
</ng-template>

72
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss

@ -38,33 +38,7 @@
} }
.tb-multiple-input-layout { .tb-multiple-input-layout {
display: flex; display: grid;
flex-direction: row;
align-items: start;
}
.color-picker-input {
padding: 7px 16px 7px 12px;
margin: 0 10px 22px 0;
border-color: rgba(0, 0, 0, 0.4);
.label-container {
display: flex;
flex-direction: row;
align-items: center;
}
.mat-icon, img {
margin-right: 5px;
}
}
.input-field {
padding-right: 10px;
mat-form-field {
margin-bottom: 5px;
}
} }
.mat-mdc-slide-toggle { .mat-mdc-slide-toggle {
@ -73,17 +47,6 @@
margin-bottom: 8px; margin-bottom: 8px;
} }
.date-time-input {
.mat-mdc-form-field {
width: 100%;
margin: 2px 0;
}
}
.vertical-alignment {
flex-direction: column;
}
&--buttons-container { &--buttons-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -112,18 +75,33 @@
:host ::ng-deep { :host ::ng-deep {
.tb-multiple-input { .tb-multiple-input {
.mat-mdc-slide-toggle .mdc-form-field { .mat-mdc-slide-toggle, .mat-mdc-checkbox {
width: 100%; .mdc-form-field {
& > label {
display: flex;
align-items: center;
width: 100%; width: 100%;
margin: 0; & > label {
.mat-icon { display: flex;
margin-left: 8px; align-items: center;
margin-right: 8px; width: 100%;
margin: 0;
.mat-icon {
margin-right: 8px;
}
} }
} }
} }
.mat-mdc-slide-toggle {
.mdc-form-field {
& > label {
.mat-icon {
margin-left: 8px;
}
}
}
}
.color-input {
.mat-mdc-form-field-icon-suffix {
padding: 0 8px 0 4px;
}
}
} }
} }

66
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts

@ -48,6 +48,7 @@ import {
} from '@shared/components/dialog/json-object-edit-dialog.component'; } from '@shared/components/dialog/json-object-edit-dialog.component';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
type FieldAlignment = 'row' | 'column'; type FieldAlignment = 'row' | 'column';
@ -55,7 +56,7 @@ type MultipleInputWidgetDataKeyType = 'server' | 'shared' | 'timeseries';
export type MultipleInputWidgetDataKeyValueType = 'string' | 'double' | 'integer' | export type MultipleInputWidgetDataKeyValueType = 'string' | 'double' | 'integer' |
'JSON' | 'booleanCheckbox' | 'booleanSwitch' | 'JSON' | 'booleanCheckbox' | 'booleanSwitch' |
'dateTime' | 'date' | 'time' | 'select' | 'color'; 'dateTime' | 'date' | 'time' | 'select' | 'color';
type MultipleInputWidgetDataKeyEditableType = 'editable' | 'disabled' | 'readonly'; export type MultipleInputWidgetDataKeyEditableType = 'editable' | 'disabled' | 'readonly';
type ConvertGetValueFunction = (value: any, ctx: WidgetContext) => any; type ConvertGetValueFunction = (value: any, ctx: WidgetContext) => any;
type ConvertSetValueFunction = (value: any, originValue: any, ctx: WidgetContext) => any; type ConvertSetValueFunction = (value: any, originValue: any, ctx: WidgetContext) => any;
@ -71,6 +72,8 @@ interface MultipleInputWidgetSettings {
groupTitle: string; groupTitle: string;
fieldsAlignment: FieldAlignment; fieldsAlignment: FieldAlignment;
fieldsInRow: number; fieldsInRow: number;
columnGap: number;
rowGap: number;
attributesShared?: boolean; attributesShared?: boolean;
} }
@ -88,6 +91,8 @@ interface MultipleInputWidgetDataKeySettings {
isEditable: MultipleInputWidgetDataKeyEditableType; isEditable: MultipleInputWidgetDataKeyEditableType;
disabledOnDataKey: string; disabledOnDataKey: string;
dataKeyHidden: boolean; dataKeyHidden: boolean;
appearance: MatFormFieldAppearance;
subscriptSizing: SubscriptSizing;
step?: number; step?: number;
minValue?: number; minValue?: number;
maxValue?: number; maxValue?: number;
@ -148,8 +153,7 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
private isSavingInProgress = false; private isSavingInProgress = false;
isVerticalAlignment: boolean; isVerticalAlignment: boolean;
inputWidthSettings: string; columns: number;
changeAlignment: boolean;
saveButtonLabel: string; saveButtonLabel: string;
resetButtonLabel: string; resetButtonLabel: string;
@ -228,15 +232,15 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
if (isUndefined(this.settings.fieldsInRow)) { if (isUndefined(this.settings.fieldsInRow)) {
this.settings.fieldsInRow = 2; this.settings.fieldsInRow = 2;
} }
// For backward compatibility
this.isVerticalAlignment = !(this.settings.fieldsAlignment === 'row'); this.isVerticalAlignment = !(this.settings.fieldsAlignment === 'row');
if (isUndefined(this.settings.columnGap)) {
if (!this.isVerticalAlignment && this.settings.fieldsInRow) { this.settings.columnGap = 10;
this.inputWidthSettings = 100 / this.settings.fieldsInRow + '%'; }
if (isUndefined(this.settings.rowGap)) {
this.settings.rowGap = 5;
} }
this.updateWidgetDisplaying(); this.updateColumns();
} }
private updateDatasources() { private updateDatasources() {
@ -284,6 +288,15 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
dataKey.settings.isEditable = 'editable'; dataKey.settings.isEditable = 'editable';
} }
} }
if (isUndefined(dataKey.settings.appearance)) {
dataKey.settings.appearance = 'outline';
}
if (isUndefined(dataKey.settings.subscriptSizing)) {
dataKey.settings.subscriptSizing = 'fixed';
}
// For backward compatibility // For backward compatibility
if (dataKey.settings.dataKeyValueType === 'select') { if (dataKey.settings.dataKeyValueType === 'select') {
@ -387,12 +400,6 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
} }
}); });
} }
} else if (key.settings.dataKeyValueType === 'color') {
formControl.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.inputChanged(source, key);
});
} }
this.multipleInputFormGroup.addControl(key.formId, formControl); this.multipleInputFormGroup.addControl(key.formId, formControl);
} }
@ -452,9 +459,8 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
} }
if (key.settings.isEditable === 'editable' && key.settings.disabledOnDataKey) { if (key.settings.isEditable === 'editable' && key.settings.disabledOnDataKey) {
const conditions = data.filter((item) => { const conditions = data.filter((item) =>
return source.datasource === item.datasource && item.dataKey.name === key.settings.disabledOnDataKey; source.datasource === item.datasource && item.dataKey.name === key.settings.disabledOnDataKey);
});
if (conditions && conditions.length) { if (conditions && conditions.length) {
if (conditions[0].data.length) { if (conditions[0].data.length) {
if (conditions[0].data[0][1] === 'false') { if (conditions[0].data[0][1] === 'false') {
@ -494,8 +500,17 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
return data; return data;
} }
private updateWidgetDisplaying() { private updateColumns() {
this.changeAlignment = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 620); const changeAlignment = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 620);
if (changeAlignment) {
this.columns = 1;
} else {
if (!this.isVerticalAlignment && this.settings.fieldsInRow) {
this.columns = this.settings.fieldsInRow;
} else {
this.columns = 1;
}
}
} }
public onDataUpdated() { public onDataUpdated() {
@ -504,7 +519,7 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
} }
private resize() { private resize() {
this.updateWidgetDisplaying(); this.updateColumns();
this.ctx.detectChanges(); this.ctx.detectChanges();
} }
@ -599,6 +614,13 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
} }
} }
public colorChanged(source: MultipleInputWidgetSource, key: MultipleInputWidgetDataKey, color: string) {
this.multipleInputFormGroup.get(key.formId).setValue(color);
this.multipleInputFormGroup.get(key.formId).markAsDirty();
this.multipleInputFormGroup.get(key.formId).markAsTouched();
this.inputChanged(source, key);
}
public saveForm() { public saveForm() {
if (this.settings.showActionButtons) { if (this.settings.showActionButtons) {
this.save(); this.save();
@ -737,7 +759,7 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
this.multipleInputFormGroup.reset(undefined, {emitEvent: false}); this.multipleInputFormGroup.reset(undefined, {emitEvent: false});
this.sources.forEach((source) => { this.sources.forEach((source) => {
for (const key of this.visibleKeys(source)) { for (const key of this.visibleKeys(source)) {
this.multipleInputFormGroup.get(key.formId).patchValue(key.value, {emitEvent: false}); this.multipleInputFormGroup.get(key.formId).patchValue(key.value);
} }
}); });
this.multipleInputFormGroup.markAsPristine(); this.multipleInputFormGroup.markAsPristine();

31
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html

@ -93,11 +93,36 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field [fxShow]="updateMultipleAttributesKeySettingsForm.get('isEditable').value !== 'disabled'" fxFlex="50" class="mat-block"> <mat-form-field [fxShow]="updateMultipleAttributesKeySettingsForm.get('isEditable').value === 'editable'" fxFlex="50" class="mat-block">
<mat-label translate>widgets.input-widgets.disable-on-datakey-name</mat-label> <mat-label translate>widgets.input-widgets.disable-on-datakey-name</mat-label>
<input matInput formControlName="disabledOnDataKey"> <input matInput formControlName="disabledOnDataKey">
</mat-form-field> </mat-form-field>
</section> </section>
<section *ngIf="!['booleanSwitch', 'booleanCheckbox'].includes(updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value)"
fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex="50" class="mat-block">
<mat-label translate>widgets.input-widgets.field-appearance</mat-label>
<mat-select formControlName="appearance">
<mat-option [value]="'fill'">
{{ 'widgets.input-widgets.appearance-fill' | translate }}
</mat-option>
<mat-option [value]="'outline'">
{{ 'widgets.input-widgets.appearance-outline' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="50" class="mat-block">
<mat-label translate>widgets.input-widgets.subscript-sizing</mat-label>
<mat-select formControlName="subscriptSizing">
<mat-option [value]="'fixed'">
{{ 'widgets.input-widgets.subscript-sizing-fixed' | translate }}
</mat-option>
<mat-option [value]="'dynamic'">
{{ 'widgets.input-widgets.subscript-sizing-dynamic' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</section>
</fieldset> </fieldset>
<fieldset [fxShow]="!updateMultipleAttributesKeySettingsForm.get('dataKeyHidden').value && <fieldset [fxShow]="!updateMultipleAttributesKeySettingsForm.get('dataKeyHidden').value &&
updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value === 'booleanSwitch'" class="fields-group"> updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value === 'booleanSwitch'" class="fields-group">
@ -208,7 +233,9 @@
</mat-form-field> </mat-form-field>
</section> </section>
</fieldset> </fieldset>
<fieldset [fxShow]="!updateMultipleAttributesKeySettingsForm.get('dataKeyHidden').value" class="fields-group"> <fieldset [fxShow]="!updateMultipleAttributesKeySettingsForm.get('dataKeyHidden').value &&
!['date', 'dateTime', 'time'].includes(updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value)"
class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.icon-settings</legend> <legend class="group-title" translate>widgets.input-widgets.icon-settings</legend>
<mat-slide-toggle formControlName="useCustomIcon" class="slide-block"> <mat-slide-toggle formControlName="useCustomIcon" class="slide-block">
{{ 'widgets.input-widgets.use-custom-icon' | translate }} {{ 'widgets.input-widgets.use-custom-icon' | translate }}

18
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts

@ -24,7 +24,10 @@ import {
dataKeySelectOptionValidator dataKeySelectOptionValidator
} from '@home/components/widget/lib/settings/input/datakey-select-option.component'; } from '@home/components/widget/lib/settings/input/datakey-select-option.component';
import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { MultipleInputWidgetDataKeyValueType } from '@home/components/widget/lib/multiple-input-widget.component'; import {
MultipleInputWidgetDataKeyEditableType,
MultipleInputWidgetDataKeyValueType
} from '@home/components/widget/lib/multiple-input-widget.component';
@Component({ @Component({
selector: 'tb-update-multiple-attributes-key-settings', selector: 'tb-update-multiple-attributes-key-settings',
@ -52,6 +55,8 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings
required: false, required: false,
isEditable: 'editable', isEditable: 'editable',
disabledOnDataKey: '', disabledOnDataKey: '',
appearance: 'outline',
subscriptSizing: 'fixed',
slideToggleLabelPosition: 'after', slideToggleLabelPosition: 'after',
selectOptions: [], selectOptions: [],
@ -92,6 +97,8 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings
required: [settings.required, []], required: [settings.required, []],
isEditable: [settings.isEditable, []], isEditable: [settings.isEditable, []],
disabledOnDataKey: [settings.disabledOnDataKey, []], disabledOnDataKey: [settings.disabledOnDataKey, []],
appearance: [settings.appearance, []],
subscriptSizing: [settings.subscriptSizing, []],
// Slide toggle settings // Slide toggle settings
@ -146,7 +153,7 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings
const dataKeyValueType: MultipleInputWidgetDataKeyValueType = const dataKeyValueType: MultipleInputWidgetDataKeyValueType =
this.updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value; this.updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value;
const required: boolean = this.updateMultipleAttributesKeySettingsForm.get('required').value; const required: boolean = this.updateMultipleAttributesKeySettingsForm.get('required').value;
const isEditable: string = this.updateMultipleAttributesKeySettingsForm.get('isEditable').value; const isEditable: MultipleInputWidgetDataKeyEditableType = this.updateMultipleAttributesKeySettingsForm.get('isEditable').value;
const useCustomIcon: boolean = this.updateMultipleAttributesKeySettingsForm.get('useCustomIcon').value; const useCustomIcon: boolean = this.updateMultipleAttributesKeySettingsForm.get('useCustomIcon').value;
const useGetValueFunction: boolean = this.updateMultipleAttributesKeySettingsForm.get('useGetValueFunction').value; const useGetValueFunction: boolean = this.updateMultipleAttributesKeySettingsForm.get('useGetValueFunction').value;
const useSetValueFunction: boolean = this.updateMultipleAttributesKeySettingsForm.get('useSetValueFunction').value; const useSetValueFunction: boolean = this.updateMultipleAttributesKeySettingsForm.get('useSetValueFunction').value;
@ -163,10 +170,15 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings
this.updateMultipleAttributesKeySettingsForm.get('useGetValueFunction').enable({emitEvent: false}); this.updateMultipleAttributesKeySettingsForm.get('useGetValueFunction').enable({emitEvent: false});
this.updateMultipleAttributesKeySettingsForm.get('useSetValueFunction').enable({emitEvent: false}); this.updateMultipleAttributesKeySettingsForm.get('useSetValueFunction').enable({emitEvent: false});
if (isEditable !== 'disabled') { if (isEditable === 'editable') {
this.updateMultipleAttributesKeySettingsForm.get('disabledOnDataKey').enable({emitEvent: false}); this.updateMultipleAttributesKeySettingsForm.get('disabledOnDataKey').enable({emitEvent: false});
} }
if (!['booleanSwitch', 'booleanCheckbox'].includes(dataKeyValueType)) {
this.updateMultipleAttributesKeySettingsForm.get('appearance').enable({emitEvent: false});
this.updateMultipleAttributesKeySettingsForm.get('subscriptSizing').enable({emitEvent: false});
}
if (dataKeyValueType === 'booleanSwitch') { if (dataKeyValueType === 'booleanSwitch') {
this.updateMultipleAttributesKeySettingsForm.get('slideToggleLabelPosition').enable({emitEvent: false}); this.updateMultipleAttributesKeySettingsForm.get('slideToggleLabelPosition').enable({emitEvent: false});
} else if (dataKeyValueType === 'select') { } else if (dataKeyValueType === 'select') {

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html

@ -72,7 +72,7 @@
</section> </section>
</fieldset> </fieldset>
<fieldset class="fields-group"> <fieldset class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.fields-alignment</legend> <legend class="group-title" translate>widgets.input-widgets.layout</legend>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center"> <section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex="50" class="mat-block"> <mat-form-field fxFlex="50" class="mat-block">
<mat-label translate>widgets.input-widgets.fields-alignment</mat-label> <mat-label translate>widgets.input-widgets.fields-alignment</mat-label>
@ -90,5 +90,15 @@
<input matInput type="number" min="1" step="1" formControlName="fieldsInRow"> <input matInput type="number" min="1" step="1" formControlName="fieldsInRow">
</mat-form-field> </mat-form-field>
</section> </section>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex="50" class="mat-block">
<mat-label translate>widgets.input-widgets.row-gap</mat-label>
<input matInput type="number" min="0" step="1" formControlName="rowGap">
</mat-form-field>
<mat-form-field fxFlex="50" [fxShow]="updateMultipleAttributesWidgetSettingsForm.get('fieldsAlignment').value === 'row'" class="mat-block">
<mat-label translate>widgets.input-widgets.column-gap</mat-label>
<input matInput type="number" min="0" step="1" formControlName="columnGap">
</mat-form-field>
</section>
</fieldset> </fieldset>
</section> </section>

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts

@ -49,7 +49,9 @@ export class UpdateMultipleAttributesWidgetSettingsComponent extends WidgetSetti
showGroupTitle: false, showGroupTitle: false,
groupTitle: '', groupTitle: '',
fieldsAlignment: 'row', fieldsAlignment: 'row',
fieldsInRow: 2 fieldsInRow: 2,
rowGap: 5,
columnGap: 10
}; };
} }
@ -77,6 +79,11 @@ export class UpdateMultipleAttributesWidgetSettingsComponent extends WidgetSetti
fieldsAlignment: [settings.fieldsAlignment, []], fieldsAlignment: [settings.fieldsAlignment, []],
fieldsInRow: [settings.fieldsInRow, [Validators.min(1)]], fieldsInRow: [settings.fieldsInRow, [Validators.min(1)]],
// Layout gap
rowGap: [settings.rowGap, [Validators.min(0)]],
columnGap: [settings.columnGap, [Validators.min(0)]]
}); });
} }
@ -105,13 +112,16 @@ export class UpdateMultipleAttributesWidgetSettingsComponent extends WidgetSetti
} }
if (fieldsAlignment === 'row') { if (fieldsAlignment === 'row') {
this.updateMultipleAttributesWidgetSettingsForm.get('fieldsInRow').enable(); this.updateMultipleAttributesWidgetSettingsForm.get('fieldsInRow').enable();
this.updateMultipleAttributesWidgetSettingsForm.get('columnGap').enable();
} else { } else {
this.updateMultipleAttributesWidgetSettingsForm.get('fieldsInRow').disable(); this.updateMultipleAttributesWidgetSettingsForm.get('fieldsInRow').disable();
this.updateMultipleAttributesWidgetSettingsForm.get('columnGap').disable();
} }
this.updateMultipleAttributesWidgetSettingsForm.get('updateAllValues').updateValueAndValidity({emitEvent}); this.updateMultipleAttributesWidgetSettingsForm.get('updateAllValues').updateValueAndValidity({emitEvent});
this.updateMultipleAttributesWidgetSettingsForm.get('saveButtonLabel').updateValueAndValidity({emitEvent}); this.updateMultipleAttributesWidgetSettingsForm.get('saveButtonLabel').updateValueAndValidity({emitEvent});
this.updateMultipleAttributesWidgetSettingsForm.get('resetButtonLabel').updateValueAndValidity({emitEvent}); this.updateMultipleAttributesWidgetSettingsForm.get('resetButtonLabel').updateValueAndValidity({emitEvent});
this.updateMultipleAttributesWidgetSettingsForm.get('groupTitle').updateValueAndValidity({emitEvent}); this.updateMultipleAttributesWidgetSettingsForm.get('groupTitle').updateValueAndValidity({emitEvent});
this.updateMultipleAttributesWidgetSettingsForm.get('fieldsInRow').updateValueAndValidity({emitEvent}); this.updateMultipleAttributesWidgetSettingsForm.get('fieldsInRow').updateValueAndValidity({emitEvent});
this.updateMultipleAttributesWidgetSettingsForm.get('columnGap').updateValueAndValidity({emitEvent});
} }
} }

2
ui-ngx/src/app/shared/components/color-input.component.html

@ -40,7 +40,7 @@
class="tb-box-button" class="tb-box-button"
[disabled]="disabled" [disabled]="disabled"
#matButton #matButton
(click)="openColorPickerPopup($event, matButton)"> (click)="openColorPickerPopup($event, matButton._elementRef)">
<div class="tb-color-preview no-margin box" [ngClass]="{'disabled': disabled}"> <div class="tb-color-preview no-margin box" [ngClass]="{'disabled': disabled}">
<div class="tb-color-result" [style]="!disabled ? {background: colorFormGroup.get('color').value} : {}"></div> <div class="tb-color-result" [style]="!disabled ? {background: colorFormGroup.get('color').value} : {}"></div>
</div> </div>

83
ui-ngx/src/app/shared/components/color-input.component.ts

@ -14,7 +14,16 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core'; import {
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
Input,
OnInit,
Renderer2,
ViewContainerRef
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -84,6 +93,10 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro
@Input() @Input()
disabled: boolean; disabled: boolean;
@Input()
@coerceBoolean()
readonly = false;
private modelValue: string; private modelValue: string;
private propagateChange = null; private propagateChange = null;
@ -151,43 +164,47 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro
showColorPicker($event: MouseEvent) { showColorPicker($event: MouseEvent) {
$event.stopPropagation(); $event.stopPropagation();
this.dialogs.colorPicker(this.colorFormGroup.get('color').value, if (!this.disabled && !this.readonly) {
this.colorClearButton).subscribe( this.dialogs.colorPicker(this.colorFormGroup.get('color').value,
(result) => { this.colorClearButton).subscribe(
if (!result?.canceled) { (result) => {
this.colorFormGroup.patchValue( if (!result?.canceled) {
{color: result?.color}, {emitEvent: true} this.colorFormGroup.patchValue(
); {color: result?.color}, {emitEvent: true}
this.cd.markForCheck(); );
} this.cd.markForCheck();
} }
); }
);
}
} }
openColorPickerPopup($event: Event, matButton: MatButton) { openColorPickerPopup($event: Event, element?: ElementRef) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const trigger = matButton._elementRef.nativeElement; if (!this.disabled && !this.readonly) {
if (this.popoverService.hasPopover(trigger)) { const trigger = element ? element.nativeElement : $event.target;
this.popoverService.hidePopover(trigger); if (this.popoverService.hasPopover(trigger)) {
} else { this.popoverService.hidePopover(trigger);
const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer, } else {
this.viewContainerRef, ColorPickerPanelComponent, 'left', true, null, const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer,
{ this.viewContainerRef, ColorPickerPanelComponent, 'left', true, null,
color: this.colorFormGroup.get('color').value, {
colorClearButton: this.colorClearButton color: this.colorFormGroup.get('color').value,
}, colorClearButton: this.colorClearButton
{}, },
{}, {}, true); {},
colorPickerPopover.tbComponentRef.instance.popover = colorPickerPopover; {}, {}, true);
colorPickerPopover.tbComponentRef.instance.colorSelected.subscribe((color) => { colorPickerPopover.tbComponentRef.instance.popover = colorPickerPopover;
colorPickerPopover.hide(); colorPickerPopover.tbComponentRef.instance.colorSelected.subscribe((color) => {
this.colorFormGroup.patchValue( colorPickerPopover.hide();
{color}, {emitEvent: true} this.colorFormGroup.patchValue(
); {color}, {emitEvent: true}
this.cd.markForCheck(); );
}); this.cd.markForCheck();
});
}
} }
} }

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

@ -298,14 +298,16 @@ export enum SnmpSpecType {
TELEMETRY_QUERYING = 'TELEMETRY_QUERYING', TELEMETRY_QUERYING = 'TELEMETRY_QUERYING',
CLIENT_ATTRIBUTES_QUERYING = 'CLIENT_ATTRIBUTES_QUERYING', CLIENT_ATTRIBUTES_QUERYING = 'CLIENT_ATTRIBUTES_QUERYING',
SHARED_ATTRIBUTES_SETTING = 'SHARED_ATTRIBUTES_SETTING', SHARED_ATTRIBUTES_SETTING = 'SHARED_ATTRIBUTES_SETTING',
TO_DEVICE_RPC_REQUEST = 'TO_DEVICE_RPC_REQUEST' TO_DEVICE_RPC_REQUEST = 'TO_DEVICE_RPC_REQUEST',
TO_SERVER_RPC_REQUEST = 'TO_SERVER_RPC_REQUEST'
} }
export const SnmpSpecTypeTranslationMap = new Map<SnmpSpecType, string>([ export const SnmpSpecTypeTranslationMap = new Map<SnmpSpecType, string>([
[SnmpSpecType.TELEMETRY_QUERYING, ' Telemetry'], [SnmpSpecType.TELEMETRY_QUERYING, ' Telemetry (SNMP GET)'],
[SnmpSpecType.CLIENT_ATTRIBUTES_QUERYING, 'Client attributes'], [SnmpSpecType.CLIENT_ATTRIBUTES_QUERYING, 'Client attributes (SNMP GET)'],
[SnmpSpecType.SHARED_ATTRIBUTES_SETTING, 'Shared attributes'], [SnmpSpecType.SHARED_ATTRIBUTES_SETTING, 'Shared attributes (SNMP SET)'],
[SnmpSpecType.TO_DEVICE_RPC_REQUEST, 'RPC request'] [SnmpSpecType.TO_DEVICE_RPC_REQUEST, 'To-device RPC request (SNMP GET/SET)'],
[SnmpSpecType.TO_SERVER_RPC_REQUEST, 'From-device RPC request (SNMP TRAP)']
]); ]);
export interface SnmpCommunicationConfig { export interface SnmpCommunicationConfig {

848
ui-ngx/src/assets/dashboard/api_usage.json

File diff suppressed because it is too large

45
ui-ngx/src/assets/dashboard/customer_user_home_page.json

@ -7,13 +7,7 @@
"description": "", "description": "",
"widgets": { "widgets": {
"867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": { "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "documentation_links",
"type": "static", "type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 3, "sizeY": 3,
"config": { "config": {
@ -41,16 +35,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4" "id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4",
"typeFullFqn": "system.home_page_widgets.documentation_links"
}, },
"9e3ef045-d8bc-1640-a3f4-2dd10b19d50e": { "9e3ef045-d8bc-1640-a3f4-2dd10b19d50e": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "quick_links",
"type": "static", "type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 3, "sizeY": 3,
"config": { "config": {
@ -73,16 +62,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "9e3ef045-d8bc-1640-a3f4-2dd10b19d50e" "id": "9e3ef045-d8bc-1640-a3f4-2dd10b19d50e",
"typeFullFqn": "system.home_page_widgets.quick_links"
}, },
"6d6e2b1d-6ce7-4678-3745-c6b0897b2674": { "6d6e2b1d-6ce7-4678-3745-c6b0897b2674": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "dashboards",
"type": "static", "type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 6.5, "sizeY": 6.5,
"config": { "config": {
@ -103,16 +87,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "6d6e2b1d-6ce7-4678-3745-c6b0897b2674" "id": "6d6e2b1d-6ce7-4678-3745-c6b0897b2674",
"typeFullFqn": "system.home_page_widgets.dashboards"
}, },
"35d3ac3b-f1af-dc3d-a1d1-11f351d16fc6": { "35d3ac3b-f1af-dc3d-a1d1-11f351d16fc6": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -238,16 +217,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "35d3ac3b-f1af-dc3d-a1d1-11f351d16fc6" "id": "35d3ac3b-f1af-dc3d-a1d1-11f351d16fc6",
"typeFullFqn": "system.cards.markdown_card"
}, },
"7ac20b6a-dc40-b18e-9f5f-bca20bc693bb": { "7ac20b6a-dc40-b18e-9f5f-bca20bc693bb": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -392,7 +366,8 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "7ac20b6a-dc40-b18e-9f5f-bca20bc693bb" "id": "7ac20b6a-dc40-b18e-9f5f-bca20bc693bb",
"typeFullFqn": "system.cards.markdown_card"
} }
}, },
"states": { "states": {

250
ui-ngx/src/assets/dashboard/sys_admin_home_page.json

@ -7,13 +7,7 @@
"description": "", "description": "",
"widgets": { "widgets": {
"d70cc256-4c7b-ee06-9905-b8c5e546605f": { "d70cc256-4c7b-ee06-9905-b8c5e546605f": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -72,16 +66,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "d70cc256-4c7b-ee06-9905-b8c5e546605f" "id": "d70cc256-4c7b-ee06-9905-b8c5e546605f",
"typeFullFqn": "system.cards.markdown_card"
}, },
"8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e": { "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "timeseries_bars_flot",
"type": "timeseries", "type": "timeseries",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 8, "sizeX": 8,
"sizeY": 5, "sizeY": 5,
"config": { "config": {
@ -179,7 +168,18 @@
"showLabels": true "showLabels": true
}, },
"customLegendEnabled": false, "customLegendEnabled": false,
"dataKeysListForLabels": [] "dataKeysListForLabels": [],
"showLegend": false,
"legendConfig": {
"direction": "column",
"position": "bottom",
"sortDataKeys": false,
"showMin": false,
"showMax": false,
"showAvg": true,
"showTotal": false,
"showLatest": false
}
}, },
"title": "Transport messages", "title": "Transport messages",
"dropShadow": false, "dropShadow": false,
@ -192,37 +192,21 @@
"padding": "0" "padding": "0"
}, },
"useDashboardTimewindow": false, "useDashboardTimewindow": false,
"showLegend": false,
"actions": {}, "actions": {},
"displayTimewindow": true, "displayTimewindow": true,
"showTitleIcon": false, "showTitleIcon": false,
"titleTooltip": "", "titleTooltip": "",
"widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}\n\n.tb-widget-container > .tb-widget .flot-base {\n opacity: 0.48;\n}\n", "widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}\n\n.tb-widget-container > .tb-widget .flot-base {\n opacity: 0.48;\n}\n",
"pageSize": 1024, "pageSize": 1024,
"noDataDisplayMessage": "", "noDataDisplayMessage": ""
"legendConfig": {
"direction": "column",
"position": "bottom",
"sortDataKeys": false,
"showMin": false,
"showMax": false,
"showAvg": true,
"showTotal": false,
"showLatest": false
}
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e" "id": "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e",
"typeFullFqn": "system.charts.timeseries_bars_flot"
}, },
"4b5e47ed-c197-a937-d727-041ba8decec2": { "4b5e47ed-c197-a937-d727-041ba8decec2": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -307,16 +291,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "4b5e47ed-c197-a937-d727-041ba8decec2" "id": "4b5e47ed-c197-a937-d727-041ba8decec2",
"typeFullFqn": "system.cards.markdown_card"
}, },
"eace9148-b02a-48fe-1a95-acd9928aa8c5": { "eace9148-b02a-48fe-1a95-acd9928aa8c5": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries", "type": "timeseries",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 8, "sizeX": 8,
"sizeY": 5, "sizeY": 5,
"config": { "config": {
@ -501,7 +480,18 @@
"showLabels": true "showLabels": true
}, },
"customLegendEnabled": false, "customLegendEnabled": false,
"dataKeysListForLabels": [] "dataKeysListForLabels": [],
"showLegend": true,
"legendConfig": {
"direction": "row",
"position": "top",
"sortDataKeys": false,
"showMin": false,
"showMax": false,
"showAvg": false,
"showTotal": false,
"showLatest": false
}
}, },
"title": "System Info Chart", "title": "System Info Chart",
"dropShadow": false, "dropShadow": false,
@ -518,31 +508,15 @@
"pageSize": 1024, "pageSize": 1024,
"units": "%", "units": "%",
"noDataDisplayMessage": "", "noDataDisplayMessage": "",
"showLegend": true,
"legendConfig": {
"direction": "row",
"position": "top",
"sortDataKeys": false,
"showMin": false,
"showMax": false,
"showAvg": false,
"showTotal": false,
"showLatest": false
},
"displayTimewindow": true "displayTimewindow": true
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "eace9148-b02a-48fe-1a95-acd9928aa8c5" "id": "eace9148-b02a-48fe-1a95-acd9928aa8c5",
"typeFullFqn": "system.charts.basic_timeseries"
}, },
"8acbf5df-f9fc-114d-216f-86f081aa4779": { "8acbf5df-f9fc-114d-216f-86f081aa4779": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -601,16 +575,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "8acbf5df-f9fc-114d-216f-86f081aa4779" "id": "8acbf5df-f9fc-114d-216f-86f081aa4779",
"typeFullFqn": "system.cards.markdown_card"
}, },
"d2784f6c-0518-fd95-1d28-b21f70bdcb10": { "d2784f6c-0518-fd95-1d28-b21f70bdcb10": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -668,16 +637,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "d2784f6c-0518-fd95-1d28-b21f70bdcb10" "id": "d2784f6c-0518-fd95-1d28-b21f70bdcb10",
"typeFullFqn": "system.cards.markdown_card"
}, },
"66dcf1a2-9d83-6873-c693-d6a5d989b3f8": { "66dcf1a2-9d83-6873-c693-d6a5d989b3f8": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -735,16 +699,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "66dcf1a2-9d83-6873-c693-d6a5d989b3f8" "id": "66dcf1a2-9d83-6873-c693-d6a5d989b3f8",
"typeFullFqn": "system.cards.markdown_card"
}, },
"867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": { "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "documentation_links",
"type": "static", "type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 3, "sizeY": 3,
"config": { "config": {
@ -772,16 +731,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4" "id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4",
"typeFullFqn": "system.home_page_widgets.documentation_links"
}, },
"a23185ad-dc46-806c-0e50-5b21fb080ace": { "a23185ad-dc46-806c-0e50-5b21fb080ace": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "getting_started",
"type": "static", "type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 6.5, "sizeY": 6.5,
"config": { "config": {
@ -809,16 +763,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "a23185ad-dc46-806c-0e50-5b21fb080ace" "id": "a23185ad-dc46-806c-0e50-5b21fb080ace",
"typeFullFqn": "system.home_page_widgets.getting_started"
}, },
"b35fa1b0-7f95-0b23-fade-d066653a40e1": { "b35fa1b0-7f95-0b23-fade-d066653a40e1": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -930,16 +879,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "b35fa1b0-7f95-0b23-fade-d066653a40e1" "id": "b35fa1b0-7f95-0b23-fade-d066653a40e1",
"typeFullFqn": "system.cards.markdown_card"
}, },
"5eabde4c-095f-defc-dd09-6c0afb9b9f7b": { "5eabde4c-095f-defc-dd09-6c0afb9b9f7b": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -1063,16 +1007,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "5eabde4c-095f-defc-dd09-6c0afb9b9f7b" "id": "5eabde4c-095f-defc-dd09-6c0afb9b9f7b",
"typeFullFqn": "system.cards.markdown_card"
}, },
"451e2588-80b8-cd08-2199-1263ed4dc12c": { "451e2588-80b8-cd08-2199-1263ed4dc12c": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -1196,16 +1135,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "451e2588-80b8-cd08-2199-1263ed4dc12c" "id": "451e2588-80b8-cd08-2199-1263ed4dc12c",
"typeFullFqn": "system.cards.markdown_card"
}, },
"01b94c17-84eb-4e49-6a92-c3f108ecff8e": { "01b94c17-84eb-4e49-6a92-c3f108ecff8e": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -1417,16 +1351,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "01b94c17-84eb-4e49-6a92-c3f108ecff8e" "id": "01b94c17-84eb-4e49-6a92-c3f108ecff8e",
"typeFullFqn": "system.cards.markdown_card"
}, },
"f0479b00-ed47-0a30-0c36-bde7847aae00": { "f0479b00-ed47-0a30-0c36-bde7847aae00": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -1488,16 +1417,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "f0479b00-ed47-0a30-0c36-bde7847aae00" "id": "f0479b00-ed47-0a30-0c36-bde7847aae00",
"typeFullFqn": "system.cards.markdown_card"
}, },
"57ed0ad9-8fcf-4454-1fa7-d39e22f84570": { "57ed0ad9-8fcf-4454-1fa7-d39e22f84570": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -1579,16 +1503,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "57ed0ad9-8fcf-4454-1fa7-d39e22f84570" "id": "57ed0ad9-8fcf-4454-1fa7-d39e22f84570",
"typeFullFqn": "system.cards.markdown_card"
}, },
"0464b6bc-7863-ba75-4b84-64b8acae2af9": { "0464b6bc-7863-ba75-4b84-64b8acae2af9": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -1670,16 +1589,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "0464b6bc-7863-ba75-4b84-64b8acae2af9" "id": "0464b6bc-7863-ba75-4b84-64b8acae2af9",
"typeFullFqn": "system.cards.markdown_card"
}, },
"4c043bba-6ac1-dd23-0796-fe6ae4c3c0a3": { "4c043bba-6ac1-dd23-0796-fe6ae4c3c0a3": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -1761,16 +1675,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "4c043bba-6ac1-dd23-0796-fe6ae4c3c0a3" "id": "4c043bba-6ac1-dd23-0796-fe6ae4c3c0a3",
"typeFullFqn": "system.cards.markdown_card"
}, },
"8b8d5d4f-2c5f-c768-d4fb-68d6e0b716f4": { "8b8d5d4f-2c5f-c768-d4fb-68d6e0b716f4": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -1852,16 +1761,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "8b8d5d4f-2c5f-c768-d4fb-68d6e0b716f4" "id": "8b8d5d4f-2c5f-c768-d4fb-68d6e0b716f4",
"typeFullFqn": "system.cards.markdown_card"
}, },
"d58bfdd4-18b0-a70c-5d5f-73d12c2ea468": { "d58bfdd4-18b0-a70c-5d5f-73d12c2ea468": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -1943,16 +1847,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "d58bfdd4-18b0-a70c-5d5f-73d12c2ea468" "id": "d58bfdd4-18b0-a70c-5d5f-73d12c2ea468",
"typeFullFqn": "system.cards.markdown_card"
}, },
"a3d83738-a4f2-ea92-17d7-1a3c52aa1770": { "a3d83738-a4f2-ea92-17d7-1a3c52aa1770": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -2034,16 +1933,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "a3d83738-a4f2-ea92-17d7-1a3c52aa1770" "id": "a3d83738-a4f2-ea92-17d7-1a3c52aa1770",
"typeFullFqn": "system.cards.markdown_card"
}, },
"ff4f04a2-f739-2f3e-17dc-9ede9f2df75d": { "ff4f04a2-f739-2f3e-17dc-9ede9f2df75d": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -2235,16 +2129,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "ff4f04a2-f739-2f3e-17dc-9ede9f2df75d" "id": "ff4f04a2-f739-2f3e-17dc-9ede9f2df75d",
"typeFullFqn": "system.cards.markdown_card"
}, },
"163025d8-3ca4-dd1e-c17b-e40d8e03e8a8": { "163025d8-3ca4-dd1e-c17b-e40d8e03e8a8": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -2306,7 +2195,8 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "163025d8-3ca4-dd1e-c17b-e40d8e03e8a8" "id": "163025d8-3ca4-dd1e-c17b-e40d8e03e8a8",
"typeFullFqn": "system.cards.markdown_card"
} }
}, },
"states": { "states": {
@ -2721,4 +2611,4 @@
}, },
"externalId": null, "externalId": null,
"name": "System Administrator Home Page" "name": "System Administrator Home Page"
} }

144
ui-ngx/src/assets/dashboard/tenant_admin_home_page.json

@ -7,13 +7,7 @@
"description": "", "description": "",
"widgets": { "widgets": {
"d70cc256-4c7b-ee06-9905-b8c5e546605f": { "d70cc256-4c7b-ee06-9905-b8c5e546605f": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -72,16 +66,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "d70cc256-4c7b-ee06-9905-b8c5e546605f" "id": "d70cc256-4c7b-ee06-9905-b8c5e546605f",
"typeFullFqn": "system.cards.markdown_card"
}, },
"8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e": { "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "timeseries_bars_flot",
"type": "timeseries", "type": "timeseries",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 8, "sizeX": 8,
"sizeY": 5, "sizeY": 5,
"config": { "config": {
@ -179,7 +168,18 @@
"showLabels": true "showLabels": true
}, },
"customLegendEnabled": false, "customLegendEnabled": false,
"dataKeysListForLabels": [] "dataKeysListForLabels": [],
"showLegend": false,
"legendConfig": {
"direction": "column",
"position": "bottom",
"sortDataKeys": false,
"showMin": false,
"showMax": false,
"showAvg": true,
"showTotal": false,
"showLatest": false
}
}, },
"title": "Transport messages", "title": "Transport messages",
"dropShadow": false, "dropShadow": false,
@ -192,37 +192,21 @@
"padding": "0" "padding": "0"
}, },
"useDashboardTimewindow": false, "useDashboardTimewindow": false,
"showLegend": false,
"actions": {}, "actions": {},
"displayTimewindow": true, "displayTimewindow": true,
"showTitleIcon": false, "showTitleIcon": false,
"titleTooltip": "", "titleTooltip": "",
"widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}\n\n.tb-widget-container > .tb-widget .flot-base {\n opacity: 0.48;\n}\n", "widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}\n\n.tb-widget-container > .tb-widget .flot-base {\n opacity: 0.48;\n}\n",
"pageSize": 1024, "pageSize": 1024,
"noDataDisplayMessage": "", "noDataDisplayMessage": ""
"legendConfig": {
"direction": "column",
"position": "bottom",
"sortDataKeys": false,
"showMin": false,
"showMax": false,
"showAvg": true,
"showTotal": false,
"showLatest": false
}
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e" "id": "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e",
"typeFullFqn": "system.charts.timeseries_bars_flot"
}, },
"867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": { "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "documentation_links",
"type": "static", "type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 3, "sizeY": 3,
"config": { "config": {
@ -250,16 +234,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4" "id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4",
"typeFullFqn": "system.home_page_widgets.documentation_links"
}, },
"a23185ad-dc46-806c-0e50-5b21fb080ace": { "a23185ad-dc46-806c-0e50-5b21fb080ace": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "getting_started",
"type": "static", "type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 6.5, "sizeY": 6.5,
"config": { "config": {
@ -287,16 +266,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "a23185ad-dc46-806c-0e50-5b21fb080ace" "id": "a23185ad-dc46-806c-0e50-5b21fb080ace",
"typeFullFqn": "system.home_page_widgets.getting_started"
}, },
"d26e5cd7-75ef-d475-00c7-1a2d1114efe8": { "d26e5cd7-75ef-d475-00c7-1a2d1114efe8": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries", "type": "timeseries",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 8, "sizeX": 8,
"sizeY": 5, "sizeY": 5,
"config": { "config": {
@ -412,7 +386,18 @@
"showLabels": true "showLabels": true
}, },
"customLegendEnabled": false, "customLegendEnabled": false,
"dataKeysListForLabels": [] "dataKeysListForLabels": [],
"showLegend": false,
"legendConfig": {
"direction": "column",
"position": "bottom",
"sortDataKeys": false,
"showMin": false,
"showMax": false,
"showAvg": true,
"showTotal": false,
"showLatest": false
}
}, },
"title": "Devices activity", "title": "Devices activity",
"dropShadow": false, "dropShadow": false,
@ -430,31 +415,15 @@
}, },
"widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}\n", "widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}\n",
"pageSize": 1024, "pageSize": 1024,
"noDataDisplayMessage": "", "noDataDisplayMessage": ""
"showLegend": false,
"legendConfig": {
"direction": "column",
"position": "bottom",
"sortDataKeys": false,
"showMin": false,
"showMax": false,
"showAvg": true,
"showTotal": false,
"showLatest": false
}
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "d26e5cd7-75ef-d475-00c7-1a2d1114efe8" "id": "d26e5cd7-75ef-d475-00c7-1a2d1114efe8",
"typeFullFqn": "system.charts.basic_timeseries"
}, },
"ebbd0a6e-8a47-e770-5086-7f4974250f2d": { "ebbd0a6e-8a47-e770-5086-7f4974250f2d": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "usage_info",
"type": "static", "type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 6.5, "sizeY": 6.5,
"config": { "config": {
@ -475,16 +444,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "ebbd0a6e-8a47-e770-5086-7f4974250f2d" "id": "ebbd0a6e-8a47-e770-5086-7f4974250f2d",
"typeFullFqn": "system.home_page_widgets.usage_info"
}, },
"9e3ef045-d8bc-1640-a3f4-2dd10b19d50e": { "9e3ef045-d8bc-1640-a3f4-2dd10b19d50e": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "quick_links",
"type": "static", "type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 3, "sizeY": 3,
"config": { "config": {
@ -507,16 +471,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "9e3ef045-d8bc-1640-a3f4-2dd10b19d50e" "id": "9e3ef045-d8bc-1640-a3f4-2dd10b19d50e",
"typeFullFqn": "system.home_page_widgets.quick_links"
}, },
"6d6e2b1d-6ce7-4678-3745-c6b0897b2674": { "6d6e2b1d-6ce7-4678-3745-c6b0897b2674": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "dashboards",
"type": "static", "type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 6.5, "sizeY": 6.5,
"config": { "config": {
@ -537,16 +496,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "6d6e2b1d-6ce7-4678-3745-c6b0897b2674" "id": "6d6e2b1d-6ce7-4678-3745-c6b0897b2674",
"typeFullFqn": "system.home_page_widgets.dashboards"
}, },
"35d3ac3b-f1af-dc3d-a1d1-11f351d16fc6": { "35d3ac3b-f1af-dc3d-a1d1-11f351d16fc6": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -672,16 +626,11 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "35d3ac3b-f1af-dc3d-a1d1-11f351d16fc6" "id": "35d3ac3b-f1af-dc3d-a1d1-11f351d16fc6",
"typeFullFqn": "system.cards.markdown_card"
}, },
"7ac20b6a-dc40-b18e-9f5f-bca20bc693bb": { "7ac20b6a-dc40-b18e-9f5f-bca20bc693bb": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest", "type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5, "sizeX": 5,
"sizeY": 3.5, "sizeY": 3.5,
"config": { "config": {
@ -826,7 +775,8 @@
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
"id": "7ac20b6a-dc40-b18e-9f5f-bca20bc693bb" "id": "7ac20b6a-dc40-b18e-9f5f-bca20bc693bb",
"typeFullFqn": "system.cards.markdown_card"
} }
}, },
"states": { "states": {
@ -1101,4 +1051,4 @@
}, },
"externalId": null, "externalId": null,
"name": "Tenant Administrator Home Page" "name": "Tenant Administrator Home Page"
} }

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

@ -2784,6 +2784,12 @@
"lwm2m-server-secret-key": "LwM2M server secret key", "lwm2m-server-secret-key": "LwM2M server secret key",
"lwm2m-server-public-key-id": "LwM2M server public key or id" "lwm2m-server-public-key-id": "LwM2M server public key or id"
}, },
"snmp": {
"host": "SNMP host",
"port": "SNMP port",
"version": "SNMP version (v1, v2c or v3)",
"community-string": "SNMP community string"
},
"isgateway": "Is Gateway", "isgateway": "Is Gateway",
"activity-time-from-gateway-device": "Activity time from gateway device", "activity-time-from-gateway-device": "Activity time from gateway device",
"description": "Description", "description": "Description",
@ -5329,6 +5335,9 @@
"input-fields-alignment": "Input fields alignment", "input-fields-alignment": "Input fields alignment",
"input-fields-alignment-column": "Column (default)", "input-fields-alignment-column": "Column (default)",
"input-fields-alignment-row": "Row", "input-fields-alignment-row": "Row",
"layout": "Layout",
"row-gap": "Gap between rows in pixels",
"column-gap": "Gap between columns in pixels",
"latitude-field-required": "Latitude field required", "latitude-field-required": "Latitude field required",
"longitude-field-required": "Longitude field required", "longitude-field-required": "Longitude field required",
"attribute-settings": "Attribute settings", "attribute-settings": "Attribute settings",
@ -5384,6 +5393,12 @@
"ability-to-edit-attribute-disabled": "Disabled", "ability-to-edit-attribute-disabled": "Disabled",
"ability-to-edit-attribute-readonly": "Read-only", "ability-to-edit-attribute-readonly": "Read-only",
"disable-on-datakey-name": "Disable on false value of another datakey (specify datakey name)", "disable-on-datakey-name": "Disable on false value of another datakey (specify datakey name)",
"field-appearance": "Field appearance",
"appearance-fill": "Fill",
"appearance-outline": "Outline",
"subscript-sizing": "Subscript sizing",
"subscript-sizing-fixed": "Fixed",
"subscript-sizing-dynamic": "Dynamic",
"slide-toggle-settings": "Slide toggle settings", "slide-toggle-settings": "Slide toggle settings",
"slide-toggle-label-position": "Slide toggle label position", "slide-toggle-label-position": "Slide toggle label position",
"slide-toggle-label-position-after": "After", "slide-toggle-label-position-after": "After",

Loading…
Cancel
Save