diff --git a/application/src/main/data/json/system/widget_types/alarms_table.json b/application/src/main/data/json/system/widget_types/alarms_table.json index 2abf10e995..952047d236 100644 --- a/application/src/main/data/json/system/widget_types/alarms_table.json +++ b/application/src/main/data/json/system/widget_types/alarms_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.alarmsTableWidget.onEditModeChanged();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.alarmsTableWidget.onEditModeChanged();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-alarms-table-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/asset_admin_table.json b/application/src/main/data/json/system/widget_types/asset_admin_table.json index a74d2dd644..cd373b9635 100644 --- a/application/src/main/data/json/system/widget_types/asset_admin_table.json +++ b/application/src/main/data/json/system/widget_types/asset_admin_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-entities-table-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/device_admin_table.json b/application/src/main/data/json/system/widget_types/device_admin_table.json index 3265554da1..014734df54 100644 --- a/application/src/main/data/json/system/widget_types/device_admin_table.json +++ b/application/src/main/data/json/system/widget_types/device_admin_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-entities-table-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/digital_horizontal_bar.json b/application/src/main/data/json/system/widget_types/digital_horizontal_bar.json index deb82422c6..70e470e273 100644 --- a/application/src/main/data/json/system/widget_types/digital_horizontal_bar.json +++ b/application/src/main/data/json/system/widget_types/digital_horizontal_bar.json @@ -15,7 +15,9 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 80) {\\n\\tvalue = 80;\\n} else if (value > 160) {\\n\\tvalue = 160;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#008000\",\"#fbc02d\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ffffff\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"horizontalBar\",\"showTitle\":false,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital horizontal bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "hasBasicMode": true, + "basicModeDirective": "tb-digital-simple-gauge-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 80) {\\n\\tvalue = 80;\\n} else if (value > 160) {\\n\\tvalue = 160;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#008000\",\"#fbc02d\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ffffff\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"horizontalBar\",\"showTitle\":false,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital horizontal bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"configMode\":\"basic\"}" }, "externalId": null, "tags": [ diff --git a/application/src/main/data/json/system/widget_types/digital_speedometer.json b/application/src/main/data/json/system/widget_types/digital_speedometer.json index a3844fc560..4ac6ac7c1d 100644 --- a/application/src/main/data/json/system/widget_types/digital_speedometer.json +++ b/application/src/main/data/json/system/widget_types/digital_speedometer.json @@ -15,7 +15,9 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 45) {\\n\\tvalue = 45;\\n} else if (value > 130) {\\n\\tvalue = 130;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#008000\",\"#fbc02d\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ffffff\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"arc\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital speedometer\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "hasBasicMode": true, + "basicModeDirective": "tb-digital-simple-gauge-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 45) {\\n\\tvalue = 45;\\n} else if (value > 130) {\\n\\tvalue = 130;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#008000\",\"#fbc02d\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ffffff\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"arc\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital speedometer\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"configMode\":\"basic\"}" }, "externalId": null, "tags": [ diff --git a/application/src/main/data/json/system/widget_types/digital_thermometer.json b/application/src/main/data/json/system/widget_types/digital_thermometer.json index ab75474f9c..df7d63ea1d 100644 --- a/application/src/main/data/json/system/widget_types/digital_thermometer.json +++ b/application/src/main/data/json/system/widget_types/digital_thermometer.json @@ -15,7 +15,9 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < -60) {\\n\\tvalue = 60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":60,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":1,\"levelColors\":[\"#304ffe\",\"#7e57c2\",\"#ff4081\",\"#d32f2f\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"dashThickness\":1.5,\"minValue\":-60,\"gaugeColor\":\"#333333\",\"neonGlowBrightness\":35,\"gaugeType\":\"donut\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital thermometer\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "hasBasicMode": true, + "basicModeDirective": "tb-digital-simple-gauge-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < -60) {\\n\\tvalue = 60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":60,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":1,\"levelColors\":[\"#304ffe\",\"#7e57c2\",\"#ff4081\",\"#d32f2f\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"dashThickness\":1.5,\"minValue\":-60,\"gaugeColor\":\"#333333\",\"neonGlowBrightness\":35,\"gaugeType\":\"donut\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital thermometer\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"configMode\":\"basic\"}" }, "externalId": null, "tags": [ diff --git a/application/src/main/data/json/system/widget_types/digital_vertical_bar.json b/application/src/main/data/json/system/widget_types/digital_vertical_bar.json index aecd305300..7daf1f3b9c 100644 --- a/application/src/main/data/json/system/widget_types/digital_vertical_bar.json +++ b/application/src/main/data/json/system/widget_types/digital_vertical_bar.json @@ -15,7 +15,9 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":60,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#3d5afe\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":14},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":8,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#cccccc\"},\"neonGlowBrightness\":20,\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"verticalBar\",\"showTitle\":false,\"minValue\":-60,\"dashThickness\":1.2,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital vertical bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "hasBasicMode": true, + "basicModeDirective": "tb-digital-simple-gauge-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":60,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[\"#3d5afe\",\"#f44336\"],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":14},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":8,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#cccccc\"},\"neonGlowBrightness\":20,\"showUnitTitle\":true,\"gaugeColor\":\"#171a1c\",\"gaugeType\":\"verticalBar\",\"showTitle\":false,\"minValue\":-60,\"dashThickness\":1.2,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Digital vertical bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"configMode\":\"basic\"}" }, "externalId": null, "tags": [ diff --git a/application/src/main/data/json/system/widget_types/entities_table.json b/application/src/main/data/json/system/widget_types/entities_table.json index ce3ec3add1..8e79ea96d0 100644 --- a/application/src/main/data/json/system/widget_types/entities_table.json +++ b/application/src/main/data/json/system/widget_types/entities_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-entities-table-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/gauge.json b/application/src/main/data/json/system/widget_types/gauge.json index f5494f8b4a..3739623ccf 100644 --- a/application/src/main/data/json/system/widget_types/gauge.json +++ b/application/src/main/data/json/system/widget_types/gauge.json @@ -15,7 +15,9 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":36,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":0,\"gaugeColor\":\"#eeeeee\",\"showTitle\":true,\"gaugeType\":\"arc\"},\"title\":\"Gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "hasBasicMode": true, + "basicModeDirective": "tb-digital-simple-gauge-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":36,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":0,\"gaugeColor\":\"#eeeeee\",\"showTitle\":true,\"gaugeType\":\"arc\"},\"title\":\"Gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"configMode\":\"basic\"}" }, "externalId": null, "tags": [ diff --git a/application/src/main/data/json/system/widget_types/horizontal_bar.json b/application/src/main/data/json/system/widget_types/horizontal_bar.json index 27ec8d2367..08abe291df 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_bar.json +++ b/application/src/main/data/json/system/widget_types/horizontal_bar.json @@ -15,7 +15,9 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":0,\"gaugeColor\":\"#eeeeee\",\"showTitle\":true,\"gaugeType\":\"horizontalBar\"},\"title\":\"Horizontal bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "hasBasicMode": true, + "basicModeDirective": "tb-digital-simple-gauge-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":18,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":0,\"gaugeColor\":\"#eeeeee\",\"showTitle\":true,\"gaugeType\":\"horizontalBar\"},\"title\":\"Horizontal bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"configMode\":\"basic\"}" }, "externalId": null } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/lcd_bar_gauge.json b/application/src/main/data/json/system/widget_types/lcd_bar_gauge.json index b2c8caa4c3..bab8886b0b 100644 --- a/application/src/main/data/json/system/widget_types/lcd_bar_gauge.json +++ b/application/src/main/data/json/system/widget_types/lcd_bar_gauge.json @@ -15,7 +15,9 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#babab2\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\"linear\",\"refreshAnimationTime\":700,\"startAnimationType\":\"linear\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"400\",\"size\":16},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":1.5,\"decimals\":0,\"showUnitTitle\":true,\"defaultColor\":\"#444444\",\"gaugeType\":\"verticalBar\",\"units\":\"%\"},\"title\":\"LCD bar gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "hasBasicMode": true, + "basicModeDirective": "tb-digital-simple-gauge-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#babab2\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\"linear\",\"refreshAnimationTime\":700,\"startAnimationType\":\"linear\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"400\",\"size\":16},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":1.5,\"decimals\":0,\"showUnitTitle\":true,\"defaultColor\":\"#444444\",\"gaugeType\":\"verticalBar\",\"units\":\"%\"},\"title\":\"LCD bar gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"configMode\":\"basic\"}" }, "externalId": null } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/lcd_gauge.json b/application/src/main/data/json/system/widget_types/lcd_gauge.json index bfe7a64a5f..175dc089f4 100644 --- a/application/src/main/data/json/system/widget_types/lcd_gauge.json +++ b/application/src/main/data/json/system/widget_types/lcd_gauge.json @@ -15,7 +15,9 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 180) {\\n\\tvalue = 180;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#babab2\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\"linear\",\"refreshAnimationTime\":700,\"startAnimationType\":\"linear\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":1.5,\"decimals\":0,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"defaultColor\":\"#444444\",\"gaugeType\":\"arc\"},\"title\":\"LCD gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "hasBasicMode": true, + "basicModeDirective": "tb-digital-simple-gauge-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 180) {\\n\\tvalue = 180;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#babab2\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":180,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\"linear\",\"refreshAnimationTime\":700,\"startAnimationType\":\"linear\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":1.5,\"decimals\":0,\"unitTitle\":\"MPH\",\"showUnitTitle\":true,\"defaultColor\":\"#444444\",\"gaugeType\":\"arc\"},\"title\":\"LCD gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"configMode\":\"basic\"}" }, "externalId": null } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/mini_gauge.json b/application/src/main/data/json/system/widget_types/mini_gauge.json index 2789102c6c..3d1b01fa58 100644 --- a/application/src/main/data/json/system/widget_types/mini_gauge.json +++ b/application/src/main/data/json/system/widget_types/mini_gauge.json @@ -15,7 +15,9 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#7cb342\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":0,\"decimals\":0,\"roundedLineCap\":true,\"gaugeType\":\"donut\"},\"title\":\"Mini gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "hasBasicMode": true, + "basicModeDirective": "tb-digital-simple-gauge-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#7cb342\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":0,\"decimals\":0,\"roundedLineCap\":true,\"gaugeType\":\"donut\"},\"title\":\"Mini gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"configMode\":\"basic\"}" }, "externalId": null } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json index 37f7df0ae6..f3929d88f1 100644 --- a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json +++ b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json @@ -7,15 +7,17 @@ "descriptor": { "type": "static", "sizeX": 7.5, - "sizeY": 3, + "sizeY": 4, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '250px',\n previewHeight: '250px',\n embedTitlePanel: true\n };\n};", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-mobile-app-qr-code-widget-settings", - "defaultConfig": "{\n \"showTitle\": true,\n \"backgroundColor\": \"rgb(255, 255, 255)\",\n \"color\": \"rgba(0, 0, 0, 0.87)\",\n \"padding\": \"8px\",\n \"settings\": {\n \"useSystemSettings\": true,\n \"androidConfig\": {\n \"enabled\": true\n },\n \"iosConfig\": {\n \"enabled\": true\n },\n \"qrCodeConfig\": {\n \"badgeEnabled\": true,\n \"badgeStyle\": \"ORIGINAL\",\n \"badgePosition\": \"RIGHT\",\n \"qrCodeLabelEnabled\": true,\n \"qrCodeLabel\": \"Scan to connect or download mobile app\"\n }\n },\n \"title\": \"Mobile app QR code\",\n \"dropShadow\": true,\n \"enableFullscreen\": false,\n \"widgetStyle\": {},\n \"widgetCss\": \"\",\n \"pageSize\": 1024,\n \"noDataDisplayMessage\": \"\",\n \"showTitleIcon\": false,\n \"titleTooltip\": \"\",\n \"titleStyle\": {\n \"fontSize\": \"16px\",\n \"fontWeight\": 400\n }\n}\n" + "hasBasicMode": true, + "basicModeDirective": "tb-mobile-app-qr-code-basic-config", + "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"useSystemSettings\":true,\"qrCodeConfig\":{\"badgeEnabled\":true,\"badgePosition\":\"RIGHT\",\"qrCodeLabelEnabled\":true,\"qrCodeLabel\":\"Scan to connect or download mobile app\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Mobile app QR code\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"enableFullscreen\":false,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\"}" }, "tags": [ "ios", diff --git a/application/src/main/data/json/system/widget_types/neon_gauge.json b/application/src/main/data/json/system/widget_types/neon_gauge.json index 4f595392b6..a4f03169cf 100644 --- a/application/src/main/data/json/system/widget_types/neon_gauge.json +++ b/application/src/main/data/json/system/widget_types/neon_gauge.json @@ -15,7 +15,9 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":70,\"dashThickness\":1,\"gaugeType\":\"arc\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Neon gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "hasBasicMode": true, + "basicModeDirective": "tb-digital-simple-gauge-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":70,\"dashThickness\":1,\"gaugeType\":\"arc\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Neon gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"configMode\":\"basic\"}" }, "externalId": null } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/simple_gauge.json b/application/src/main/data/json/system/widget_types/simple_gauge.json index 1683efee14..c9e451039c 100644 --- a/application/src/main/data/json/system/widget_types/simple_gauge.json +++ b/application/src/main/data/json/system/widget_types/simple_gauge.json @@ -15,7 +15,9 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#ef6c00\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":0,\"decimals\":0,\"gaugeColor\":\"#eeeeee\",\"gaugeType\":\"donut\"},\"title\":\"Simple gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "hasBasicMode": true, + "basicModeDirective": "tb-digital-simple-gauge-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#ef6c00\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":0,\"dashThickness\":0,\"decimals\":0,\"gaugeColor\":\"#eeeeee\",\"gaugeType\":\"donut\"},\"title\":\"Simple gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"configMode\":\"basic\"}" }, "externalId": null } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/simple_neon_gauge.json b/application/src/main/data/json/system/widget_types/simple_neon_gauge.json index fd84396430..ab46ed93e6 100644 --- a/application/src/main/data/json/system/widget_types/simple_neon_gauge.json +++ b/application/src/main/data/json/system/widget_types/simple_neon_gauge.json @@ -15,7 +15,9 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#388e3c\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":1,\"levelColors\":[],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"gaugeType\":\"donut\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Simple neon gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "hasBasicMode": true, + "basicModeDirective": "tb-digital-simple-gauge-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#388e3c\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#000000\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":1,\"levelColors\":[],\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"style\":\"normal\",\"weight\":\"500\",\"size\":32},\"minMaxFont\":{\"family\":\"Segment7Standard\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\"},\"neonGlowBrightness\":40,\"dashThickness\":1.5,\"gaugeType\":\"donut\",\"animation\":true,\"animationDuration\":500,\"animationRule\":\"linear\"},\"title\":\"Simple neon gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"configMode\":\"basic\"}" }, "externalId": null } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/vertical_bar.json b/application/src/main/data/json/system/widget_types/vertical_bar.json index 31d3b3fdd9..84fdb8c1c8 100644 --- a/application/src/main/data/json/system/widget_types/vertical_bar.json +++ b/application/src/main/data/json/system/widget_types/vertical_bar.json @@ -15,7 +15,9 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-digital-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#f57c00\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":12,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":1.5,\"gaugeColor\":\"#eeeeee\",\"showTitle\":false,\"gaugeType\":\"verticalBar\"},\"title\":\"Vertical bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "hasBasicMode": true, + "basicModeDirective": "tb-digital-simple-gauge-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#f57c00\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"maxValue\":100,\"minValue\":0,\"donutStartAngle\":90,\"showValue\":true,\"showMinMax\":true,\"gaugeWidthScale\":0.75,\"levelColors\":[],\"refreshAnimationType\":\">\",\"refreshAnimationTime\":700,\"startAnimationType\":\">\",\"startAnimationTime\":700,\"titleFont\":{\"family\":\"Roboto\",\"size\":12,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#999999\"},\"labelFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\"},\"valueFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"size\":12,\"color\":\"#666666\"},\"minMaxFont\":{\"family\":\"Roboto\",\"size\":8,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#666666\"},\"neonGlowBrightness\":0,\"decimals\":0,\"dashThickness\":1.5,\"gaugeColor\":\"#eeeeee\",\"showTitle\":false,\"gaugeType\":\"verticalBar\"},\"title\":\"Vertical bar\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"configMode\":\"basic\"}" }, "externalId": null } \ No newline at end of file diff --git a/application/src/main/data/json/tenant/dashboards/gateways.json b/application/src/main/data/json/tenant/dashboards/gateways.json index 59e5672b53..d98daa3632 100644 --- a/application/src/main/data/json/tenant/dashboards/gateways.json +++ b/application/src/main/data/json/tenant/dashboards/gateways.json @@ -1,6 +1,6 @@ { "title": "ThingsBoard IoT Gateways", - "image": "tb-image:Z2F0ZXdheS1kYXNoYm9hcmQucG5n:Z2F0ZXdheS1kYXNoYm9hcmQucG5n;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAagAAAGoCAYAAAATsnHAAAAmgUlEQVR4Ae3de2xc5bnv8cd2jO04tmM7V8dJHEfQxpB9ykXFFNhtyWkq0cKptPNHuIgKyt7/UIlIRagCCaFKjRClbegp0tERUG1EaCqlPToN7D8S0tNuOBC0S6E7wSmhnkBw7Ni5OI7jeBw7yZ7HzkrGk5m13nWZmXfNfD/SlMRek4uh6+fnfZ/1vBUCP+ZkeVWmXhUXfywXf14pAMrN+YzXhYv/nMrygoEKQTYaMFelvarlchgBQFhnU69zqVfy4o/1dV4wCwE1Q4NnrsyEUZ1croYAoFA0pCZTrzMXf1z2lVY5B1StzIRSjcwEEwDYRENqQmYCKyllqNwCygmlemG5DkB8aDWlYXVayiisyiGgCCUApcQJq1MyU2WVrFINKA2ieXJ5CQ8ASpEG1KjMVFYlp9QCSoOpMfVqEKolAOXDqapOSgk1V5RKQGnX3XyZWcbLm/vuu6/xrrvuau/s7Gxvbm5ub2hoaK+urm6sra1tr6qqaqysrNRwlNTH2gVAWTl//vypc+fOnXL+OTU1dWp8fLxvdHS0b3h4uC+RSPRt3LixR/JvTEokqOIeUBpMCyQPy3gaRo8++mj3kiVLupqamrrq6uq6CB4AYU1MTPSkXn0jIyM9Bw4c2POrX/2qZ+vWrackerEPqrgGVOQVkwbSE088sb61tbWrpaVlPWEEoFA0tMbGxnp6e3t3/vGPf+x5/PHH+yQ6sQ2quAVUpHtMb775ZvcXv/jF7tRyXffcuXO7BQAscObMmT39/f0733///T0RLgtqSGlDRWwmVsQpoLQjr1lCTnl49tln2++9994NixYt2kCVBMB2k5OTfSdOnNj5yiuvvBxBZaVV1IjEpOsvDgEVep9Jl++efvrpDW1tbeuplADElVZWiURi+9q1a7dLOLFY9rM9oHQ5r0kCLuc51dLSpUsfcjrsACDutKoaHR3d89JLL20JUVXpUp82Z4yIpWwNqFBVk+4t3XLLLZuolgCUutTy3/aQQaWjk46LhdWUjQEVuGoimACUq5BBpdXUsFi2N2VTQGkgaet4g/ikS3mPPPLIcwQTgHIXMqi0y0/3pqzo9LMloHRJb7H47NDT5oef//znmxYuXPiQAAAuCRFUutQ3KBYs+dkQUNo+3io+l/R6e3sf6ujo2ETzAwBkp80UH3/88ZYAXX9aQem+1BkpomIPVNUlvYV+/hzbtm3rGhsb29bZ2fkU4QQAuemzntddd91zZ8+efVu3Qny8Ve/Jem9ukiIqZgXVIj73m/r6+jYtW7ZskwAAfDt8+PCW9vb2LeKP7kudkCIoRgWlv+cS8RFOmvzJZPLfCCcACE7voQGqKb1X+1rpikqhKyhtgtC/6FWmb2CvCQCipUeCfPrpp1tWr179so+36eGIR6WAzROFTESnU88onLRDb2ho6Cn2mgAgWnpP1XvrkSNHntJ7reHb9N7tu9s6jCopDF9t5Fp+bt68+V8bGxvXCwAgL+bNm3f9XSkNDQ07d+3aZXImlRY12nk9LgV4VqoQS3y+wkmnQXz961//31RNAFAY2o7+u9/97l98HO1RkGel8h1QvsLpo48+2tDV1fWcAAAKLpFI/MjHvlTeQyqfAaWloPGeEy3kAFB8PlvRtXFCQyovy335bJLQaeSEEwDEiN6L9Z5seLne4xdKnuSrgjJ+CFc79ZilBwB28VlJ5eVh3nwElI4vMhqPMTw8/Nz8+fM3CADAOjpwtrW19THDy3UKeqSHH0a9xKfth0bhpCUk4QQA9mppadmgz0oZXq7FyVyJUJQBpZ16rSYXsucEAPGwePHih3zsSWkGRPYgb1QB5XTsef56hBMAxIuPxgnjLDARVUBpaeeZmjpXj3ACgPjRe/fevXtNtmU0C+ZLBKIIqHli0LGn5zjp7CcBAMSSni2l034MLtVMCD0NKOwsPmc6uWs3oM7We/DBB/+1qqqK8UUAEGMdHR3r582bt8Ngdl+NzJzIG/gh3rABtSj1qna7QCflPvPMM/9HT3YUAECsVVRU1Nx0003dBw8e3JFa8ptwu1RmHuQdk4DCPAdl9LwTD+ICQOkZHBx8ecmSJT8yuHQ49TKZlH6FoHtQurTnue+kTRGEEwCUHm0/13u8waVayARqPQ+6xKejjGrcLtB9pzvvvPN/ajkoAICS09TUdL3BflTgpb4gS3zatef5QK6ee8++EwCUtomJiZ7a2to7DS7V4+LPiA9+l/j0es99J32gi3ACgNJXU1PTdejQIZPnW5vFZ+b4DShtE3ddS9SlPR7GBYDysXz58k36rKvHZUa9C+n8LPHpL77M6yKW9gCg/Jw5c2ZPfX39Ro/L9JmoATE8hddPBeU5ukKPbCecAKD8zJ07t9ugq08zx3gMkmlAafVU73aBLu1dffXVLO0BQJnq6OjYpMMZPC7TLDFqOzdd4lsgHgF1/Pjx5/TsEAEAlC3DB3iTeqnHNUYVlFH1RDgBAPQBXs0Ej8tqxaCKMqmgqJ4AAMYMGyb0wd1jbhd4VVBUTwAAX7RhwuBYDs0W1wzyqqCongAAvhlWUSdTr5Fcn/SqoDzn7RFOAIBMhlWUdvzlzCG3Cspz5h7VEwAgF8MqKudxHG4VlOtICqonAIAbraIMOvrqcn0iVwWlo9GXiguqJwCAl88//3zLihUrtnhcps9EJTM/mKuC8noSWBoaGrzWFgEAZW7ZsmUPGUyXyFpF5Qoo1+YIZu4BAExUVlY2Pv30016rbfOyvjfLxzyf8O3o6GBpDwBgpK2tbb3HJZpFtdk+mClrkjl0w0s3vgQAAAOaGZs3b/bdLJEtoFyX9x544AGvceoAAMxy//33+17mywwoz+W9lpYWr1INAIBZlixZ4hVQVyzzZQbUXHGhR/rSHAEA8Euzw2CyxKxlvsyAcl3eu/HGG9l7AgAEcs0113hlSM4KSpf2rnJ7p0EnBgAAWbW2tnoFlGbQpVyqzPhETnTvAQDC0AwxeGj30lZTZbYPZvO1r32tSwAACOGHP/yh10pctfODymwfzGb16tUs7wEAQlm4cKFXsXNFBaX/dF3iq6+vp4ICAIRi8KiS9kNMZ5MTUK7hpGuGNTU1BBQAIBRtNzfYh5rOJKOAevDBBwknAEAkvv/975t0810KqFq3Kw161wEAMLJs2TKvomdWQFW5XdnU1EQFBQCIhEGmTDftGS3xpfafGG8EAIhEXV2dV0BNz4StFI/hsIoGCQBAVAwaJaazyTOgdECsAAAQobvuustrZc47oDo7O1neAwBEauXKleEDqrm5mYACAERqwYIF4QOqoaGBgAIARMogW6YDqtLtirq6OgIKABCp6upqr2kSFZ4BNWfOHK9fBAAAX2pra72Kn0oNpwq3K6qqqggoAECkDLLFe4mvsrKSgAIARMokWzwDigoKAFAElVRQAICC02kSHpdUVgoAABYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWIqAAAFYioAAAViKgAABWmiMAIpfYOyIf/GFIBhKnZXxsSpoX1Urz4lpZdV2T3LBukQDwVpF6rXS74MKFC58KACMaTL99/oCcHJrIeY2G1R33rCCoUPYqKio6XD8vBBQQid2vHZI/bDtkfP0dG1fIuntXCFCuvAKKPSggAn7DSen1+j4A2RFQQEjDg0nf4eTQ9x1MLQsCuBIBBYS0+9fhqqCw7wdKFQEFhJAcm5ru1gvj4L6R6V8HwGwEFBBCf2JMopDYxzIfkImAAkIYHkpKFE4OTgiA2QgowALjLPEBVyCggBDq6qMZxrK0s14AzEZAASF0rm2SKOh0CQCzEVBACLWpCkrn64Wh4bR0FRUUkImAAkJad0+4cUV33MO4IyAbAgoIaVVqme/6O4INfr1h3WKGxgI5EFBABL79z52+l+n0+m89vEoAZMc0cyBCb7yYkHd+3+953a13t00v7dXWcyQbyhfHbQAFpsNjdb6ejjBKPxdKw0iX87q6W0M3VgClgIACikhn7CXHzqXCqYpqCcjgFVD8PwbIIw0lggkIhiYJAICV+NYOyIN9u0QaFqReC2d+PpXaiho/JXL6qEhnt0hdowDwQEABeTDQk3rl+JwGFABvLPEBAKxEQAEArERAAQCsREABAKxEQAEArEQXH5AHtbSRA6Ex6ggAUBReo45Y4gMAWImAAgBYiT0oxJZOCu9PjMnAwbHpHyM4PTyxc20Tg21hFf5rRCztfu2QvLOjn2CKkIbTV+5qk3X3rhDABjRJIFY0kF798f7pwwCRH1pNPbx5LdUU8o4mCZQUrZwIp/zSJdPtWz4RoNgIKMRGYu/I9LIe8m//e8fl4F6+EUBxEVCIjb/sHhQUzu5fHxKgmAgoxMaR1NITCmeArzeKjF1QxIbJDVM39pd21k//2GuJSq+bv6hWBhKn5eTQhGA2OiRRbAQUSsa6e1bIHfdcbpEeHkzKi0/uvSJ8NMTuf3KNrLqu6dLHdPnwjRcPclMGLMISH0rCDesWzwon1by4Vh7+8dorrv3Ww52zwsl5/1fubhMA9iCgUBJuWLco68c1pK4Mo+zX3kpAAVYhoFASmlN7SbmkP3Bqeh2A4iOgUBJ69hzP+bmBg6cv/Xh4KJlzn0n3rADYg4BCSXjn9/1ZO/F08kTmx3NNSdAmCQD2YE0DJUEroxef2DvdKKGz5MZTVdL+VFWVbfKETklwrtU9Kq2c/vBrRigBtmFYLGLjybvfFhTWj39/mwD5wrBYAEAsEVAAACsRUAAAKxFQAAArEVAAACsRUAAAKxFQAAArEVCIjfmLagSF4za3ECgEAgqxoUdioHBWrW0SoJgIKMTGDXcsYuJ4Aa3LOF8LKDQCCrGhc/PWbeSmWQgaTiypotgIKMTKV/5HG9/Z55l+fe/gawwLMCwWsTSQGJM3XkrIwb1MII+K7jlpOGWeQAzki9ewWAIKsaaHDw4cHBMEp/t6zanlPPb3UGgEFADAShy3AQCIJWp6wND4qdyfq65J/Z8pQNPb1ITI5ETuz9c1ClC2CCjA0Ec7RYYPZ//ctd8QaesS34Z6U7/uruyf03C67UEByhZLfAAAKxFQAAArEVAAACsRUAAAKxFQAAArEVAAACsRUAAAKxFQAAArEVAAACsRUAAAKxFQAAArEVAAACsRUAAAKzHNHCVBj6043CNy+qj7sRhhjB6TyLkd0aHHcPx5u4T6tRetDjZlHbABAYXY608F08f/PhNSxRL03Ca39+nfJ9fxHqaOJkR63xP54j+KLFwtQKywxIdY690zc55SMcNJNSyUQPR9QQ469COZqig/fD0V5PsFiBUCCrE1mlrOS7wnRadLaGFCZsWXpCA+/lP+lj+BfCCgEFu5TqItJF2iW32zhLLy+sIc7a5Vpg1fM8AUAYVY0kpAK6hi0lC56Z9EakOGi1Zf+usUIqSG+4q/HAqYIqAQS8UMJw0UXdbrvjd8ODlqL4Zd2xrJO5b5EBd08SGW3KoAbTzI176OhlNLe34aGzSkrl0v8oWvzgRwmCBx62qkgkJcEFAoOU6FE1f6529OhWCzBKfdjQQR4o4lPgCAlQgoAICVCCgAgJUIKACAlQgoAICV6OJDLOlDrbk69eoapOSc6JtpO9e/t7a5e9Ep5lNns38uSIu8398fiEJF6rXS7YILFy58KgCKQkPhr6/PfjB5adfMdPJ8D5l16JEf6VPVnYeKCzH5AqWtoqKiw+3zLPEBFvto55VTMwZ6RD77QApCB8xmHvmh09GZ6YdCIKAAS7mdB3XoQymIoUT2jw/3MTIJ+UdAAZaadJkEUagpEUlCCEVEQAEArERAAQCsRJs5rJAcm5LhwQkZT/2z0FoW18r8RQVqiTM0PDQhA58k5eRw7msSeyXv3H7/zw+INLTMkaWd9QLkA23mKLq/7B6SN15MTIdUsXzl7jb51sOdUmz////2yzs7DsvJofiMIm9eVCt33LNCbli3SAA/vNrMCSgU1fBgUp775z+LDfQmuy71KgYN51d/vF8O7huRuFq6ql7uf7LLumoU9uI5KFht968PiS3e+X2/FMuLT+yNdTipgYNjqZDtESAqBBSKyqalrGItMf5l9+D0zb0U6N/Dpm86EG8EFFBkpXZDL2YlitJCQAFFNJAYi1VDhAmtREulIkRx0WaOopq/uEbmD5Xvprqftvra+jmpV5UUS3LsnPEyqAavNk0AYRBQKKoNj14j5UBHE/3tTyJHE7PHFB3pT3q+99a726Y7DDWgik27Lj/4w5DnsqQOkz2+b/bHdAr76puZgg5zBBRQAO++Fmyunba931Gk1vdsmhfPPPNUO3eOvPFSwtd7dQr76aMi3fcKYIQ9KCDPhnqDhZNWTDaFU7ob/vuiQBWdHh2iXw/ABBUUiqq/x/zYhtXdEojeEDPPVIr693Bj+ntnqrNgSS8XZz8sSGu+fj30xF/ACwGFotKAynXmUaYwATWw3+zafAQUey6z8fWAKZb4gDzTaoGb8gz9OrR1CWCEgALybE6NyE3/JNLcLmVN//76dQBMscQHFEBt48zNWVvM09vM//Ntkb8ZLj9meutls+talotc+43LP//rDpFThvtitz8kgXzhqyL/cOvsj9VSRcInAgooIK2m5qQ9l1wV4hnl5KjZdZlNKHqUvOl7g5pzFYGE8FjiAwBYiQoKsXGiTwIZz3O14Ie2WE+mLfGdDtiCbjut2jL/fbWU+R4c/COgEBvv/1ZiS4Ppw9evfGD3SIkO/k68J3ImYxqSsw9HRyNMscQH5JlWE9nCqdzo3//Pv53dJAK4IaCAPBvuI5wc+nXo59BdGCKggDwbJ5xmmaSCgiECCkVVDq3INAfM1rBQACMEFIqq2qKzCufk6c+iExTKfYqEQ8OJQbEwRRcfisqm76bz+Wf50rdFet+b2Y9KX+KqPiGBmVafmcGr76vNc+t9dZYHdZd1iaz4kgDGCCgUlX43raev2qBtjeSNhsQX/vHKj7+/W2TvBxLI7Q9KINetl7y75qsiN6wTIBSW+FBUeuO2ZfmLvSLALgQUim71zVJ0egQEs+MAu7DEh6JzmgiGA44yikIhQlIfUD3cM/tB1aMhjj/v3WN2XeYZTIU6xbi3/vLPtVLmXCz4RUAhL/QmrM0Apjck3Z/Z85oUhd6E8109aSh8/O9XTlE4GmLUkY4TMqHhnxlQ+T7F+GhCpGp89scOpP7+167P714fSgsBBV+cG6xbS7ZWBft2zVyrN37tYPPqkNPPa0jpTbyQ9PftzHP1pNWKLY0gxfbxn1KVVGf+WvpRWtiDgjFdUvp//2vm9eGO3DPV/pZWKTjz10ysuD74d+xBaDgV4oTXoRDLeKVG/7tg1BFMEVAwojfZ9CUlXcLJtgei1ULm3Dm9KZnueWg1U4iQco4f5zv5wmPUEUyxxAcjo1nOLTLdx/BLQ0r3KbTyinqOnQaSBmAhHxhtZLTPLLTzwxQVFALLtsSXa3SR3w493bu67cHUpvo3onlOygkmfbi10NMM9M9fyKVLm+nXgbFPMEUFBSN1Psbq1DaklvkyRumMBjw5VrvP9JW8eEKr/jp6Qq7XmUL659CwnLdgpoIp9k1Rq0L9ugwlZv/ZxyZT/7NfSo7+95L+Ndefa3PEQubwwQcCCoHp8pvebDP3cfTGNJBx0+3fP/Pdc9A9n9qMZ3niyAnbdBW7Rf6jwJ2LhaCBzKgjhMUSH4zkqkCyVUbLsgSJBlmv4XM7AKAIKBjRJZps1c+JLHtLGmbZrj30wUwlBQAmWOKDMX1uKLPZIVcnn44OyvbQrT6oqUcxlONehIb5QM9Mm7WzD3XwgARm+gxX5jcLX/iq9x5eWJ+lvhk5P3z5586eVNyXaVFYBBSMNS/LElAXGxcyJ0XoQ7daLWUuAeqN8cPXZz6v3XTlMptNnxnLNpooOSKBBW38KMQZXKePpf7bqL78c/1GRv970H1LOhphiiU+GMv13W+uSQk64ihXU4Qu9+nsPR2JpO/X6iLqZ55soX+vBPtv0xLvFXcoMOKFCgrGnGWazBvMoQ9nwiuzGtLOO32O6a+vZ//1tJrSJa+BntnvMZndFyeMOppNq2qehYIJKij4km0StQZNriGvesTCf/u2eXu5Pu/04euCGEiOnRMgnwgo+JKtUlI6wTxXh56G1C33mu83aUiV0nKftt0Hef5reCgpB/eG2KTKo+HBZCqgpiSIRTysC0MEFHzTZbtstEMvV7Ckjy7yCir9fCk1T2g46VEiQfz2+U/k5JBd01U1nF58cq8EoR2EnFwMUxWp10q3Cy5cuPCpABl0kGu2zW69+Wj7s1fATB+a1zfT3ZUeanoz1/eX0h6UQyvDwd7ZLd6f/OegvPX6J57vXXNzqyztrJdi07Ds2XPcs3q6/dvXyNX/sOjSz/Xf6+LVhBNmq6io6HD9vBBQCEBvtu++lv15GtOQckxNzD4IsZyOwBhIjMkvN30gpebhzWtl1XVNArjxCiiW+BCI06GXjYaXtpCbHkw3PWC2ceZVbuczNS+ukdr60mumXbqq+NUe4o+AQmC62Z3roUutiPSYc33OqVSfb4qChtOam1uklNywbnFJhi4Kj4BCKF4n4OozTm//iqBys+6eFSVzQ9e/h/59gCiwB4VI6ISAbEfAZ9JxSTqHT09VLcVGiKC08WDr5vhP0r3/iTWyprtVABM0SaBgdM9JH9j1M4hUD/FzmikaFs0MmS23fSiH075tW1u5iVVrm+Rb3+u0otMQ8UFAoaC0QUKX84LOW9PqSkcdlbOD+0YksXdEjn4+Mf0AdC7psxGdE4ezyTzddqg39zcRWtWmV7ZujS7676qhpUq6UhUTHXsIgoBCUeiNTZf9guw7feNRgcx87XT/Lpf0r5N+vbUpJRsNp/SjOf68PfcxKZl7iruel5z0wetymUaP/PAKKFptkBfO8eZhggpAeSOgkFdOUOmSny4t6Xfu2Y6Jd3BWEAAHAYWC0GUmZx9E9z80pMYzhsK2tHMMQyG4NaFU11x57VT8ejZQIggoFJze9KYDS+CmrjF3QGQGudteUOYRKdoEcTSR/drM1n/9ebaGl1Ib6As78aAuYDFtu8+koXVdxpgpDaxsZ3VpiLRkhNnK67OHy4rrrww+ncKereLSa4F8o4sPsJw2muhZW7ocqhWNhlauh5zTr9VzqFZ8KXvAaFu6Plg9emzm8xpinTdn/zUzr9Xfn6VYRIE2cwCAlZhmDgCIJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUCCgBgJQIKAGAlAgoAYCUNqPNuF0xOTvYJAAAROn/+/CmvSzwDCgCAqJ07d84ooNyv8E45AAB8Ma2gzrldYZByAAD4YlpBuS7xJZNJ9qAAAJGampoKH1CTk5NUUACASI2Pj3sVPxc0oKbcrhgdHaWCAgBEyiBbpjwDanh4mIACAETq2LFj4QMqkUgQUACASH322WfhA2rHjh0EFAAgUhs3buzxuGSq4uIPlovL2KOzZ8++XV1d3S4AAIQ0MTHRU1tbe6fHZZ85oeRaRY2Pj3slHQAARlIB5bUyd1b/xwmoSbcrR0ZGCCgAQCQMMmW6aHIC6qzblUeOHCGgAACR2L9//x6PSyb0f4wC6vnnn/f6xQAAMPLKK694FT3TmeQ0SWhQLXe9mkYJAEBIhg0Sn0vaNHMdd+TaKHHixImdAgBACGNjYybV0/QIvvTW8nG3dxw/fpx9KABAKAcOHPAqdi5tOVVm+2A2mzdvpoICAITy1ltveRU7l4qlirQPeu5DpUqzbXPnzu0WAAB8OnPmzJ76+vqNHpcdlow2c6Vrfq5VVGqZj24+AEAg/f39Jst7l/ohMscbTbi9M7V2SEABAAJ5//33vTIkmf6TioxP1qZei8UF7eYAAL8mJyf7rrrqqts8LhuUtJDKrKD0E64n7B45cmS7AADgg8GjSrq0N6uCyjbBfExcbNu2jYACAPjyyiuvvOxxSTLzAxVZLvJc5qObDwBgyrB7b9bynspWQXku8xl0YgAAMC2RSHitvF2xvKeqclw8J/WqyfE5XUvs/c53vnNfRUVFzmsAANDmiLa2tsc8LjsjWaYZVbpcnNPWrVtPHT582Gs9EQBQ5kZHR00eTxrN9sFcAaWlluszUTRLAAC8vPTSS1s8LjkrOYZEVLi8qTH1anb5PM0SAICcUttB21tbW72W946nXqezfaLS5U36BtdmiXfffdcrGQEAZcqgetLmiNO5PulWQan5qVeT2wVUUQCATIbVk4bT8VyfdKug1CmPz1NFAQCuYFA9qRG3T3pVUKo19ZrndgFVFADAEUX1pLwqKDXidcELL7zg9QcBAJSJKKonZVJBqSXi8uCuOnLkyFOLFy9+SAAAZSuq6kmZVFDqmNcFP/jBD7acP3/ec88KAFCadGpEVNWTMq2glOdeVG9v70OdnZ1PCQCg7Ozbt++xtWvXeg1xMKqelJ+A0vl8S8Wj6qJhAgDKj+GBhOqwpB3r7sZ0iU8u/oKjXhft2LHjRwIAKCtbtmzxOk5DnRTDcFJ+KiilgaZV1By3iw4dOrRp+fLlmwQAUPI+//zzLStWrDCZGjEgHhOK0vkNKDU39VrodVEymfy3mpqaLgEAlCwfS3s5Z+7l4meJz6FHcUx4XfSLX/ziX+jqA4DSpfd4w6W90+IznFSQCkoZNUzQ1QcApevvf//7j66++mqvswF1SU+X9oz3nhxBKii5+Bt59rGvXr365cHBQQ42BIASo/d2g3BSupLmO5xU0IByflPPpT59gHdiYqJHAAAlQfed9N5ucKkefmv0UG42VRKO/ub68G7OpcK9e/dONDY2/umWW27ZUFFRUSMAgNjScNJ9p5/97GdHPS7Vpb0h8dG1lynoHlQ6z5N31Ztvvtm9bt26bQIAiK3f/OY3d27cuNFkVcx3116mKAJKaUA1el300Ucfbejq6npOAACxY9gUoXQLaFhCCrMHlU7XGD03wa699trt+kCXAABiRe/dhuFk1ERnIqqA0jXGQTFYa9SnjQkpAIgPw0kRyjgLTEQVUEpT02hCrf5FaT8HAPv5CCelGRCopTybqPag0s1PvZpMLhweHn5u/vz5GwQAYB3DwwcdOgg2kqU9R5QVlEP/kEYjjpqbmx9juQ8A7KOrXD7CSe/5kYaTykdAKe3e8HyIV7EnBQB20XvykiVLTI9O0vmsoTv2sslXQCl9QOusyYWEFADYweeek97jjXoPgshnQGkXhz5pbLRhpl+QRCLBYYcAUCR6ZLuPcNJ7u97jI+nYyyafAaX0LzAohiGlw2X1KWUdpSEAgILQYzN27ty5ce3atdsN3+Lr3h5U2Fl8JjRdx2XmoEPPQNy+ffvRhoaGnV/+8pfXV1VVeU6nAAAEp8O8dbbed7/7XdOh3gUJJ1WIgFK+QmrXrl2nUst922+99daaefPmXS8AgMhpp94jjzzymMHgV0fBwknl4zkoN3rQoR4Xf5XpG/TQw46Ojk2VlZVUUwAQAV3SSxUBpqOLHNoQYdxXEIV870FlctJ33PQNui/105/+lH0pAIiALunpPdVnOOnRSgWrnByFrqDSGU1AT3fo0KFNy5cv3yQAAN98tpA7IplMHkShK6h0+hf29eSxfmF/8pOf3EY1BQDmzpw5s0c7pAOEk04GKko4qWJWUA5tnGgVn2GpZ0ulStRN1dXV7QIAuELAvabpt8rMA7hnpIiKWUE59AswID7XNvVsKW2N1GGGAgCYRTv0HnjggdsChJPei/WeXNRwUjZUUA4NS52C7rtb79lnn23/3ve+t6mlpYXJ6ADKmi7nvfDCC489/vjjQbZCnKGveZsO4YdNAeWYJzMNFL6rO4IKQLnSYHr77be3fPOb39wj/mkg6X7TqFjExoBS+rzUgtSrRgIgqACUi5DBpLSFPNKDBqNia0A59PDDBgm4V+YEVUNDQzfNFABKhTY/HD58+OVt27ZtD7iUN/3LiIVVUzrbA0ppNaV7U/MkBO366+jo2DB37txuAYAY0mqpv79/59NPP71969atRgfD5mBt1ZQuDgHl0IDSoJojIWhV9cADDzyUWv5bT1UFwHb63OeRI0e2v/rqq9ufeOKJsM+AaiDpc01F79AzEaeAUrrUp11+TRKBVHncdeONN3a3tbWtp7ICYAutlI4fP75n//79e0LsLaXT5TytuEbFkg49E3ELKEcky37ptLK6/fbbu6655pr19fX1XTU1NV0CAAWgVdKJEyd2Hj16tOeZZ57ZGXL5LtNpmWkdt3o5L5u4BpQj8qBy3HfffY2ppcCuNWvWdDc1NWlgtRNaAMLSMBofH+8ZGRnpOXz4cM8vf/nLPREHkiMW+0xu4h5QjrwFVSZdFly5cmX7ggUL2hsaGtrr6ura58yZ06iHK+qRIM4/BUBZcWaEaofduXPnTiWTyb7Ux06Njo72HTt2rO+zzz7r27FjR1+ewihdbCumTKUSUA4nqGolZDMFAMRILPeYvJRaQKXTakqfoTI+HBEAYkaX8fR8Pa2aSiaYHKUcUA4NKA0qqioApUCDSANJgykpJawcAiqdhlS9EFYA4qVsQilduQVUOg2puov/ZBkQgG3OyuUlvLIJpXTlHFDptJrSkKq7+E8CC0ChadedTniYvPjPkttT8ouAyk4nVjhBpRVWlRBaAKKhwaNhpEF0Nu1V9oGUiYDyZ06WV+XFV0Xaj204qRhAYZ2XyyFzLu3nU1leMPBf2AatVWx9ge0AAAAASUVORK5CYII=", + "image": "tb-image:Z2F0ZXdheS1kYXNoYm9hcmRfKDEpLnBuZw==:Z2F0ZXdheS1kYXNoYm9hcmQucG5n;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALwAAAC8CAYAAADCScSrAAAKeUlEQVR4Ae3dS2xc1R3H8eP3jBMnxChpSVDaOAuUl2grpERFpRVRKxYVXXRDUTeVuqzUTVd0zxqk7hq13VStVOiCCqkCKaIPUKiCghpIhFAcEYhTErBpTGyPxw/md6//9rXxY2Z8PXPP/L8faTQPewa49zf/87/nHl+6ZmdnFwPgRHcAHCHwcIXAwxUCD1cIPFwh8HCFwMMVAg9XCDxcIfBwhcDDFQIPVwg8XCHwcIXAwxUCD1cIPFwh8HCFwMMVAg9XCDxcIfBwhcDDFQIPVwg8XCHwcIXAwxUCD1cIPFwh8HCFwMMVAg9XCDxcIfBwhcDDFQIPVwg8XCHwcIXAwxUCD1d6Q4dbXFwM8/PzYW5ubvmxve5ZV1fX8q27uzv09vaGnp6e5Hkn6+rU/xO3Al77bwsLCwvuw90IBb+vry+570QdF3gFfWZmZlXItQNVvezeKptnNtrpXtusWq0m90ZVv7+/P9lmnaRjAq9KrqBby6JAl0ql5OY93PXSNlTwp6enk8ei4A8ODnbMNuyIwKt10U3VSjumXC4nQUfzKpXKcvC1TVXtdYtd9IHXjlHYRSFX2Kno+VDYFXptY1HgBwYGQsyiDnw27Bp2qeo7Q63i1NRU8jj20EcbeAu7qvnQ0FDHzioUhQ5oJycnk7Yx5tBHeeLJenYh7K2hbbx79+7kcXb7xya6wKuvzLYxhL11NEWpbS42SRCb6AKvgyhtaA2p9OytZ1O92gfaF7GJKvCaI1aF19ywZmPQHjYTZks2YhJV4G16TBtcoUd72LkOsX0Si2hSY4u/FPTY54I7gZ3B1ohrZ7djEE3g1c4IrUxxWOGJqcpHEXhb4CSdtpgpZlZ8YlqRGkXgbchU2Ondi0MtjaaFs39nUHRRpMeqO3PuxWP7JJbZmigCb0tVtZYdxWKBp6XJkW1MAl88FngrSkUXVeDp34vHlmJT4XNkG5N17sVD4IECI/BwhcDDFQIPVwg8XCHwcIXAwxUCD1cIPFwh8HCFwLfJzL25MHr5/wGtxQLzNlDQ//js1ST0+w6Uws+fPRXuO8Df6bYCFb4Nzv/pRhJ2mbg9E15/6WZAaxB4uELg2+DbTx5cfqyW5tEnDwW0RhRXD9ZVa2V4eDh0iomPZ8JndyrhgSO7QmlX3IdS4+Pjyb0ubFt0VPg2uTtWCh/9Z2+oTjFv0EoEvk3mKrWpycn0Hq1DedkBE7e3TvHwSHpLf3/z3y3v6om+7SkKtmLOzj1zOVx/J/8TSo8/dTicffpwwPbQ0uRIJ5QUdlXjY2fuT17TYx2Ybtf5P98I2D4CvwOOnNobfvrMsXDkZO3+18eSM6koBlqaHXC9VulfPjeaVPsXn3s/fP3knoBiIPA7QMsG3nhpLHmspQMT52cCioGWJkdlZlIKj8Dn6IGRXauWDeTp7E+YockDSwuwbSwtwLrGPwrh1edDePfV1a//63fp69h5BB6uEHi4wrTCDpi8k7YvaxeGTd/d/H3XLqw87h0I4cDR2swPU/i5IvA5U6gvvrj5Ksh9a/7e42vfDOG9f4Yw+ubq1/X8Oz9Lw498EPicTSxV9oPHvxxsUcXe9+Dq1w7XAj+0f/UIcHs0hDvXavfX0s9CPgj8DlHYGwmqvgT7Ms8VfgUe+SLwOVPfPfxgc23IxRfS96viq805dHz152R/juYwS5MzBbS0p7nAT9xMW5mNPif7czSHM605+M0vL4Vb1+9t+HNdmeBX5x7Z8OdXL3waXnj+/eVr1WylaBdviulMKy1NDu6rBbC0u/FNqYPbu7UpzL//Ib0wk9bibPWnfLragVZgvvaXm+GxH40k7RPqR+Bz8IOnjoVqJZ2ByQZQc/Gbzb1r3v3G2yF8Pp5W9l88t3VzfqU2GugyfR/+dz68VXvbwz9M+3rUh8Dn4L1/pFcg0ExLNvBjV0K4dTV9fGidGRsdmKpHv3ix9mRpyby+IHqfKMh6rhNZcvRMWB4BNI05cjqEPfsDGkDgc6RgvvvKyvOJLS4ZqVFhZs0IoIDbCSiNGBol7EujwBu1Q7pxUqoxBD5HCuDY1fp/X5Vcv9/MtWn0xVA7pFGFlqZ+BD4H5b1pS7ORoQ3ajoe+m4b17cu19y+1NGpRHvnxyvt0W68dUut05umNPxvrYx6+AarE1k+rOtvjhx7b/H1HT6//us6kvvPK6rZGszZ6TTe1M/rn2PMs/Z7W1du/A+pD4BugAGphmCTtyNLBpaqsFnmpvdDJIrvpuar1/g1ajmQGp+vLr2u0sMvwVSsrz9eqcpm+htHSNKBvYOVgUWFWD63T/OWlgFsrUi+9V1+GC6+vtDR9S2dYRQek2edZ9mVi+XBjCHwDVMkVwg8upVOKmj1RxT8wki4Ua7Sf1nSmvjTZlsZGC6MeX73+WmqH/v175uEbReAboLAf/kYaUoVMFVbhn/wkDW2jgdeXZG629rlL8/Av/3Y0fHVk88vy/W80XcKgyq73Mw/fGALfIM2Ff34nrew6GLWWphn6gpz4fi3En3wl+f8+vfG3sbrf+60n9oYTZwMaxOKxJunkkKq7zaFvd4rwypufhrf+Opd8xu7a7VbtgPjgifTAduLDWlvzvVoL9Fr6z3j4iVJy3cqiYPGYAzqtr/ZG04LTd7c/H3789P3h40vp5+hzFz8L4dSj6V88DczXKvrjIdz7IP35kZMBTaLCF4iOA3ScoJu+SPYl0uuaqcn+vEio8GhKdvoxO2LY6yWmILeNE09whcDDFQIPVwg8XCHwcIXAwxUCD1cIPFwh8HCFwMOVKALf1ZX+HdziYuGX/bhj+8T2UdFFVeEJfPHMz88n9wQ+Rz09Pcn93Fx9FxtF6ywsLCT3BD5HtjEJfPFYhe/ujqNZiOLfsrc3XcVsGxfFUa1Wk3vbR0UXTUujKq+NSx9fHCpANupa21l00bQ0NmTO2AVc0HYWdlV3evicDQykf9dG4Itjeno6ue/r6wuxiCbw1taopSH07VepVJIZGo28sfTvEtU8fKlUSu5VWejl20e9u1X3/v7+EJOoAq9KokqvsE9NTQW0h8Ju1T2mdkaiW0ujKq/WRkMqrU3raZvPzs4m+2BwcDDEJrrAq6rYMKoqz8mo1tG2tpFVkwixzMxkRblaUoG3WRtdpInQ7zydA7ELYmnbx9bKmCiuPLYRDa1qbUTDqx3UIl9qY7KVPbYD1ayoAy/Z0GtHlMvlaM76FZ1NDtj2jT3sEn3gRaHXTTtIPb5Cby0PmqOqbtO/6tVjbmOyOiLwomkymy4TC75NZWJrCre2oSq6nefQttN2jPEAdT0dE3ijg6vsDhNVJgu+vgh21tYzbR/ddMCfvRltJ1X1mM6i1qPjAm+08xR+ZnDqZ4v0FPROHRU7NvBGVcyWsardscrmfWmCjXA22tkI2OkjX8cHHsjiMh1whcDDFQIPVwg8XCHwcIXAwxUCD1cIPFwh8HCFwMMVAg9XCDxcIfBwhcDDFQIPVwg8XCHwcIXAwxUCD1cIPFwh8HCFwMMVAg9XCDxcIfBwhcDDFQIPVwg8XCHwcIXAwxUCD1cIPFwh8HCFwMMVAg9XCDxcIfBwhcDDFQIPVwg8XPkCcAEXfkdt4uwAAAAASUVORK5CYII=", "mobileHide": false, "mobileOrder": 6, "configuration": { diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index f411344e3f..b125daa591 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -636,6 +636,10 @@ public class ActorSystemContext { return partitionService.resolve(serviceType, queueName, tenantId, entityId); } + public TopicPartitionInfo resolve(TenantId tenantId, EntityId entityId, TbMsg msg) { + return partitionService.resolve(ServiceType.TB_RULE_ENGINE, msg.getQueueName(), tenantId, entityId, msg.getPartition()); + } + public String getServiceId() { return serviceInfoProvider.getServiceId(); } diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index 253169a51d..7432038096 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -90,6 +90,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCre import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; import org.thingsboard.server.gen.transport.TransportProtos.UplinkNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseReason; import org.thingsboard.server.service.rpc.RpcSubmitStrategy; import org.thingsboard.server.service.state.DefaultDeviceStateService; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; @@ -845,7 +846,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso notifyTransportAboutDeviceCredentialsUpdate(k, v, ((DeviceCredentialsUpdateNotificationMsg) msg).getDeviceCredentials()); }); } else { - sessions.forEach((sessionId, sessionMd) -> notifyTransportAboutClosedSession(sessionId, sessionMd, "device credentials updated!")); + sessions.forEach((sessionId, sessionMd) -> notifyTransportAboutClosedSession(sessionId, sessionMd, "device credentials updated!", SessionCloseReason.CREDENTIALS_UPDATED)); attributeSubscriptions.clear(); rpcSubscriptions.clear(); dumpSessions(); @@ -855,13 +856,15 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso private void notifyTransportAboutClosedSessionMaxSessionsLimit(UUID sessionId, SessionInfoMetaData sessionMd) { log.debug("remove eldest session (max concurrent sessions limit reached per device) sessionId: [{}] sessionMd: [{}]", sessionId, sessionMd); - notifyTransportAboutClosedSession(sessionId, sessionMd, "max concurrent sessions limit reached per device!"); + notifyTransportAboutClosedSession(sessionId, sessionMd, "max concurrent sessions limit reached per device!", SessionCloseReason.MAX_CONCURRENT_SESSIONS_LIMIT_REACHED); } - private void notifyTransportAboutClosedSession(UUID sessionId, SessionInfoMetaData sessionMd, String message) { + private void notifyTransportAboutClosedSession(UUID sessionId, SessionInfoMetaData sessionMd, String message, SessionCloseReason reason) { SessionCloseNotificationProto sessionCloseNotificationProto = SessionCloseNotificationProto .newBuilder() - .setMessage(message).build(); + .setMessage(message) + .setReason(reason) + .build(); ToTransportMsg msg = ToTransportMsg.newBuilder() .setSessionIdMSB(sessionId.getMostSignificantBits()) .setSessionIdLSB(sessionId.getLeastSignificantBits()) @@ -1044,7 +1047,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso attributeSubscriptions.remove(id); if (session != null) { removed++; - notifyTransportAboutClosedSession(id, session, SESSION_TIMEOUT_MESSAGE); + notifyTransportAboutClosedSession(id, session, SESSION_TIMEOUT_MESSAGE, SessionCloseReason.SESSION_TIMEOUT); } } if (removed != 0) { diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java index a16e6122f5..eaa2218116 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java @@ -42,7 +42,6 @@ import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.RuleNodeException; -import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.rule.RuleChainService; @@ -295,7 +294,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor ruleNodeRelations = nodeRoutes.get(originatorNodeId); if (ruleNodeRelations == null) { // When unchecked, this will cause NullPointerException when rule node doesn't exist anymore diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 571f32d171..f95dfcb648 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.oas.annotations.Parameter; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @@ -34,8 +33,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; +import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @@ -49,7 +48,6 @@ import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.config.annotations.ApiOperation; -import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.model.ActivateUserRequest; @@ -105,9 +103,8 @@ public class AuthController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) - public ObjectNode changePassword( - @Parameter(description = "Change Password Request") - @RequestBody ChangePasswordRequest changePasswordRequest) throws ThingsboardException { + public JwtPair changePassword(@Parameter(description = "Change Password Request") + @RequestBody ChangePasswordRequest changePasswordRequest) throws ThingsboardException { String currentPassword = changePasswordRequest.getCurrentPassword(); String newPassword = changePasswordRequest.getNewPassword(); SecurityUser securityUser = getCurrentUser(); @@ -123,10 +120,7 @@ public class AuthController extends BaseController { userService.replaceUserCredentials(securityUser.getTenantId(), userCredentials); eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); - ObjectNode response = JacksonUtil.newObjectNode(); - response.put("token", tokenFactory.createAccessJwtToken(securityUser).getToken()); - response.put("refreshToken", tokenFactory.createRefreshToken(securityUser).getToken()); - return response; + return tokenFactory.createTokenPair(securityUser); } @ApiOperation(value = "Get the current User password policy (getUserPasswordPolicy)", diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java index 626f6f3b0f..77e34fc18c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java @@ -97,7 +97,7 @@ public class MobileApplicationController extends BaseController { public ResponseEntity getAssetLinks() { MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); AndroidConfig androidConfig = mobileAppSettings.getAndroidConfig(); - if (androidConfig != null && androidConfig.isEnabled() && !androidConfig.getAppPackage().isBlank() && !androidConfig.getSha256CertFingerprints().isBlank()) { + if (androidConfig != null && androidConfig.isEnabled() && androidConfig.getAppPackage() != null && androidConfig.getSha256CertFingerprints() != null) { return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, androidConfig.getAppPackage(), androidConfig.getSha256CertFingerprints()))); } else { return ResponseEntity.notFound().build(); @@ -109,7 +109,7 @@ public class MobileApplicationController extends BaseController { public ResponseEntity getAppleAppSiteAssociation() { MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); IosConfig iosConfig = mobileAppSettings.getIosConfig(); - if (iosConfig != null && iosConfig.isEnabled() && !iosConfig.getAppId().isBlank()) { + if (iosConfig != null && iosConfig.isEnabled() && iosConfig.getAppId() != null) { return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, iosConfig.getAppId()))); } else { return ResponseEntity.notFound().build(); diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index 69728b20c3..f386cc8e42 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -18,6 +18,7 @@ package org.thingsboard.server.controller.plugin; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.RemovalCause; +import jakarta.annotation.PreDestroy; import jakarta.websocket.RemoteEndpoint; import jakarta.websocket.SendHandler; import jakarta.websocket.SendResult; @@ -134,6 +135,11 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke .build(); } + @PreDestroy + private void stop() { + internalSessionMap.clear(); + } + @Override public void handleTextMessage(WebSocketSession session, TextMessage message) { try { diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 33e2ddee01..4bef0d420a 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -136,7 +136,7 @@ public class ThingsboardInstallService { dataUpdateService.updateData("3.6.4"); entityDatabaseSchemaService.createCustomerTitleUniqueConstraintIfNotExists(); systemDataLoaderService.updateDefaultNotificationConfigs(false); - systemDataLoaderService.updateJwtSettings(); + systemDataLoaderService.updateSecuritySettings(); //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache break; default: diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index bdd0494443..7588726a73 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -26,6 +26,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -68,6 +69,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.oauth2.OAuth2Mobile; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; @@ -98,6 +100,7 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.oauth2.OAuth2MobileDao; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.settings.AdminSettingsService; @@ -120,7 +123,7 @@ import java.util.concurrent.atomic.AtomicInteger; import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE; import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.isSigningKeyDefault; -import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.validateTokenSigningKeyLength; +import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.validateKeyLength; @Service @Profile("install") @@ -146,6 +149,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { private final DeviceConnectivityConfiguration connectivityConfiguration; private final QueueService queueService; private final JwtSettingsService jwtSettingsService; + private final OAuth2MobileDao oAuth2MobileDao; private final NotificationSettingsService notificationSettingsService; private final NotificationTargetService notificationTargetService; @@ -269,21 +273,20 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Override public void createRandomJwtSettings() throws Exception { - if (jwtSettingsService.getJwtSettings() == null) { - log.info("Creating JWT admin settings..."); - var jwtSettings = new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey); - if (isSigningKeyDefault(jwtSettings) || !validateTokenSigningKeyLength(jwtSettings)) { - jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( - RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); - } - jwtSettingsService.saveJwtSettings(jwtSettings); - } else { - log.info("Skip creating JWT admin settings because they already exist."); + if (jwtSettingsService.getJwtSettings() == null) { + log.info("Creating JWT admin settings..."); + var jwtSettings = new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey); + if (isSigningKeyDefault(jwtSettings) || !validateKeyLength(jwtSettings.getTokenSigningKey())) { + jwtSettings.setTokenSigningKey(generateRandomKey()); } + jwtSettingsService.saveJwtSettings(jwtSettings); + } else { + log.info("Skip creating JWT admin settings because they already exist."); + } } @Override - public void updateJwtSettings() { + public void updateSecuritySettings() { JwtSettings jwtSettings = jwtSettingsService.getJwtSettings(); boolean invalidSignKey = false; String warningMessage = null; @@ -291,7 +294,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { if (isSigningKeyDefault(jwtSettings)) { warningMessage = "The platform is using the default JWT Signing Key, which is a security risk."; invalidSignKey = true; - } else if (!validateTokenSigningKeyLength(jwtSettings)) { + } else if (!validateKeyLength(jwtSettings.getTokenSigningKey())) { warningMessage = "The JWT Signing Key is shorter than 512 bits, which is a security risk."; invalidSignKey = true; } @@ -301,10 +304,28 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { "You can change the JWT Signing Key using the Web UI: " + "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator.", warningMessage); - jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( - RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); + jwtSettings.setTokenSigningKey(generateRandomKey()); jwtSettingsService.saveJwtSettings(jwtSettings); } + + List mobiles = oAuth2MobileDao.find(TenantId.SYS_TENANT_ID); + if (CollectionUtils.isNotEmpty(mobiles)) { + mobiles.stream() + .filter(config -> !validateKeyLength(config.getAppSecret())) + .forEach(config -> { + log.warn("WARNING: The App secret is shorter than 512 bits, which is a security risk. " + + "A new Application Secret has been added automatically for Mobile Application [{}]. " + + "You can change the Application Secret using the Web UI: " + + "Navigate to \"Security settings -> OAuth2 -> Mobile applications\" while logged in as a System Administrator.", config.getPkgName()); + config.setAppSecret(generateRandomKey()); + oAuth2MobileDao.save(TenantId.SYS_TENANT_ID, config); + }); + } + } + + private String generateRandomKey() { + return Base64.getEncoder().encodeToString( + RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8)); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java index 71c829ee11..2f4b5fa885 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java @@ -25,7 +25,7 @@ public interface SystemDataLoaderService { void createRandomJwtSettings() throws Exception; - void updateJwtSettings() throws Exception; + void updateSecuritySettings() throws Exception; void createOAuth2Templates() throws Exception; diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index b355ac32e9..d8ba443d85 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -203,14 +203,14 @@ public class DefaultDataUpdateService implements DataUpdateService { @Override public void upgradeRuleNodes() { - try { - int totalRuleNodesUpgraded = 0; - log.info("Starting rule nodes upgrade ..."); - var nodeClassToVersionMap = componentDiscoveryService.getVersionedNodes(); - log.debug("Found {} versioned nodes to check for upgrade!", nodeClassToVersionMap.size()); - for (var ruleNodeClassInfo : nodeClassToVersionMap) { - var ruleNodeTypeForLogs = ruleNodeClassInfo.getSimpleName(); - var toVersion = ruleNodeClassInfo.getCurrentVersion(); + int totalRuleNodesUpgraded = 0; + log.info("Starting rule nodes upgrade ..."); + var nodeClassToVersionMap = componentDiscoveryService.getVersionedNodes(); + log.debug("Found {} versioned nodes to check for upgrade!", nodeClassToVersionMap.size()); + for (var ruleNodeClassInfo : nodeClassToVersionMap) { + var ruleNodeTypeForLogs = ruleNodeClassInfo.getSimpleName(); + var toVersion = ruleNodeClassInfo.getCurrentVersion(); + try { log.debug("Going to check for nodes with type: {} to upgrade to version: {}.", ruleNodeTypeForLogs, toVersion); var ruleNodesIdsToUpgrade = getRuleNodesIdsWithTypeAndVersionLessThan(ruleNodeClassInfo.getClassName(), toVersion); if (ruleNodesIdsToUpgrade.isEmpty()) { @@ -222,11 +222,11 @@ public class DefaultDataUpdateService implements DataUpdateService { totalRuleNodesUpgraded += processRuleNodePack(ruleNodePack, ruleNodeClassInfo); log.info("{} upgraded rule nodes so far ...", totalRuleNodesUpgraded); } + } catch (Exception e) { + log.error("Unexpected error during {} rule nodes upgrade: ", ruleNodeTypeForLogs, e); } - log.info("Finished rule nodes upgrade. Upgraded rule nodes count: {}", totalRuleNodesUpgraded); - } catch (Exception e) { - log.error("Unexpected error during rule nodes upgrade: ", e); } + log.info("Finished rule nodes upgrade. Upgraded rule nodes count: {}", totalRuleNodesUpgraded); } private int processRuleNodePack(List ruleNodeIdsBatch, RuleNodeClassInfo ruleNodeClassInfo) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 7af2579b89..bce5dece3b 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasRuleEngineProfile; -import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; @@ -59,8 +58,8 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.msg.rule.engine.DeviceEdgeUpdateMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg; -import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto; import org.thingsboard.server.gen.transport.TransportProtos.QueueDeleteMsg; @@ -74,6 +73,7 @@ import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.MultipleTbQueueCallbackWrapper; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.TbRuleEngineProducerService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; @@ -116,6 +116,9 @@ public class DefaultTbClusterService implements TbClusterService { @Lazy private TbQueueProducerProvider producerProvider; + @Autowired + private TbRuleEngineProducerService ruleEngineProducerService; + @Autowired @Lazy private OtaPackageStateService otaPackageStateService; @@ -203,13 +206,8 @@ public class DefaultTbClusterService implements TbClusterService { HasRuleEngineProfile ruleEngineProfile = getRuleEngineProfileForEntityOrElseNull(tenantId, entityId); tbMsg = transformMsg(tbMsg, ruleEngineProfile); } - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, entityId); - log.trace("PUSHING msg: {} to:{}", tbMsg, tpi); - ToRuleEngineMsg msg = ToRuleEngineMsg.newBuilder() - .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) - .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) - .setTbMsg(TbMsg.toByteString(tbMsg)).build(); - producerProvider.getRuleEngineMsgProducer().send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), callback); + + ruleEngineProducerService.sendToRuleEngine(producerProvider.getRuleEngineMsgProducer(), tenantId, tbMsg, callback); toRuleEngineMsgs.incrementAndGet(); } @@ -584,6 +582,7 @@ public class DefaultTbClusterService implements TbClusterService { .setQueueName(queue.getName()) .setQueueTopic(queue.getTopic()) .setPartitions(queue.getPartitions()) + .setDuplicateMsgToAllPartitions(queue.isDuplicateMsgToAllPartitions()) .build()) .collect(Collectors.toList()); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index fbef0cfd29..ae8ea8f7f4 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -204,7 +204,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService queueFactory.createToCoreMsgConsumer()) + .consumerCreator((config, partitionId) -> queueFactory.createToCoreMsgConsumer()) .consumerExecutor(consumersExecutor) .scheduler(scheduler) .taskExecutor(mgmtExecutor) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java index 6792422b57..6eb5c94c9b 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java @@ -41,7 +41,7 @@ import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Function; +import java.util.function.BiFunction; import java.util.stream.Collectors; @Slf4j @@ -51,7 +51,7 @@ public class MainQueueConsumerManager msgPackProcessor; - protected final Function> consumerCreator; + protected final BiFunction> consumerCreator; protected final ExecutorService consumerExecutor; protected final ScheduledExecutorService scheduler; protected final ExecutorService taskExecutor; @@ -67,7 +67,7 @@ public class MainQueueConsumerManager msgPackProcessor, - Function> consumerCreator, + BiFunction> consumerCreator, ExecutorService consumerExecutor, ScheduledExecutorService scheduler, ExecutorService taskExecutor) { @@ -273,8 +273,9 @@ public class MainQueueConsumerManager consumers.remove(tpi).awaitCompletion()); addedPartitions.forEach((tpi) -> { - String key = queueKey + "-" + tpi.getPartition().orElse(-1); - TbQueueConsumerTask consumer = new TbQueueConsumerTask<>(key, consumerCreator.apply(config)); + Integer partitionId = tpi.getPartition().orElse(-1); + String key = queueKey + "-" + partitionId; + TbQueueConsumerTask consumer = new TbQueueConsumerTask<>(key, () -> consumerCreator.apply(config, partitionId)); consumers.put(tpi, consumer); consumer.subscribe(Set.of(tpi)); launchConsumer(consumer); @@ -303,7 +304,7 @@ public class MainQueueConsumerManager(queueKey, consumerCreator.apply(config)); + consumer = new TbQueueConsumerTask<>(queueKey, () -> consumerCreator.apply(config, null)); // no partitionId passed } consumer.subscribe(partitions); if (!consumer.isRunning()) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java index 84539d4d7c..5e672eb5c6 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java @@ -16,37 +16,57 @@ package org.thingsboard.server.service.queue.ruleengine; import lombok.Getter; -import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueMsg; +import java.util.Objects; import java.util.Set; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; -@RequiredArgsConstructor @Slf4j public class TbQueueConsumerTask { @Getter private final Object key; - @Getter - private final TbQueueConsumer consumer; + private volatile TbQueueConsumer consumer; + private volatile Supplier> consumerSupplier; @Setter private Future task; + public TbQueueConsumerTask(Object key, Supplier> consumerSupplier) { + this.key = key; + this.consumer = null; + this.consumerSupplier = consumerSupplier; + } + + public TbQueueConsumer getConsumer() { + if (consumer == null) { + synchronized (this) { + if (consumer == null) { + Objects.requireNonNull(consumerSupplier, "consumerSupplier for key [" + key + "] is null"); + consumer = consumerSupplier.get(); + Objects.requireNonNull(consumer, "consumer for key [" + key + "] is null"); + consumerSupplier = null; + } + } + } + return consumer; + } + public void subscribe(Set partitions) { log.trace("[{}] Subscribing to partitions: {}", key, partitions); - consumer.subscribe(partitions); + getConsumer().subscribe(partitions); } public void initiateStop() { log.debug("[{}] Initiating stop", key); - consumer.stop(); + getConsumer().stop(); } public void awaitCompletion() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index c5c04ac312..aba7c5b930 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -17,8 +17,6 @@ package org.thingsboard.server.service.security.auth.jwt.settings; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; @@ -111,8 +109,8 @@ public class DefaultJwtSettingsService implements JwtSettingsService { return TOKEN_SIGNING_KEY_DEFAULT.equals(settings.getTokenSigningKey()); } - public static boolean validateTokenSigningKeyLength(JwtSettings settings) { - return Base64.getDecoder().decode(settings.getTokenSigningKey()).length * Byte.SIZE >= KEY_LENGTH; + public static boolean validateKeyLength(String key) { + return Base64.getDecoder().decode(key).length * Byte.SIZE >= KEY_LENGTH; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java index 43cac31744..7b3224a138 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.service.security.model; +import lombok.Getter; +import lombok.Setter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.thingsboard.server.common.data.User; @@ -30,9 +32,12 @@ public class SecurityUser extends User { private static final long serialVersionUID = -797397440703066079L; private Collection authorities; + @Getter @Setter private boolean enabled; + @Getter @Setter private UserPrincipal userPrincipal; - private String sessionId; + @Getter @Setter + private String sessionId = UUID.randomUUID().toString(); public SecurityUser() { super(); @@ -46,7 +51,6 @@ public class SecurityUser extends User { super(user); this.enabled = enabled; this.userPrincipal = userPrincipal; - this.sessionId = UUID.randomUUID().toString(); } public Collection getAuthorities() { @@ -58,27 +62,4 @@ public class SecurityUser extends User { return authorities; } - public boolean isEnabled() { - return enabled; - } - - public UserPrincipal getUserPrincipal() { - return userPrincipal; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public void setUserPrincipal(UserPrincipal userPrincipal) { - this.userPrincipal = userPrincipal; - } - - public String getSessionId() { - return sessionId; - } - - public void setSessionId(String sessionId) { - this.sessionId = sessionId; - } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index a47d0034a7..f68d954aa1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -232,6 +232,7 @@ public class JwtTokenFactory { } public JwtPair createTokenPair(SecurityUser securityUser) { + securityUser.setSessionId(UUID.randomUUID().toString()); JwtToken accessToken = createAccessJwtToken(securityUser); JwtToken refreshToken = createRefreshToken(securityUser); return new JwtPair(accessToken.getToken(), refreshToken.getToken()); diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 6dcf84e721..f5bd42e29d 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -675,6 +675,7 @@ public class DefaultTransportApiService implements TransportApiService { .setQueueName(queue.getName()) .setQueueTopic(queue.getTopic()) .setPartitions(queue.getPartitions()) + .setDuplicateMsgToAllPartitions(queue.isDuplicateMsgToAllPartitions()) .build()).collect(Collectors.toList())).build()); } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java index 2a53e9a772..d7091bbad2 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -83,6 +83,7 @@ import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpda import jakarta.annotation.Nullable; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; + import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -143,6 +144,7 @@ public class DefaultWebSocketService implements WebSocketService { private final ConcurrentMap> customerSubscriptionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap> regularUserSubscriptionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap> publicUserSubscriptionsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap> sessionCmdMap = new ConcurrentHashMap<>(); private ExecutorService executor; private ScheduledExecutorService pingExecutor; @@ -201,9 +203,7 @@ public class DefaultWebSocketService implements WebSocketService { event.getError().orElse(new RuntimeException("No error specified"))); break; case CLOSED: - wsSessionsMap.remove(sessionId); - oldSubService.cancelAllSessionSubscriptions(sessionId); - entityDataSubService.cancelAllSessionSubscriptions(sessionId); + cleanupSessionById(sessionId); processSessionClose(sessionRef); break; } @@ -299,9 +299,7 @@ public class DefaultWebSocketService implements WebSocketService { public void cleanupIfStale(String sessionId) { if (!msgEndpoint.isOpen(sessionId)) { log.info("[{}] Cleaning up stale session ", sessionId); - wsSessionsMap.remove(sessionId); - oldSubService.cancelAllSessionSubscriptions(sessionId); - entityDataSubService.cancelAllSessionSubscriptions(sessionId); + cleanupSessionById(sessionId); } } @@ -451,7 +449,7 @@ public class DefaultWebSocketService implements WebSocketService { TbAttributeSubscription sub = TbAttributeSubscription.builder() .serviceId(serviceId) .sessionId(sessionId) - .subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet()) + .subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId())) .tenantId(sessionRef.getSecurityCtx().getTenantId()) .entityId(entityId) .queryTs(queryTs) @@ -500,6 +498,13 @@ public class DefaultWebSocketService implements WebSocketService { } } + private int registerNewSessionSubId(String sessionId, WebSocketSessionRef sessionRef, int cmdId) { + var cmdMap = sessionCmdMap.computeIfAbsent(sessionId, id -> new ConcurrentHashMap<>()); + var subId = sessionRef.getSessionSubIdSeq().incrementAndGet(); + cmdMap.put(cmdId, subId); + return subId; + } + private void handleWsHistoryCmd(WebSocketSessionRef sessionRef, GetHistoryCmd cmd) { if (!validateCmd(sessionRef, cmd, () -> { if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty() || cmd.getEntityType() == null || cmd.getEntityType().isEmpty()) { @@ -557,7 +562,7 @@ public class DefaultWebSocketService implements WebSocketService { TbAttributeSubscription sub = TbAttributeSubscription.builder() .serviceId(serviceId) .sessionId(sessionId) - .subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet()) + .subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId())) .tenantId(sessionRef.getSecurityCtx().getTenantId()) .entityId(entityId) .queryTs(queryTs) @@ -655,7 +660,7 @@ public class DefaultWebSocketService implements WebSocketService { TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder() .serviceId(serviceId) .sessionId(sessionId) - .subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet()) + .subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId())) .tenantId(sessionRef.getSecurityCtx().getTenantId()) .entityId(entityId) .updateProcessor((subscription, update) -> { @@ -710,7 +715,7 @@ public class DefaultWebSocketService implements WebSocketService { TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder() .serviceId(serviceId) .sessionId(sessionId) - .subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet()) + .subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId())) .tenantId(sessionRef.getSecurityCtx().getTenantId()) .entityId(entityId) .updateProcessor((subscription, update) -> { @@ -749,12 +754,25 @@ public class DefaultWebSocketService implements WebSocketService { private void unsubscribe(WebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { - oldSubService.cancelAllSessionSubscriptions(sessionId); + log.warn("[{}][{}] Cleanup session due to empty entity id.", sessionId, cmd.getCmdId()); + cleanupSessionById(sessionId); } else { - oldSubService.cancelSubscription(sessionId, cmd.getCmdId()); + Integer subId = sessionCmdMap.getOrDefault(sessionId, Collections.emptyMap()).remove(cmd.getCmdId()); + if (subId == null) { + log.trace("[{}][{}] Failed to lookup subscription id mapping", sessionId, cmd.getCmdId()); + subId = cmd.getCmdId(); + } + oldSubService.cancelSubscription(sessionId, subId); } } + private void cleanupSessionById(String sessionId) { + wsSessionsMap.remove(sessionId); + oldSubService.cancelAllSessionSubscriptions(sessionId); + sessionCmdMap.remove(sessionId); + entityDataSubService.cancelAllSessionSubscriptions(sessionId); + } + private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, EntityDataCmd cmd) { return validateCmd(sessionRef, cmd, () -> { if (cmd.getQuery() == null && !cmd.hasAnyCmd()) { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 25a2d617b7..454ca9427f 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -937,7 +937,7 @@ transport: client_side_rpc: # Processing timeout interval of the RPC command on the CLIENT SIDE. Time in milliseconds timeout: "${CLIENT_SIDE_RPC_TIMEOUT:60000}" - # Enable/disable http/mqtt/coap transport protocols (has higher priority than certain protocol's 'enabled' property) + # Enable/disable http/mqtt/coap/lwm2m transport protocols (has higher priority than certain protocol's 'enabled' property) api_enabled: "${TB_TRANSPORT_API_ENABLED:true}" log: # Enable/Disable log of transport messages to telemetry. For example, logging of LwM2M registration update @@ -972,6 +972,8 @@ transport: proxy_enabled: "${MQTT_PROXY_PROTOCOL_ENABLED:false}" # MQTT processing timeout in milliseconds timeout: "${MQTT_TIMEOUT:10000}" + # MQTT disconnect timeout in milliseconds. The time to wait for the client to disconnect after the server sends a disconnect message. + disconnect_timeout: "${MQTT_DISCONNECT_TIMEOUT:1000}" msg_queue_size_per_device_limit: "${MQTT_MSG_QUEUE_SIZE_PER_DEVICE_LIMIT:100}" # messages await in the queue before the device connected state. This limit works on the low level before TenantProfileLimits mechanism netty: # Netty leak detector level @@ -1024,10 +1026,6 @@ transport: coap: # Enable/disable CoAP transport protocol. enabled: "${COAP_ENABLED:true}" - # CoAP bind-address - bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" - # CoAP bind port - bind_port: "${COAP_BIND_PORT:5683}" # CoaP processing timeout in milliseconds timeout: "${COAP_TIMEOUT:10000}" # CoaP piggyback response timeout in milliseconds @@ -1036,56 +1034,6 @@ transport: psm_activity_timer: "${COAP_PSM_ACTIVITY_TIMER:10000}" # Default PSM Activity Timer if not specified in device profile paging_transmission_window: "${COAP_PAGING_TRANSMISSION_WINDOW:10000}" - dtls: - # Enable/disable DTLS 1.2 support - enabled: "${COAP_DTLS_ENABLED:false}" - # RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000 - retransmission_timeout: "${COAP_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}" - # CoAP DTLS bind-address - bind_address: "${COAP_DTLS_BIND_ADDRESS:0.0.0.0}" - # CoAP DTLS bind port - bind_port: "${COAP_DTLS_BIND_PORT:5684}" - # Server DTLS credentials - # CoAP DTLS connection ID length. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off - # Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used - connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:}" - credentials: - # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) - type: "${COAP_DTLS_CREDENTIALS_TYPE:PEM}" - # PEM server credentials - pem: - # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) - cert_file: "${COAP_DTLS_PEM_CERT:coapserver.pem}" - # Path to the server certificate private key file. Optional by default. Required if the private key is not present in the server certificate file; - key_file: "${COAP_DTLS_PEM_KEY:coapserver_key.pem}" - # Server certificate private key password (optional) - key_password: "${COAP_DTLS_PEM_KEY_PASSWORD:server_key_password}" - # Keystore server credentials - keystore: - # Type of the key store (JKS or PKCS12) - type: "${COAP_DTLS_KEY_STORE_TYPE:JKS}" - # Path to the key store that holds the SSL certificate - store_file: "${COAP_DTLS_KEY_STORE:coapserver.jks}" - # Password used to access the key store - store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}" - # Key alias - key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}" - # Password used to access the key - key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}" - x509: - # Skip certificate validity check for client certificates. - skip_validity_check_for_client_cert: "${TB_COAP_X509_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" - # Inactivity timeout of DTLS session. Used to clean cache - dtls_session_inactivity_timeout: "${TB_COAP_X509_DTLS_SESSION_INACTIVITY_TIMEOUT:86400000}" - # Interval of periodic eviction of the timed-out DTLS sessions - dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}" # Local LwM2M transport parameters lwm2m: # Enable/disable LwM2M transport protocol. @@ -1250,6 +1198,65 @@ transport: # Interval of transport statistics logging print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" +# CoAP server parameters +coap: + # Enable/disable coap server. + enabled: "${COAP_SERVER_ENABLED:true}" + # CoAP bind address + bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" + # CoAP bind port + bind_port: "${COAP_BIND_PORT:5683}" + dtls: + # Enable/disable DTLS 1.2 support + enabled: "${COAP_DTLS_ENABLED:false}" + # RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000 + retransmission_timeout: "${COAP_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}" + # CoAP DTLS bind address + bind_address: "${COAP_DTLS_BIND_ADDRESS:0.0.0.0}" + # CoAP DTLS bind port + bind_port: "${COAP_DTLS_BIND_PORT:5684}" + # CoAP DTLS connection ID length. RFC 9146, Connection Identifier for DTLS 1.2 + # Default: off + # Control usage of DTLS connection ID length (CID). + # - 'off' to deactivate it. + # - 'on' to activate Connection ID support (same as CID 0 or more 0). + # - A positive value defines generated CID size in bytes. + # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). + # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used + # - A value that are > 4: MultiNodeConnectionIdGenerator is used + connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:}" + # Server DTLS credentials + credentials: + # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) + type: "${COAP_DTLS_CREDENTIALS_TYPE:PEM}" + # PEM server credentials + pem: + # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) + cert_file: "${COAP_DTLS_PEM_CERT:coapserver.pem}" + # Path to the server certificate private key file. Optional by default. Required if the private key is not present in the server certificate file; + key_file: "${COAP_DTLS_PEM_KEY:coapserver_key.pem}" + # Server certificate private key password (optional) + key_password: "${COAP_DTLS_PEM_KEY_PASSWORD:server_key_password}" + # Keystore server credentials + keystore: + # Type of the key store (JKS or PKCS12) + type: "${COAP_DTLS_KEY_STORE_TYPE:JKS}" + # Path to the key store that holds the SSL certificate + store_file: "${COAP_DTLS_KEY_STORE:coapserver.jks}" + # Password used to access the key store + store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}" + # Key alias + key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}" + # Password used to access the key + key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}" + x509: + # Skip certificate validity check for client certificates. + skip_validity_check_for_client_cert: "${TB_COAP_X509_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" + # Inactivity timeout of DTLS session. Used to cleanup cache + dtls_session_inactivity_timeout: "${TB_COAP_X509_DTLS_SESSION_INACTIVITY_TIMEOUT:86400000}" + # Interval of periodic eviction of the timed-out DTLS sessions + dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}" + # Device connectivity parameters device: connectivity: @@ -1477,23 +1484,23 @@ queue: # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) topic-properties: # Kafka properties for Rule Engine - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Core topics - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Transport Api topics - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" # Kafka properties for Notifications topics - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for JS Executor topics - js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100;min.insync.replicas:1}" + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1}" # Kafka properties for OTA updates topic - ota-updates: "${TB_QUEUE_KAFKA_OTA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + ota-updates: "${TB_QUEUE_KAFKA_OTA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" # Kafka properties for Version Control topic - version-control: "${TB_QUEUE_KAFKA_VC_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + version-control: "${TB_QUEUE_KAFKA_VC_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Housekeeper tasks topic - housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" # Kafka properties for Housekeeper reprocessing topic; retention.ms is set to 90 days; partitions is set to 1 since only one reprocessing service is running at a time - housekeeper-reprocessing: "${TB_QUEUE_KAFKA_HOUSEKEEPER_REPROCESSING_TOPIC_PROPERTIES:retention.ms:7776000000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + housekeeper-reprocessing: "${TB_QUEUE_KAFKA_HOUSEKEEPER_REPROCESSING_TOPIC_PROPERTIES:retention.ms:7776000000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" consumer-stats: # Prints lag between consumer group offset and last messages offset in Kafka topics enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java index b0a6db944c..ad52934587 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java @@ -16,6 +16,8 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.commons.lang3.RandomStringUtils; import org.junit.Assert; import org.junit.Test; @@ -25,9 +27,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.queue.ProcessingStrategy; @@ -36,14 +42,19 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.queue.QueueStats; import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategyType; +import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.timeseries.TimeseriesDao; +import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.service.queue.TbRuleEngineConsumerStats; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult; @@ -57,20 +68,25 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest @TestPropertySource(properties = { - "queue.rule-engine.stats.max-error-message-length=100" + "queue.rule-engine.stats.max-error-message-length=100", + "transport.http.enabled=true" }) public class BaseQueueControllerTest extends AbstractControllerTest { @@ -82,6 +98,12 @@ public class BaseQueueControllerTest extends AbstractControllerTest { private TimeseriesDao timeseriesDao; @Autowired private QueueStatsService queueStatsService; + @SpyBean + private PartitionService partitionService; + @SpyBean + private TimeseriesService timeseriesService; + @SpyBean + private ActorSystemContext actorSystemContext; @Test public void testQueueWithServiceTypeRE() throws Exception { @@ -236,4 +258,75 @@ public class BaseQueueControllerTest extends AbstractControllerTest { assertThat(finalErrorMessage).isEqualTo(largeExceptionMessage.substring(0, 100) + "...[truncated 50 symbols]"); } + @Test + public void testMsgDuplicationToAllPartitions_fromTransport() throws Exception { + loginSysAdmin(); + Queue queue = new Queue(); + queue.setName("RealTime"); + queue.setTopic("tb_rule_engine.real_time"); + queue.setPollInterval(25); + int partitions = 12; + queue.setPartitions(partitions); + queue.setTenantId(TenantId.SYS_TENANT_ID); + queue.setConsumerPerPartition(true); + queue.setPackProcessingTimeout(2000); + SubmitStrategy submitStrategy = new SubmitStrategy(); + submitStrategy.setType(SubmitStrategyType.BURST); + queue.setSubmitStrategy(submitStrategy); + ProcessingStrategy processingStrategy = new ProcessingStrategy(); + processingStrategy.setType(ProcessingStrategyType.RETRY_ALL); + processingStrategy.setRetries(0); + processingStrategy.setPauseBetweenRetries(3); + processingStrategy.setMaxPauseBetweenRetries(5); + queue.setProcessingStrategy(processingStrategy); + queue.setAdditionalInfo(JacksonUtil.newObjectNode() + .put("duplicateMsgToAllPartitions", true)); + queue = saveQueue(queue); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(partitionService.resolveAll(ServiceType.TB_RULE_ENGINE, "RealTime", tenantId, tenantId)).hasSize(partitions); + }); + + loginTenantAdmin(); + DeviceProfile deviceProfile = createDeviceProfile("realtime"); + deviceProfile.setDefaultQueueName(queue.getName()); + deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + Device device = createDevice("test", deviceProfile.getName(), "test-token"); + + JsonNode payload = JacksonUtil.newObjectNode() + .put("test", "test"); + doPost("/api/v1/test-token/telemetry", payload).andExpect(status().isOk()); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + verify(timeseriesService, times(partitions)).save(eq(tenantId), eq(device.getId()), + argThat(ts -> ts.size() == 1 && ts.get(0).getKey().equals("test")), anyLong()); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbActorMsg.class); + verify(actorSystemContext, atLeastOnce()).tell(msgCaptor.capture()); + List tbMsgs = msgCaptor.getAllValues().stream() + .map(actorMsg -> actorMsg instanceof QueueToRuleEngineMsg queueToRuleEngineMsg ? queueToRuleEngineMsg.getMsg() : null) + .filter(tbMsg -> tbMsg != null && tbMsg.getCorrelationId() != null && tbMsg.getInternalType() == TbMsgType.POST_TELEMETRY_REQUEST) + .toList(); + assertThat(tbMsgs).hasSize(partitions); + UUID correlationId = tbMsgs.get(0).getCorrelationId(); + assertThat(tbMsgs).extracting(TbMsg::getCorrelationId).containsOnly(correlationId); + assertThat(tbMsgs).extracting(TbMsg::getId).doesNotHaveDuplicates(); + assertThat(tbMsgs).extracting(TbMsg::getPartition).containsExactlyInAnyOrder(IntStream.range(0, partitions).boxed().toArray(Integer[]::new)); + }); + + loginSysAdmin(); + ((ObjectNode) queue.getAdditionalInfo()).put("duplicateMsgToAllPartitions", false); + queue = saveQueue(queue); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(partitionService.resolveAll(ServiceType.TB_RULE_ENGINE, "RealTime", tenantId, tenantId)).hasSize(1); + }); + + doDelete("/api/queues/" + queue.getUuidId()).andExpect(status().isOk()); + } + + private Queue saveQueue(Queue queue) { + return doPost("/api/queues?serviceType=TB_RULE_ENGINE", queue, Queue.class); + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java index 38e537340f..71b6a52571 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java @@ -462,8 +462,7 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { basicMqttCredentials.setUserName(userName); basicMqttCredentials.setPassword(password); credentials.setCredentialsValue(JacksonUtil.toString(basicMqttCredentials)); - doPost("/api/device/credentials", credentials) - .andExpect(status().isOk()); + credentials = doPost("/api/device/credentials", credentials, DeviceCredentials.class); JsonNode commands = doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { @@ -485,6 +484,33 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { "/bin/sh -c \"curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + "mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h host.docker.internal -p 8883 -t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); + + basicMqttCredentials.setClientId(""); + credentials.setCredentialsValue(JacksonUtil.toString(basicMqttCredentials)); + + doPost("/api/device/credentials", credentials) + .andExpect(status().isOk()); + + commands = + doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { + }); + assertThat(commands).hasSize(1); + + mqttCommands = commands.get(MQTT); + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " + + "-u \"%s\" -P \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, userName, password)); + assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/mqtts/certificate/download"); + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h localhost -p 8883 " + + "-t %s -u \"%s\" -P \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, userName, password)); + + dockerMqttCommands = commands.get(MQTT).get(DOCKER); + assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h host.docker.internal" + + " -p 1883 -t %s -u \"%s\" -P \"%s\" -m \"{temperature:25}\"", + DEVICE_TELEMETRY_TOPIC, userName, password)); + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients " + + "/bin/sh -c \"curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + + "mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h host.docker.internal -p 8883 -t %s -u \"%s\" -P \"%s\" -m \"{temperature:25}\"\"", + DEVICE_TELEMETRY_TOPIC, userName, password)); } @Test diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 8303d8b9fb..388f725665 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -53,8 +53,12 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.common.data.query.RelationsQueryFilter; import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.queue.QueueStats; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -423,6 +427,65 @@ public class EntityQueryControllerTest extends AbstractControllerTest { } + @Test + public void testFindEntityDataByQueryWithNegateParam() throws Exception { + List devices = new ArrayList<>(); + for (int i = 0; i < 97; i++) { + Device device = new Device(); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(doPost("/api/device", device, Device.class)); + Thread.sleep(1); + } + + Device mainDevice = new Device(); + mainDevice.setName("Main device"); + mainDevice = doPost("/api/device", mainDevice, Device.class); + + for (int i = 0; i < 10; i++) { + EntityRelation relation = createFromRelation(mainDevice, devices.get(i), "CONTAINS"); + doPost("/api/relation", relation).andExpect(status().isOk()); + } + + for (int i = 10; i < 97; i++) { + EntityRelation relation = createFromRelation(mainDevice, devices.get(i), "NOT_CONTAINS"); + doPost("/api/relation", relation).andExpect(status().isOk()); + } + + RelationsQueryFilter filter = new RelationsQueryFilter(); + filter.setRootEntity(mainDevice.getId()); + filter.setDirection(EntitySearchDirection.FROM); + filter.setNegate(true); + filter.setFilters(List.of(new RelationEntityTypeFilter("CONTAINS", List.of(EntityType.DEVICE), false))); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null); + + PageData data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {}); + + Assert.assertEquals(87, data.getTotalElements()); + + filter.setFilters(List.of(new RelationEntityTypeFilter("NOT_CONTAINS", List.of(EntityType.DEVICE), false))); + query = new EntityDataQuery(filter, pageLink, entityFields, null, null); + data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {}); + Assert.assertEquals(10, data.getTotalElements()); + + filter.setFilters(List.of(new RelationEntityTypeFilter("NOT_CONTAINS", List.of(EntityType.DEVICE), true))); + query = new EntityDataQuery(filter, pageLink, entityFields, null, null); + data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {}); + Assert.assertEquals(87, data.getTotalElements()); + } + + private EntityRelation createFromRelation(Device mainDevice, Device device, String relationType) { + return new EntityRelation(mainDevice.getId(), device.getId(), relationType); + } + @Test public void testFindEntityDataByQueryWithAttributes() throws Exception { List devices = new ArrayList<>(); diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index e8f1a43ad1..06740e2851 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -101,7 +101,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @DaoSqlTest @TestPropertySource(properties = { - "transport.http.enabled=true", "queue.core.housekeeper.task-reprocessing-delay-ms=2000", "queue.core.housekeeper.poll-interval-ms=1000", "queue.core.housekeeper.max-reprocessing-attempts=5" diff --git a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java index ae9dbe92c4..15650ff2e6 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java @@ -21,6 +21,7 @@ import org.junit.After; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.util.Pair; +import org.springframework.jdbc.core.JdbcTemplate; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.server.common.data.User; @@ -102,6 +103,8 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest protected SqlPartitioningRepository partitioningRepository; @Autowired protected DefaultNotifications defaultNotifications; + @Autowired + private JdbcTemplate jdbcTemplate; public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test"; public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL; @@ -112,7 +115,8 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest notificationRuleService.deleteNotificationRulesByTenantId(TenantId.SYS_TENANT_ID); notificationTemplateService.deleteNotificationTemplatesByTenantId(TenantId.SYS_TENANT_ID); notificationTargetService.deleteNotificationTargetsByTenantId(TenantId.SYS_TENANT_ID); - partitioningRepository.dropPartitionsBefore("notification", Long.MAX_VALUE, 1); + jdbcTemplate.execute("TRUNCATE TABLE notification"); + partitioningRepository.cleanupPartitionsCache("notification", Long.MAX_VALUE, 0); notificationSettingsService.deleteNotificationSettings(TenantId.SYS_TENANT_ID); } diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java index 1a788d437b..883a173edb 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -331,7 +331,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest { @Test public void testNotificationUpdatesForSeveralUsers() throws Exception { - int usersCount = 150; + int usersCount = 50; Map sessions = new HashMap<>(); List targets = new ArrayList<>(); diff --git a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java index 33b0fc7801..22f3620d8c 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java @@ -33,6 +33,7 @@ import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.TbRuleEngineProducerService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; @@ -77,6 +78,8 @@ public class DefaultTbClusterServiceTest { protected PartitionService partitionService; @MockBean protected TbQueueProducerProvider producerProvider; + @MockBean + protected TbRuleEngineProducerService ruleEngineProducerService; @SpyBean protected TopicService topicService; diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java index 4d4d59a407..fdf2e203ee 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java @@ -21,10 +21,10 @@ import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.testcontainers.shaded.org.apache.commons.lang3.RandomUtils; @@ -55,6 +55,8 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.provider.KafkaMonolithQueueFactory; +import org.thingsboard.server.queue.provider.KafkaTbRuleEngineQueueFactory; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategyFactory; @@ -79,10 +81,12 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willCallRealMethod; import static org.mockito.Mockito.after; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; @@ -114,6 +118,7 @@ public class TbRuleEngineQueueConsumerManagerTest { private PartitionService partitionService; @Mock private TbQueueProducerProvider producerProvider; + @Mock private TbQueueProducer> ruleEngineMsgProducer; @Mock private TbQueueAdmin queueAdmin; @@ -148,7 +153,7 @@ public class TbRuleEngineQueueConsumerManagerTest { log.trace("totalProcessedMsgs = {}", totalProcessedMsgs); return null; }).when(actorContext).tell(any()); - ruleEngineMsgProducer = mock(TbQueueProducer.class); + when(producerProvider.getRuleEngineMsgProducer()).thenReturn(ruleEngineMsgProducer); consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer")); mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(3, "tb-rule-engine-mgmt"); @@ -180,7 +185,7 @@ public class TbRuleEngineQueueConsumerManagerTest { } consumers.add(consumer); return consumer; - }).when(queueFactory).createToRuleEngineMsgConsumer(any()); + }).when(queueFactory).createToRuleEngineMsgConsumer(any(), any()); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue); consumerManager = TbRuleEngineQueueConsumerManager.create() @@ -210,6 +215,20 @@ public class TbRuleEngineQueueConsumerManagerTest { } } + @ParameterizedTest + @ValueSource(classes = {KafkaMonolithQueueFactory.class, KafkaTbRuleEngineQueueFactory.class}) + public void testUnsupported_createToRuleEngineMsgConsumer_KafkaTbRuleEngineQueueFactory(Class factoryClass) { + // obsolete, but need to pass the afterEach + queue.setConsumerPerPartition(false); + consumerManager.init(queue); + + var factory = mock(factoryClass); + willCallRealMethod().given(factory).createToRuleEngineMsgConsumer(any()); + assertThatThrownBy(() -> factory.createToRuleEngineMsgConsumer(mock(Queue.class))) + .isInstanceOf(UnsupportedOperationException.class); + + } + @Test public void testInit_consumerPerPartition() { queue.setConsumerPerPartition(true); @@ -247,7 +266,8 @@ public class TbRuleEngineQueueConsumerManagerTest { Set partitions = Collections.emptySet(); consumerManager.update(partitions); - verify(queueFactory, after(1000).never()).createToRuleEngineMsgConsumer(any()); + verify(queueFactory, after(1000).never()).createToRuleEngineMsgConsumer(any(), any()); + verify(queueFactory, never()).createToRuleEngineMsgConsumer(any()); partitions = createTpis(1); consumerManager.update(partitions); @@ -278,7 +298,8 @@ public class TbRuleEngineQueueConsumerManagerTest { consumerManager.init(queue); consumerManager.update(Collections.emptySet()); - verify(queueFactory, after(1000).never()).createToRuleEngineMsgConsumer(any()); + verify(queueFactory, after(1000).never()).createToRuleEngineMsgConsumer(any(), any()); + verify(queueFactory, never()).createToRuleEngineMsgConsumer(any()); consumerManager.update(createTpis(1)); TestConsumer consumer1 = getConsumer(1); @@ -420,7 +441,8 @@ public class TbRuleEngineQueueConsumerManagerTest { consumerManager.update(createTpis(1)); TestConsumer consumer = getConsumer(1); verifySubscribedAndLaunched(consumer, 1); - verify(queueFactory, times(1)).createToRuleEngineMsgConsumer(any()); + verify(queueFactory, times(1)).createToRuleEngineMsgConsumer(any(), any()); + verify(queueFactory, never()).createToRuleEngineMsgConsumer(any()); consumerManager.stop(); consumerManager.update(createTpis(1, 2, 3, 4)); // to check that no new tasks after stop are processed diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java index e6c6cfbac4..e9d874084d 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java @@ -73,16 +73,7 @@ public class JwtTokenFactoryTest { @Test public void testCreateAndParseAccessJwtToken() { - SecurityUser securityUser = new SecurityUser(); - securityUser.setId(new UserId(UUID.randomUUID())); - securityUser.setEmail("tenant@thingsboard.org"); - securityUser.setAuthority(Authority.TENANT_ADMIN); - securityUser.setTenantId(new TenantId(UUID.randomUUID())); - securityUser.setEnabled(true); - securityUser.setFirstName("A"); - securityUser.setLastName("B"); - securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, securityUser.getEmail())); - securityUser.setCustomerId(new CustomerId(UUID.randomUUID())); + SecurityUser securityUser = createSecurityUser(); testCreateAndParseAccessJwtToken(securityUser); @@ -111,18 +102,12 @@ public class JwtTokenFactoryTest { assertThat(parsedSecurityUser.getCustomerId()).isEqualTo(securityUser.getCustomerId()); assertThat(parsedSecurityUser.getFirstName()).isEqualTo(securityUser.getFirstName()); assertThat(parsedSecurityUser.getLastName()).isEqualTo(securityUser.getLastName()); + assertThat(parsedSecurityUser.getSessionId()).isNotNull().isEqualTo(securityUser.getSessionId()); } @Test public void testCreateAndParseRefreshJwtToken() { - SecurityUser securityUser = new SecurityUser(); - securityUser.setId(new UserId(UUID.randomUUID())); - securityUser.setEmail("tenant@thingsboard.org"); - securityUser.setAuthority(Authority.TENANT_ADMIN); - securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, securityUser.getEmail())); - securityUser.setEnabled(true); - securityUser.setTenantId(new TenantId(UUID.randomUUID())); - securityUser.setCustomerId(new CustomerId(UUID.randomUUID())); + SecurityUser securityUser = createSecurityUser(); JwtToken refreshToken = tokenFactory.createRefreshToken(securityUser); checkExpirationTime(refreshToken, jwtSettings.getRefreshTokenExpTime()); @@ -138,15 +123,7 @@ public class JwtTokenFactoryTest { @Test public void testCreateAndParsePreVerificationJwtToken() { - SecurityUser securityUser = new SecurityUser(); - securityUser.setId(new UserId(UUID.randomUUID())); - securityUser.setEmail("tenant@thingsboard.org"); - securityUser.setAuthority(Authority.TENANT_ADMIN); - securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, securityUser.getEmail())); - securityUser.setEnabled(true); - securityUser.setTenantId(new TenantId(UUID.randomUUID())); - securityUser.setCustomerId(new CustomerId(UUID.randomUUID())); - + SecurityUser securityUser = createSecurityUser(); int tokenLifetime = (int) TimeUnit.MINUTES.toSeconds(30); JwtToken preVerificationToken = tokenFactory.createPreVerificationToken(securityUser, tokenLifetime); checkExpirationTime(preVerificationToken, tokenLifetime); @@ -162,6 +139,34 @@ public class JwtTokenFactoryTest { }); } + @Test + public void testSessionId() { + SecurityUser securityUser = createSecurityUser(); + String sessionId = securityUser.getSessionId(); + + String accessToken = tokenFactory.createAccessJwtToken(securityUser).getToken(); + securityUser = tokenFactory.parseAccessJwtToken(accessToken); + assertThat(securityUser.getSessionId()).isNotNull().isEqualTo(sessionId); + + String newAccessToken = tokenFactory.createTokenPair(securityUser).getToken(); + securityUser = tokenFactory.parseAccessJwtToken(newAccessToken); + assertThat(securityUser.getSessionId()).isNotNull().isNotEqualTo(sessionId); + } + + private SecurityUser createSecurityUser() { + SecurityUser securityUser = new SecurityUser(); + securityUser.setId(new UserId(UUID.randomUUID())); + securityUser.setEmail("tenant@thingsboard.org"); + securityUser.setAuthority(Authority.TENANT_ADMIN); + securityUser.setTenantId(new TenantId(UUID.randomUUID())); + securityUser.setEnabled(true); + securityUser.setFirstName("A"); + securityUser.setLastName("B"); + securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, securityUser.getEmail())); + securityUser.setCustomerId(new CustomerId(UUID.randomUUID())); + return securityUser; + } + private void mockJwtSettings(JwtSettings settings) { AdminSettings adminJwtSettings = new AdminSettings(); adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(settings)); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index a44d2b6a62..983cfde0e5 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -178,6 +178,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte private String[] resources; protected String deviceId; protected boolean isWriteAttribute = false; + protected boolean supportFormatOnly_SenMLJSON_SenMLCBOR = false; @Before public void startInit() throws Exception { @@ -318,8 +319,8 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte try (ServerSocket socket = new ServerSocket(0)) { int clientPort = socket.getLocalPort(); lwM2MTestClient.init(security, securityBs, clientPort, isRpc, - this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest, isWriteAttribute - , clientDtlsCidLength, queueMode); + this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest, isWriteAttribute, + clientDtlsCidLength, queueMode, supportFormatOnly_SenMLJSON_SenMLCBOR); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java index 00a5510788..02ff8be9d4 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -47,10 +47,19 @@ import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.StaticModel; import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder; import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder; +import org.eclipse.leshan.core.node.codec.NodeDecoder; +import org.eclipse.leshan.core.node.codec.NodeEncoder; +import org.eclipse.leshan.core.node.codec.cbor.LwM2mNodeCborDecoder; +import org.eclipse.leshan.core.node.codec.cbor.LwM2mNodeCborEncoder; +import org.eclipse.leshan.core.node.codec.senml.LwM2mNodeSenMLDecoder; +import org.eclipse.leshan.core.node.codec.senml.LwM2mNodeSenMLEncoder; import org.eclipse.leshan.core.request.BootstrapRequest; +import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.DeregisterRequest; import org.eclipse.leshan.core.request.RegisterRequest; import org.eclipse.leshan.core.request.UpdateRequest; +import org.eclipse.leshan.senml.cbor.upokecenter.SenMLCborUpokecenterEncoderDecoder; +import org.eclipse.leshan.senml.json.jackson.SenMLJsonJacksonEncoderDecoder; import org.junit.Assert; import org.mockito.Mockito; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; @@ -78,6 +87,7 @@ import static org.eclipse.leshan.core.LwM2mId.LOCATION; import static org.eclipse.leshan.core.LwM2mId.SECURITY; import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT; +import static org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder.getDefaultPathEncoder; import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverId; import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverIdBs; import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.shortServerId; @@ -130,7 +140,8 @@ public class LwM2MTestClient { public void init(Security security, Security securityBs, int port, boolean isRpc, LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, - LwM2mClientContext clientContext, boolean isWriteAttribute, Integer cIdLength, boolean queueMode) throws InvalidDDFFileException, IOException { + LwM2mClientContext clientContext, boolean isWriteAttribute, Integer cIdLength, boolean queueMode, + boolean supportFormatOnly_SenMLJSON_SenMLCBOR) throws InvalidDDFFileException, IOException { Assert.assertNull("client already initialized", leshanClient); this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler; this.clientContext = clientContext; @@ -274,12 +285,28 @@ public class LwM2MTestClient { builder.setEndpointsProviders(endpointsProvider.toArray(new LwM2mClientEndpointsProvider[endpointsProvider.size()])); builder.setDataSenders(new ManualDataSender()); builder.setRegistrationEngineFactory(engineFactory); - boolean supportOldFormat = true; - if (supportOldFormat) { + Map decoders = new HashMap<>(); + Map encoders = new HashMap<>(); + if (supportFormatOnly_SenMLJSON_SenMLCBOR) { +// decoders.put(ContentFormat.OPAQUE, new LwM2mNodeOpaqueDecoder()); + decoders.put(ContentFormat.CBOR, new LwM2mNodeCborDecoder()); + decoders.put(ContentFormat.SENML_JSON, new LwM2mNodeSenMLDecoder(new SenMLJsonJacksonEncoderDecoder(), true)); + decoders.put(ContentFormat.SENML_CBOR, new LwM2mNodeSenMLDecoder(new SenMLCborUpokecenterEncoderDecoder(), false)); + builder.setDecoder(new DefaultLwM2mDecoder(decoders)); + +// encoders.put(ContentFormat.OPAQUE, new LwM2mNodeOpaqueEncoder()); + encoders.put(ContentFormat.CBOR, new LwM2mNodeCborEncoder()); + encoders.put(ContentFormat.SENML_JSON, new LwM2mNodeSenMLEncoder(new SenMLJsonJacksonEncoderDecoder())); + encoders.put(ContentFormat.SENML_CBOR, new LwM2mNodeSenMLEncoder(new SenMLCborUpokecenterEncoderDecoder())); + builder.setEncoder(new DefaultLwM2mEncoder(new LwM2mValueConverterImpl(), false)); + builder.setEncoder(new DefaultLwM2mEncoder(encoders, getDefaultPathEncoder(), new LwM2mValueConverterImpl())); + } else { + boolean supportOldFormat = true; builder.setDecoder(new DefaultLwM2mDecoder(supportOldFormat)); builder.setEncoder(new DefaultLwM2mEncoder(new LwM2mValueConverterImpl(), supportOldFormat)); } + builder.setRegistrationEngineFactory(engineFactory); builder.setSharedExecutor(executor); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index 1c6e384ae1..b5f23c3e9a 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -86,6 +86,9 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationDiscoverWriteAttributesTest")){ isWriteAttribute = true; } + if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationWriteCborTest")){ + supportFormatOnly_SenMLJSON_SenMLCBOR = true; + } initRpc(); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteCborTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteCborTest.java new file mode 100644 index 0000000000..2850d5b8cd --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteCborTest.java @@ -0,0 +1,98 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.rpc.sql; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.eclipse.leshan.core.ResponseCode; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; + +public class RpcLwm2mIntegrationWriteCborTest extends AbstractRpcLwM2MIntegrationTest { + + /** + * Client with ContentFormat ={} + */ + + /** + * Resource: + * Opaque + * Input type String (HexDec) + * WriteReplace {"id": "/19_1.1/0/0","value": {"0":"01234567890CAB"}} + * return Hex.decodeHex(((String)value).toCharArray()) - ok [1, 35, 69, 103, -119, 12, -85, -1] + * {"result":"CHANGED"} -> Actual Value Equals expectedValue0 + */ + @Test + public void testWriteReplaceValueMultipleResource_InputFormatData_Ok_Result_CHANGED_Value_Equals_expectedValue0() throws Exception { + String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0; + int resourceInstanceId0 = 0; + String expectedValue0 = "01234567890CABFF"; + String expectedValue = "{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\"}"; + String actualResult = sendRPCWriteObjectById("WriteReplace", expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); + String expectedPath0 = expectedPath + "/" + resourceInstanceId0; + actualResult = sendRPCReadById(expectedPath0); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + String actualValues = rpcActualResult.get("value").asText(); + String expected = "LwM2mResourceInstance [id=" + resourceInstanceId0 + ", value=" + expectedValue0.length()/2 + "Bytes, type=OPAQUE]"; + assertTrue(actualValues.contains(expected)); + } + + /** + * Resource: + * Opaque + * Input type String (not HexDec) + * return Hex.decodeHex(((String)value).toCharArray()) - error + * return Base64.getDecoder().decode(((String) value).getBytes()) - ok; + * WriteReplace {"id": "/19_1.1/0/0","value": {"0":"01234567890"}} + * {"result":"CHANGED"} -> Actual Value Not Equals expectedValue0 + */ + @Test + public void testWriteReplaceValueMultipleResource_Error_InputFormatData_Result_CHANGED_Value_Not_Equals_expectedValue0() throws Exception { + String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0; + int resourceInstanceId0 = 0; + String expectedValue0 = "01234567890"; + String expectedValue = "{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\"}"; + String actualResult = sendRPCWriteObjectById("WriteReplace", expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); + String expectedPath0 = expectedPath + "/" + resourceInstanceId0; + actualResult = sendRPCReadById(expectedPath0); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + String actualValues = rpcActualResult.get("value").asText(); + String expected = "LwM2mResourceInstance [id=" + resourceInstanceId0 + ", value=" + expectedValue0.length()/2 + "Bytes, type=OPAQUE]"; + assertFalse(actualValues.contains(expected)); + } + + + private String sendRPCWriteObjectById(String method, String path, Object value) throws Exception { + String setRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"id\": \"" + path + "\", \"value\": " + value + " }}"; + return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); + } + + private String sendRPCReadById(String id) throws Exception { + String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + id + "\"}}"; + return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java index a230b17eb3..f99826780e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java @@ -23,6 +23,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; @@ -107,6 +108,32 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes assertTrue(actualValues.contains(expected)); } + /** + * Resource: + * Opaque + * Input type String (not HexDec) + * return Hex.decodeHex(((String)value).toCharArray()) - error + * return Base64.getDecoder().decode(((String) value).getBytes()) - ok; + * WriteReplace {"id": "/19_1.1/0/0","value": {"0":"01234567890"}} + * {"result":"CHANGED"} -> Actual Value Not Equals expectedValue0 + */ + @Test + public void testWriteReplaceValueMultipleResource_Error_InputFormatData_Result_CHANGED_Value_Not_Equals_expectedValue0() throws Exception { + String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0; + int resourceInstanceId0 = 0; + String expectedValue0 = "01234567890"; + String expectedValue = "{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\"}"; + String actualResult = sendRPCWriteObjectById("WriteReplace", expectedPath, expectedValue); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); + String expectedPath0 = expectedPath + "/" + resourceInstanceId0; + actualResult = sendRPCReadById(expectedPath0); + rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + String actualValues = rpcActualResult.get("value").asText(); + String expected = "LwM2mResourceInstance [id=" + resourceInstanceId0 + ", value=" + expectedValue0.length()/2 + "Bytes, type=OPAQUE]"; + assertFalse(actualValues.contains(expected)); + } + /** * bad: singleResource, operation="R" - only read * WriteReplace {"id":"/3/0/9","value":90} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java index 794d93a44a..a1986ca2c2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java @@ -81,7 +81,8 @@ public class MqttGatewayRateLimitsTest extends AbstractControllerTest { @Test public void transportGatewayMsgRateLimitTest() throws Exception { - transportGatewayRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayMsgRateLimit("1:600")); + // Device A 2 msgs success ('create device', 'to device actor'), Device B 'create device success' , 'to device actor' - limited + transportGatewayRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayMsgRateLimit("3:600")); } @Test diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerContext.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerContext.java index 06a4d5d690..46caba5f29 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerContext.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerContext.java @@ -27,29 +27,13 @@ import org.springframework.stereotype.Component; public class CoapServerContext { @Getter - @Value("${transport.coap.bind_address}") + @Value("${coap.bind_address}") private String host; @Getter - @Value("${transport.coap.bind_port}") + @Value("${coap.bind_port}") private Integer port; - @Getter - @Value("${transport.coap.timeout}") - private Long timeout; - - @Getter - @Value("${transport.coap.piggyback_timeout}") - private Long piggybackTimeout; - - @Getter - @Value("${transport.coap.psm_activity_timer:10000}") - private long psmActivityTimer; - - @Getter - @Value("${transport.coap.paging_transmission_window:10000}") - private long pagingTransmissionWindow; - @Getter @Autowired(required = false) private TbCoapDtlsSettings dtlsSettings; diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerService.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerService.java index c5c30d4bc0..0b1ba35709 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerService.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerService.java @@ -26,9 +26,4 @@ public interface CoapServerService { CoapServer getCoapServer() throws UnknownHostException; ConcurrentMap getDtlsSessionsMap(); - - long getTimeout(); - - long getPiggybackTimeout(); - } diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java index 0eca05aab2..508103a1db 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java @@ -83,16 +83,6 @@ public class DefaultCoapServerService implements CoapServerService { return tbDtlsCertificateVerifier != null ? tbDtlsCertificateVerifier.getTbCoapDtlsSessionsMap() : null; } - @Override - public long getTimeout() { - return coapServerContext.getTimeout(); - } - - @Override - public long getPiggybackTimeout() { - return coapServerContext.getPiggybackTimeout(); - } - private CoapServer createCoapServer() throws UnknownHostException { Configuration networkConfig = new Configuration(); networkConfig.set(CoapConfig.BLOCKWISE_STRICT_BLOCK2_OPTION, true); diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java index c2cf1a4603..f83a20b139 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java @@ -50,24 +50,24 @@ import static org.eclipse.californium.scandium.config.DtlsConfig.DtlsRole.SERVER @Getter @Slf4j -@ConditionalOnProperty(prefix = "transport.coap.dtls", value = "enabled", havingValue = "true", matchIfMissing = false) +@ConditionalOnProperty(prefix = "coap.dtls", value = "enabled", havingValue = "true") @Component public class TbCoapDtlsSettings { - @Value("${transport.coap.dtls.bind_address}") + @Value("${coap.dtls.bind_address}") private String host; - @Value("${transport.coap.dtls.bind_port}") + @Value("${coap.dtls.bind_port}") private Integer port; - @Value("${transport.coap.dtls.retransmission_timeout:9000}") + @Value("${coap.dtls.retransmission_timeout:9000}") private int dtlsRetransmissionTimeout; - @Value("${transport.coap.dtls.connection_id_length:}") + @Value("${coap.dtls.connection_id_length:}") private Integer cIdLength; @Bean - @ConfigurationProperties(prefix = "transport.coap.dtls.credentials") + @ConfigurationProperties(prefix = "coap.dtls.credentials") public SslCredentialsConfig coapDtlsCredentials() { return new SslCredentialsConfig("COAP DTLS Credentials", false); } @@ -76,19 +76,19 @@ public class TbCoapDtlsSettings { @Qualifier("coapDtlsCredentials") private SslCredentialsConfig coapDtlsCredentialsConfig; - @Value("${transport.coap.dtls.x509.skip_validity_check_for_client_cert:false}") + @Value("${coap.dtls.x509.skip_validity_check_for_client_cert:false}") private boolean skipValidityCheckForClientCert; - @Value("${transport.coap.dtls.x509.dtls_session_inactivity_timeout:86400000}") + @Value("${coap.dtls.x509.dtls_session_inactivity_timeout:86400000}") private long dtlsSessionInactivityTimeout; - @Value("${transport.coap.dtls.x509.dtls_session_report_timeout:1800000}") + @Value("${coap.dtls.x509.dtls_session_report_timeout:1800000}") private long dtlsSessionReportTimeout; - @Autowired + @Autowired(required = false) private TransportService transportService; - @Autowired + @Autowired(required = false) private TbServiceInfoProvider serviceInfoProvider; public DtlsConnectorConfig dtlsConnectorConfig(Configuration configuration) throws UnknownHostException { @@ -100,8 +100,8 @@ public class TbCoapDtlsSettings { configBuilder.set(DTLS_CLIENT_AUTHENTICATION_MODE, WANTED); configBuilder.set(DTLS_RETRANSMISSION_TIMEOUT, dtlsRetransmissionTimeout, MILLISECONDS); configBuilder.set(DTLS_ROLE, SERVER_ONLY); - configBuilder.set(DTLS_CONNECTION_ID_LENGTH, cIdLength); if (cIdLength != null) { + configBuilder.set(DTLS_CONNECTION_ID_LENGTH, cIdLength); if (cIdLength > 4) { configBuilder.set(DTLS_CONNECTION_ID_NODE_ID, 0); } else { diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerComponent.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerComponent.java index c468c71095..2d94de2682 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerComponent.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerComponent.java @@ -21,6 +21,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) -@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')") +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${coap.enabled}'=='true')") public @interface TbCoapServerComponent { } diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapTransportComponent.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapTransportComponent.java new file mode 100644 index 0000000000..33f4b36b70 --- /dev/null +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapTransportComponent.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.coapserver; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || " + + "('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${coap.enabled}'=='true' && '${transport.coap.enabled}'=='true')") +public @interface TbCoapTransportComponent { +} diff --git a/common/coap-server/src/test/java/org/thingsboard/server/coapserver/TbCoapDtlsSettingsTest.java b/common/coap-server/src/test/java/org/thingsboard/server/coapserver/TbCoapDtlsSettingsTest.java index 72f5b964ae..870c4210e5 100644 --- a/common/coap-server/src/test/java/org/thingsboard/server/coapserver/TbCoapDtlsSettingsTest.java +++ b/common/coap-server/src/test/java/org/thingsboard/server/coapserver/TbCoapDtlsSettingsTest.java @@ -28,14 +28,14 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(classes = TbCoapDtlsSettings.class) @TestPropertySource(properties = { - "transport.coap.dtls.enabled=true", - "transport.coap.dtls.bind_address=192.168.1.1", - "transport.coap.dtls.bind_port=1234", - "transport.coap.dtls.retransmission_timeout=100", - "transport.coap.dtls.connection_id_length=500", - "transport.coap.dtls.x509.skip_validity_check_for_client_cert=true", - "transport.coap.dtls.x509.dtls_session_inactivity_timeout=1000", - "transport.coap.dtls.x509.dtls_session_report_timeout=3000", + "coap.dtls.enabled=true", + "coap.dtls.bind_address=192.168.1.1", + "coap.dtls.bind_port=1234", + "coap.dtls.retransmission_timeout=100", + "coap.dtls.connection_id_length=500", + "coap.dtls.x509.skip_validity_check_for_client_cert=true", + "coap.dtls.x509.dtls_session_inactivity_timeout=1000", + "coap.dtls.x509.dtls_session_report_timeout=3000", }) class TbCoapDtlsSettingsTest { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java index 5b6d20adfd..f6b0bdb9c6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java @@ -18,17 +18,17 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.validation.NoXss; import java.io.ByteArrayInputStream; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Objects; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; /** @@ -64,6 +64,23 @@ public abstract class BaseDataWithAdditionalInfo extends Ba setJson(addInfo, json -> this.additionalInfo = json, bytes -> this.additionalInfoBytes = bytes); } + public void setAdditionalInfoField(String field, JsonNode value) { + JsonNode additionalInfo = getAdditionalInfo(); + if (!(additionalInfo instanceof ObjectNode)) { + additionalInfo = mapper.createObjectNode(); + } + ((ObjectNode) additionalInfo).set(field, value); + setAdditionalInfo(additionalInfo); + } + + public T getAdditionalInfoField(String field, Function mapper, T defaultValue) { + JsonNode additionalInfo = getAdditionalInfo(); + if (additionalInfo != null && additionalInfo.has(field)) { + return mapper.apply(additionalInfo.get(field)); + } + return defaultValue; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java index b3f975870e..8bc9b23481 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/QRCodeConfig.java @@ -33,7 +33,6 @@ public class QRCodeConfig { private boolean badgeEnabled; private boolean qrCodeLabelEnabled; private BadgePosition badgePosition; - private BadgeStyle badgeStyle; @NoXss private String qrCodeLabel; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java index 8f750eae2c..9fefeff2c7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java @@ -40,5 +40,6 @@ public class RelationsQueryFilter implements EntityFilter { private List filters; private int maxLevel; private boolean fetchLastLevelOnly; + private boolean negate; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java index 1c08072e61..ad5b9cf808 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java @@ -73,4 +73,11 @@ public class Queue extends BaseDataWithAdditionalInfo implements HasNam .filter(JsonNode::isTextual).map(JsonNode::asText).orElse(null); } + @JsonIgnore + public boolean isDuplicateMsgToAllPartitions() { + return Optional.ofNullable(getAdditionalInfo()) + .map(info -> info.get("duplicateMsgToAllPartitions")) + .filter(JsonNode::isBoolean).map(JsonNode::asBoolean).orElse(false); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java index f4eccf7984..2baca28f0b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java @@ -16,7 +16,6 @@ package org.thingsboard.server.common.data.relation; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; import lombok.Data; import org.thingsboard.server.common.data.EntityType; @@ -26,13 +25,27 @@ import java.util.List; * Created by ashvayka on 02.05.17. */ @Data -@AllArgsConstructor @Schema public class RelationEntityTypeFilter { + public RelationEntityTypeFilter() {} + + public RelationEntityTypeFilter(String relationType, List entityTypes) { + this(relationType, entityTypes, false); + } + + public RelationEntityTypeFilter(String relationType, List entityTypes, boolean negate) { + this.relationType = relationType; + this.entityTypes = entityTypes; + this.negate = negate; + } + @Schema(description = "Type of the relation between root entity and other entity (e.g. 'Contains' or 'Manages').", example = "Contains") private String relationType; @Schema(description = "Array of entity types to filter the related entities (e.g. 'DEVICE', 'ASSET').") private List entityTypes; + + @Schema(description = "Negate relation type between root entity and other entity.") + private boolean negate; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java index ae484136ce..d95855fe04 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.security.model; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.NoArgsConstructor; @@ -25,6 +26,7 @@ import java.io.Serializable; @Schema(description = "JWT Pair") @Data @NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) public class JwtPair implements Serializable { @Schema(description = "The JWT Access Token. Used to perform API calls.", example = "AAB254FF67D..") diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index cbf9ae34b3..47fb80c90e 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -42,6 +44,8 @@ import java.util.UUID; */ @Data @Slf4j +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder(toBuilder = true) public final class TbMsg implements Serializable { public static final String EMPTY_JSON_OBJECT = "{}"; @@ -60,6 +64,10 @@ public final class TbMsg implements Serializable { private final String data; private final RuleChainId ruleChainId; private final RuleNodeId ruleNodeId; + + private final UUID correlationId; + private final Integer partition; + @Getter(value = AccessLevel.NONE) @JsonIgnore //This field is not serialized because we use queues and there is no need to do it @@ -257,7 +265,7 @@ public final class TbMsg implements Serializable { @Deprecated(since = "3.6.0") public static TbMsg transformMsg(TbMsg tbMsg, String type, EntityId originator, TbMsgMetaData metaData, String data) { return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, null, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, - data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback); + data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.callback); } public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { @@ -271,54 +279,54 @@ public final class TbMsg implements Serializable { } public static TbMsg transformMsg(TbMsg tbMsg, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data) { - return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, - data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback); + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type, type.name(), originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, + data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.callback); } public static TbMsg transformMsgOriginator(TbMsg tbMsg, EntityId originatorId) { return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, originatorId, tbMsg.getCustomerId(), tbMsg.metaData, tbMsg.dataType, - tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); + tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsgData(TbMsg tbMsg, String data) { return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, - data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); + data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsgMetadata(TbMsg tbMsg, TbMsgMetaData metadata) { return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata.copy(), tbMsg.dataType, - tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); + tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsg(TbMsg tbMsg, TbMsgMetaData metadata, String data) { return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata, tbMsg.dataType, - data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); + data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsgCustomerId(TbMsg tbMsg, CustomerId customerId) { return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, customerId, tbMsg.metaData, tbMsg.dataType, - tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); + tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsgRuleChainId(TbMsg tbMsg, RuleChainId ruleChainId) { return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, - tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback()); + tbMsg.data, ruleChainId, null, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsgQueueName(TbMsg tbMsg, String queueName) { return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, - tbMsg.data, tbMsg.getRuleChainId(), null, tbMsg.ctx.copy(), tbMsg.getCallback()); + tbMsg.data, tbMsg.getRuleChainId(), null, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsg(TbMsg tbMsg, RuleChainId ruleChainId, String queueName) { return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.internalType, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, - tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback()); + tbMsg.data, ruleChainId, null, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), tbMsg.getCallback()); } //used for enqueueForTellNext public static TbMsg newMsg(TbMsg tbMsg, String queueName, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { return new TbMsg(queueName, UUID.randomUUID(), tbMsg.getTs(), tbMsg.getInternalType(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.customerId, tbMsg.getMetaData().copy(), - tbMsg.getDataType(), tbMsg.getData(), ruleChainId, ruleNodeId, tbMsg.ctx.copy(), TbMsgCallback.EMPTY); + tbMsg.getDataType(), tbMsg.getData(), ruleChainId, ruleNodeId, tbMsg.correlationId, tbMsg.partition, tbMsg.ctx.copy(), TbMsgCallback.EMPTY); } private TbMsg(String queueName, UUID id, long ts, TbMsgType internalType, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data, @@ -328,6 +336,11 @@ public final class TbMsg implements Serializable { private TbMsg(String queueName, UUID id, long ts, TbMsgType internalType, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId, TbMsgProcessingCtx ctx, TbMsgCallback callback) { + this(queueName, id, ts, internalType, type, originator, customerId, metaData, dataType, data, ruleChainId, ruleNodeId, null, null, ctx, callback); + } + + private TbMsg(String queueName, UUID id, long ts, TbMsgType internalType, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data, + RuleChainId ruleChainId, RuleNodeId ruleNodeId, UUID correlationId, Integer partition, TbMsgProcessingCtx ctx, TbMsgCallback callback) { this.id = id; this.queueName = queueName; if (ts > 0) { @@ -352,6 +365,8 @@ public final class TbMsg implements Serializable { this.data = data; this.ruleChainId = ruleChainId; this.ruleNodeId = ruleNodeId; + this.correlationId = correlationId; + this.partition = partition; this.ctx = ctx != null ? ctx : new TbMsgProcessingCtx(); this.callback = Objects.requireNonNullElse(callback, TbMsgCallback.EMPTY); } @@ -391,6 +406,14 @@ public final class TbMsg implements Serializable { builder.setDataType(msg.getDataType().ordinal()); builder.setData(msg.getData()); + if (msg.getCorrelationId() != null) { + builder.setCorrelationIdMSB(msg.getCorrelationId().getMostSignificantBits()); + builder.setCorrelationIdLSB(msg.getCorrelationId().getLeastSignificantBits()); + } + if (msg.getPartition() != null) { + builder.setPartition(msg.getPartition()); + } + builder.setCtx(msg.ctx.toProto()); return builder.build().toByteArray(); } @@ -403,6 +426,8 @@ public final class TbMsg implements Serializable { CustomerId customerId = null; RuleChainId ruleChainId = null; RuleNodeId ruleNodeId = null; + UUID correlationId = null; + Integer partition = null; if (proto.getCustomerIdMSB() != 0L && proto.getCustomerIdLSB() != 0L) { customerId = new CustomerId(new UUID(proto.getCustomerIdMSB(), proto.getCustomerIdLSB())); } @@ -412,6 +437,10 @@ public final class TbMsg implements Serializable { if (proto.getRuleNodeIdMSB() != 0L && proto.getRuleNodeIdLSB() != 0L) { ruleNodeId = new RuleNodeId(new UUID(proto.getRuleNodeIdMSB(), proto.getRuleNodeIdLSB())); } + if (proto.getCorrelationIdMSB() != 0L && proto.getCorrelationIdLSB() != 0L) { + correlationId = new UUID(proto.getCorrelationIdMSB(), proto.getCorrelationIdLSB()); + partition = proto.getPartition(); + } TbMsgProcessingCtx ctx; if (proto.hasCtx()) { @@ -423,7 +452,7 @@ public final class TbMsg implements Serializable { TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()]; return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), null, proto.getType(), entityId, customerId, - metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, ctx, callback); + metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, correlationId, partition, ctx, callback); } catch (InvalidProtocolBufferException e) { throw new IllegalStateException("Could not parse protobuf for TbMsg", e); } @@ -435,17 +464,17 @@ public final class TbMsg implements Serializable { public TbMsg copyWithRuleChainId(RuleChainId ruleChainId, UUID msgId) { return new TbMsg(this.queueName, msgId, this.ts, this.internalType, this.type, this.originator, this.customerId, - this.metaData, this.dataType, this.data, ruleChainId, null, this.ctx, callback); + this.metaData, this.dataType, this.data, ruleChainId, null, this.correlationId, this.partition, this.ctx, callback); } public TbMsg copyWithRuleNodeId(RuleChainId ruleChainId, RuleNodeId ruleNodeId, UUID msgId) { return new TbMsg(this.queueName, msgId, this.ts, this.internalType, this.type, this.originator, this.customerId, - this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx, callback); + this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.correlationId, this.partition, this.ctx, callback); } public TbMsg copyWithNewCtx() { return new TbMsg(this.queueName, this.id, this.ts, this.internalType, this.type, this.originator, this.customerId, - this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx.copy(), TbMsgCallback.EMPTY); + this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.correlationId, this.partition, this.ctx.copy(), TbMsgCallback.EMPTY); } public TbMsgCallback getCallback() { diff --git a/common/message/src/main/proto/tbmsg.proto b/common/message/src/main/proto/tbmsg.proto index 4c2d110888..fc9265aa14 100644 --- a/common/message/src/main/proto/tbmsg.proto +++ b/common/message/src/main/proto/tbmsg.proto @@ -66,4 +66,8 @@ message TbMsgProto { int64 customerIdLSB = 18; TbMsgProcessingCtxProto ctx = 19; -} \ No newline at end of file + + int64 correlationIdMSB = 20; + int64 correlationIdLSB = 21; + int32 partition = 22; +} diff --git a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java index 7cfef02873..f5c7bbb901 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java @@ -466,6 +466,13 @@ public class JsonConverter { return result; } + public static JsonObject toGatewayDeviceDisconnectJson(String deviceName, int reasonCode) { + JsonObject result = new JsonObject(); + result.addProperty(DEVICE_PROPERTY, deviceName); + result.addProperty("reason", reasonCode); + return result; + } + public static JsonElement toErrorJson(String errorMsg) { JsonObject error = new JsonObject(); error.addProperty("error", errorMsg); diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 53a3026424..038fdce130 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -397,6 +397,11 @@ message GetOrCreateDeviceFromGatewayResponseMsg { TransportApiRequestErrorCode error = 3; } +message GatewayDisconnectDeviceMsg { + string deviceName = 1; + int32 reasonCode = 2; +} + enum TransportApiRequestErrorCode { UNKNOWN_TRANSPORT_API_ERROR = 0; ENTITY_LIMIT = 1; @@ -419,6 +424,7 @@ message GetQueueRoutingInfoResponseMsg { string queueName = 5; string queueTopic = 6; int32 partitions = 7; + optional bool duplicateMsgToAllPartitions = 8; } message QueueUpdateMsg { @@ -429,6 +435,7 @@ message QueueUpdateMsg { string queueName = 5; string queueTopic = 6; int32 partitions = 7; + optional bool duplicateMsgToAllPartitions = 8; } message QueueDeleteMsg { @@ -577,8 +584,16 @@ message ResourceDeleteMsg { string resourceKey = 4; } +enum SessionCloseReason { + UNKNOWN_REASON = 0; + CREDENTIALS_UPDATED = 1; + MAX_CONCURRENT_SESSIONS_LIMIT_REACHED = 2; + SESSION_TIMEOUT = 3; +} + message SessionCloseNotificationProto { string message = 1; + SessionCloseReason reason = 2; } message SubscribeToAttributeUpdatesMsg { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/TbRuleEngineProducerService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/TbRuleEngineProducerService.java new file mode 100644 index 0000000000..49b40e3a6d --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/TbRuleEngineProducerService.java @@ -0,0 +1,74 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.common; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.discovery.PartitionService; + +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Slf4j +public class TbRuleEngineProducerService { + + private final PartitionService partitionService; + + public void sendToRuleEngine(TbQueueProducer> producer, + TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) { + List tpis = partitionService.resolveAll(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, tbMsg.getOriginator()); + if (tpis.size() > 1) { + UUID correlationId = UUID.randomUUID(); + for (int i = 0; i < tpis.size(); i++) { + TopicPartitionInfo tpi = tpis.get(i); + Integer partition = tpi.getPartition().orElse(null); + UUID id = i > 0 ? UUID.randomUUID() : tbMsg.getId(); + + tbMsg = tbMsg.toBuilder() + .id(id) + .correlationId(correlationId) + .partition(partition) + .build(); + sendToRuleEngine(producer, tpi, tenantId, tbMsg, i == tpis.size() - 1 ? callback : null); + } + } else { + sendToRuleEngine(producer, tpis.get(0), tenantId, tbMsg, callback); + } + } + + private void sendToRuleEngine(TbQueueProducer> producer, TopicPartitionInfo tpi, + TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) { + if (log.isTraceEnabled()) { + log.trace("[{}][{}] Pushing to topic {} message {}", tenantId, tbMsg.getOriginator(), tpi.getFullTopicName(), tbMsg); + } + ToRuleEngineMsg msg = ToRuleEngineMsg.newBuilder() + .setTbMsg(TbMsg.toByteString(tbMsg)) + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()).build(); + producer.send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), callback); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index e5ad2a3622..04d55f91c0 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.queue.discovery; import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; +import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; @@ -77,6 +78,7 @@ public class HashPartitionService implements PartitionService { private final ConcurrentMap partitionTopicsMap = new ConcurrentHashMap<>(); private final ConcurrentMap partitionSizesMap = new ConcurrentHashMap<>(); + private final ConcurrentMap queueConfigs = new ConcurrentHashMap<>(); private final ConcurrentMap tenantRoutingInfoMap = new ConcurrentHashMap<>(); @@ -132,6 +134,7 @@ public class HashPartitionService implements PartitionService { QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue); partitionTopicsMap.put(queueKey, queue.getQueueTopic()); partitionSizesMap.put(queueKey, queue.getPartitions()); + queueConfigs.put(queueKey, new QueueConfig(queue)); }); } @@ -174,10 +177,12 @@ public class HashPartitionService implements PartitionService { @Override public void updateQueues(List queueUpdateMsgs) { for (TransportProtos.QueueUpdateMsg queueUpdateMsg : queueUpdateMsgs) { - TenantId tenantId = TenantId.fromUUID(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); - QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueUpdateMsg.getQueueName(), tenantId); - partitionTopicsMap.put(queueKey, queueUpdateMsg.getQueueTopic()); - partitionSizesMap.put(queueKey, queueUpdateMsg.getPartitions()); + QueueRoutingInfo queueRoutingInfo = new QueueRoutingInfo(queueUpdateMsg); + TenantId tenantId = queueRoutingInfo.getTenantId(); + QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueRoutingInfo.getQueueName(), tenantId); + partitionTopicsMap.put(queueKey, queueRoutingInfo.getQueueTopic()); + partitionSizesMap.put(queueKey, queueRoutingInfo.getPartitions()); + queueConfigs.put(queueKey, new QueueConfig(queueRoutingInfo)); if (!tenantId.isSysTenantId()) { tenantRoutingInfoMap.remove(tenantId); } @@ -193,9 +198,7 @@ public class HashPartitionService implements PartitionService { }) .collect(Collectors.toList()); queueKeys.forEach(queueKey -> { - myPartitions.remove(queueKey); - partitionTopicsMap.remove(queueKey); - partitionSizesMap.remove(queueKey); + removeQueue(queueKey); evictTenantInfo(queueKey.getTenantId()); }); if (serviceInfoProvider.isService(ServiceType.TB_RULE_ENGINE)) { @@ -209,14 +212,17 @@ public class HashPartitionService implements PartitionService { List queueKeys = partitionSizesMap.keySet().stream() .filter(queueKey -> tenantId.equals(queueKey.getTenantId())) .collect(Collectors.toList()); - queueKeys.forEach(queueKey -> { - myPartitions.remove(queueKey); - partitionTopicsMap.remove(queueKey); - partitionSizesMap.remove(queueKey); - }); + queueKeys.forEach(this::removeQueue); evictTenantInfo(tenantId); } + private void removeQueue(QueueKey queueKey) { + myPartitions.remove(queueKey); + partitionTopicsMap.remove(queueKey); + partitionSizesMap.remove(queueKey); + queueConfigs.remove(queueKey); + } + @Override public boolean isManagedByCurrentService(TenantId tenantId) { if (serviceInfoProvider.isService(ServiceType.TB_CORE) || !serviceInfoProvider.isService(ServiceType.TB_RULE_ENGINE)) { @@ -254,6 +260,64 @@ public class HashPartitionService implements PartitionService { @Override public TopicPartitionInfo resolve(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId) { + QueueKey queueKey = getQueueKey(serviceType, queueName, tenantId); + return resolve(queueKey, entityId); + } + + @Override + public TopicPartitionInfo resolve(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId, Integer partition) { + QueueKey queueKey = getQueueKey(serviceType, queueName, tenantId); + if (partition != null) { + return buildTopicPartitionInfo(queueKey, partition); + } else { + return resolve(queueKey, entityId); + } + } + + @Override + public TopicPartitionInfo resolve(ServiceType serviceType, TenantId tenantId, EntityId entityId) { + return resolve(serviceType, null, tenantId, entityId); + } + + @Override + public List resolveAll(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId) { + QueueKey queueKey = getQueueKey(serviceType, queueName, tenantId); + TopicPartitionInfo tpi = resolve(queueKey, entityId); + if (serviceType != ServiceType.TB_RULE_ENGINE || tpi.getPartition().isEmpty()) { + return List.of(tpi); + } + + QueueConfig queueConfig = queueConfigs.get(queueKey); + if (queueConfig != null && queueConfig.isDuplicateMsgToAllPartitions()) { + int partition = tpi.getPartition().get(); + Integer partitionsCount = partitionSizesMap.get(queueKey); + + List partitions = new ArrayList<>(partitionsCount); + partitions.add(tpi); + for (int i = 0; i < partitionsCount; i++) { + if (i != partition) { + partitions.add(buildTopicPartitionInfo(queueKey, i, false)); + } + } + return partitions; + } else { + return Collections.singletonList(tpi); + } + } + + private TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId) { + Integer partitionSize = partitionSizesMap.get(queueKey); + if (partitionSize == null) { + throw new IllegalStateException("Partitions info for queue " + queueKey + " is missing"); + } + + int hash = hash(entityId.getId()); + int partition = Math.abs(hash % partitionSize); + + return buildTopicPartitionInfo(queueKey, partition); + } + + private QueueKey getQueueKey(ServiceType serviceType, String queueName, TenantId tenantId) { TenantId isolatedOrSystemTenantId = getIsolatedOrSystemTenantId(serviceType, tenantId); if (queueName == null) { queueName = MAIN_QUEUE_NAME; @@ -270,12 +334,7 @@ public class HashPartitionService implements PartitionService { log.warn("Using queue {} instead of isolated {} for tenant {}", queueKey, queueName, isolatedOrSystemTenantId); } } - return resolve(queueKey, entityId); - } - - @Override - public TopicPartitionInfo resolve(ServiceType serviceType, TenantId tenantId, EntityId entityId) { - return resolve(serviceType, null, tenantId, entityId); + return queueKey; } @Override @@ -288,18 +347,6 @@ public class HashPartitionService implements PartitionService { } } - private TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId) { - Integer partitionSize = partitionSizesMap.get(queueKey); - if (partitionSize == null) { - throw new IllegalStateException("Partitions info for queue " + queueKey + " is missing"); - } - - int hash = hash(entityId.getId()); - int partition = Math.abs(hash % partitionSize); - - return buildTopicPartitionInfo(queueKey, partition); - } - @Override public synchronized void recalculatePartitions(ServiceInfo currentService, List otherServices) { log.info("Recalculating partitions"); @@ -478,18 +525,17 @@ public class HashPartitionService implements PartitionService { } private TopicPartitionInfo buildTopicPartitionInfo(QueueKey queueKey, int partition) { - TopicPartitionInfo.TopicPartitionInfoBuilder tpi = TopicPartitionInfo.builder(); - tpi.topic(topicService.buildTopicName(partitionTopicsMap.get(queueKey))); - tpi.partition(partition); - tpi.tenantId(queueKey.getTenantId()); - List partitions = myPartitions.get(queueKey); - if (partitions != null) { - tpi.myPartition(partitions.contains(partition)); - } else { - tpi.myPartition(false); - } - return tpi.build(); + return buildTopicPartitionInfo(queueKey, partition, partitions != null && partitions.contains(partition)); + } + + private TopicPartitionInfo buildTopicPartitionInfo(QueueKey queueKey, int partition, boolean myPartition) { + return TopicPartitionInfo.builder() + .topic(topicService.buildTopicName(partitionTopicsMap.get(queueKey))) + .partition(partition) + .tenantId(queueKey.getTenantId()) + .myPartition(myPartition) + .build(); } private boolean isIsolated(ServiceType serviceType, TenantId tenantId) { @@ -619,4 +665,13 @@ public class HashPartitionService implements PartitionService { return serviceInfos.stream().map(ServiceInfo::getServiceId).collect(Collectors.toList()); } + @Data + public static class QueueConfig { + private boolean duplicateMsgToAllPartitions; + + public QueueConfig(QueueRoutingInfo queueRoutingInfo) { + this.duplicateMsgToAllPartitions = queueRoutingInfo.isDuplicateMsgToAllPartitions(); + } + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java index e14a6c3947..b5744981bd 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionService.java @@ -33,8 +33,12 @@ public interface PartitionService { TopicPartitionInfo resolve(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId); + TopicPartitionInfo resolve(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId, Integer partition); + TopicPartitionInfo resolve(ServiceType serviceType, TenantId tenantId, EntityId entityId); + List resolveAll(ServiceType serviceType, String queueName, TenantId tenantId, EntityId entityId); + boolean isMyPartition(ServiceType serviceType, TenantId tenantId, EntityId entityId); List getMyPartitions(QueueKey queueKey); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java index 51976d89f3..ba56044bb4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueRoutingInfo.java @@ -33,6 +33,7 @@ public class QueueRoutingInfo { private final String queueName; private final String queueTopic; private final int partitions; + private final boolean duplicateMsgToAllPartitions; public QueueRoutingInfo(Queue queue) { this.tenantId = queue.getTenantId(); @@ -40,21 +41,25 @@ public class QueueRoutingInfo { this.queueName = queue.getName(); this.queueTopic = queue.getTopic(); this.partitions = queue.getPartitions(); + this.duplicateMsgToAllPartitions = queue.isDuplicateMsgToAllPartitions(); } public QueueRoutingInfo(GetQueueRoutingInfoResponseMsg routingInfo) { - this.tenantId = new TenantId(new UUID(routingInfo.getTenantIdMSB(), routingInfo.getTenantIdLSB())); + this.tenantId = TenantId.fromUUID(new UUID(routingInfo.getTenantIdMSB(), routingInfo.getTenantIdLSB())); this.queueId = new QueueId(new UUID(routingInfo.getQueueIdMSB(), routingInfo.getQueueIdLSB())); this.queueName = routingInfo.getQueueName(); this.queueTopic = routingInfo.getQueueTopic(); this.partitions = routingInfo.getPartitions(); + this.duplicateMsgToAllPartitions = routingInfo.hasDuplicateMsgToAllPartitions() && routingInfo.getDuplicateMsgToAllPartitions(); } public QueueRoutingInfo(QueueUpdateMsg queueUpdateMsg) { - this.tenantId = new TenantId(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); + this.tenantId = TenantId.fromUUID(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); this.queueId = new QueueId(new UUID(queueUpdateMsg.getQueueIdMSB(), queueUpdateMsg.getQueueIdLSB())); this.queueName = queueUpdateMsg.getQueueName(); this.queueTopic = queueUpdateMsg.getQueueTopic(); this.partitions = queueUpdateMsg.getPartitions(); + this.duplicateMsgToAllPartitions = queueUpdateMsg.hasDuplicateMsgToAllPartitions() && queueUpdateMsg.getDuplicateMsgToAllPartitions(); } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java index 46084f8201..960be231c6 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java @@ -64,4 +64,17 @@ public class TopicService { public String buildTopicName(String topic) { return prefix.isBlank() ? topic : prefix + "." + topic; } -} \ No newline at end of file + + public String buildConsumerGroupId(String servicePrefix, TenantId tenantId, String queueName, Integer partitionId) { + return this.buildTopicName( + servicePrefix + queueName + + (tenantId.isSysTenantId() ? "" : ("-isolated-" + tenantId)) + + "-consumer" + + suffix(partitionId)); + } + + String suffix(Integer partitionId) { + return partitionId == null ? "" : "-" + partitionId; + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java index a5a0427606..0cb1194167 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java @@ -15,9 +15,13 @@ */ package org.thingsboard.server.queue.kafka; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.admin.CreateTopicsResult; +import org.apache.kafka.clients.admin.ListConsumerGroupOffsetsResult; import org.apache.kafka.clients.admin.NewTopic; +import org.apache.kafka.clients.consumer.OffsetAndMetadata; +import org.apache.kafka.common.TopicPartition; import org.apache.kafka.common.errors.TopicExistsException; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.util.PropertyUtils; @@ -27,6 +31,8 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** * Created by ashvayka on 24.09.18. @@ -120,4 +126,53 @@ public class TbKafkaAdmin implements TbQueueAdmin { public void destroy() { } + /** + * Sync offsets from a fat group to a single-partition group + * Migration back from single-partition consumer to a fat group is not supported + * TODO: The best possible approach to synchronize the offsets is to do the synchronization as a part of the save Queue parameters with stop all consumers + * */ + public void syncOffsets(String fatGroupId, String newGroupId, Integer partitionId) { + try { + syncOffsetsUnsafe(fatGroupId, newGroupId, partitionId); + } catch (Exception e) { + log.warn("Failed to syncOffsets from {} to {} partitionId {}", fatGroupId, newGroupId, partitionId, e); + } + } + + void syncOffsetsUnsafe(String fatGroupId, String newGroupId, Integer partitionId) throws ExecutionException, InterruptedException, TimeoutException { + log.info("syncOffsets [{}][{}][{}]", fatGroupId, newGroupId, partitionId); + if (partitionId == null) { + return; + } + Map oldOffsets = + settings.getAdminClient().listConsumerGroupOffsets(fatGroupId).partitionsToOffsetAndMetadata().get(10, TimeUnit.SECONDS); + if (oldOffsets.isEmpty()) { + return; + } + + for (var consumerOffset : oldOffsets.entrySet()) { + var tp = consumerOffset.getKey(); + if (!tp.topic().endsWith("." + partitionId)) { + continue; + } + var om = consumerOffset.getValue(); + Map newOffsets = + settings.getAdminClient().listConsumerGroupOffsets(newGroupId).partitionsToOffsetAndMetadata().get(10, TimeUnit.SECONDS); + + var existingOffset = newOffsets.get(tp); + if (existingOffset == null) { + log.info("[{}] topic offset does not exists in the new node group {}, all found offsets {}", tp, newGroupId, newOffsets); + } else if (existingOffset.offset() >= om.offset()) { + log.info("[{}] topic offset {} >= than old node group offset {}", tp, existingOffset.offset(), om.offset()); + break; + } else { + log.info("[{}] SHOULD alter topic offset [{}] less than old node group offset [{}]", tp, existingOffset.offset(), om.offset()); + } + settings.getAdminClient().alterConsumerGroupOffsets(newGroupId, Map.of(tp, om)).all().get(10, TimeUnit.SECONDS); + log.info("[{}] altered new consumer groupId {}", tp, newGroupId); + break; + } + + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java index 56440cf4a5..29b8064f04 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java @@ -112,8 +112,8 @@ public class TbKafkaConsumerStatsService { } private boolean isStatsPrintRequired() { - boolean isMyRuleEnginePartition = partitionService.resolve(ServiceType.TB_RULE_ENGINE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isMyPartition(); - boolean isMyCorePartition = partitionService.resolve(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isMyPartition(); + boolean isMyRuleEnginePartition = partitionService.isMyPartition(ServiceType.TB_RULE_ENGINE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID); + boolean isMyCorePartition = partitionService.isMyPartition(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID); return log.isInfoEnabled() && (isMyRuleEnginePartition || isMyCorePartition); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index d8efad2182..9728642b8d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -75,7 +75,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi private final TbKafkaConsumerStatsService consumerStatsService; private final TbQueueAdmin coreAdmin; - private final TbQueueAdmin ruleEngineAdmin; + private final TbKafkaAdmin ruleEngineAdmin; private final TbQueueAdmin jsExecutorRequestAdmin; private final TbQueueAdmin jsExecutorResponseAdmin; private final TbQueueAdmin transportApiRequestAdmin; @@ -187,18 +187,29 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(Queue configuration) { + throw new UnsupportedOperationException("Rule engine consumer should use a partitionId"); + } + + @Override + public TbQueueConsumer> createToRuleEngineMsgConsumer(Queue configuration, Integer partitionId) { String queueName = configuration.getName(); + String groupId = topicService.buildConsumerGroupId("re-", configuration.getTenantId(), queueName, partitionId); + + ruleEngineAdmin.syncOffsets(topicService.buildConsumerGroupId("re-", configuration.getTenantId(), queueName, null), // the fat groupId + groupId, partitionId); + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.buildTopicName(configuration.getTopic())); consumerBuilder.clientId("re-" + queueName + "-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet()); - consumerBuilder.groupId(topicService.buildTopicName("re-" + queueName + (configuration.getTenantId().isSysTenantId() ? "" : ("-isolated-" + configuration.getTenantId())) + "-consumer")); + consumerBuilder.groupId(groupId); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(ruleEngineAdmin); consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); } + @Override public TbQueueConsumer> createToRuleEngineNotificationsMsgConsumer() { TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index 568380d7b5..31fa7efffb 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -68,7 +68,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueAdmin coreAdmin; - private final TbQueueAdmin ruleEngineAdmin; + private final TbKafkaAdmin ruleEngineAdmin; private final TbQueueAdmin jsExecutorRequestAdmin; private final TbQueueAdmin jsExecutorResponseAdmin; private final TbQueueAdmin notificationAdmin; @@ -164,12 +164,22 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(Queue configuration) { + throw new UnsupportedOperationException("Rule engine consumer should use a partitionId"); + } + + @Override + public TbQueueConsumer> createToRuleEngineMsgConsumer(Queue configuration, Integer partitionId) { String queueName = configuration.getName(); + String groupId = topicService.buildConsumerGroupId("re-", configuration.getTenantId(), queueName, partitionId); + + ruleEngineAdmin.syncOffsets(topicService.buildConsumerGroupId("re-", configuration.getTenantId(), queueName, null), // the fat groupId + groupId, partitionId); + TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.buildTopicName(configuration.getTopic())); consumerBuilder.clientId("re-" + queueName + "-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet()); - consumerBuilder.groupId(topicService.buildTopicName("re-" + queueName + (configuration.getTenantId().isSysTenantId() ? "" : ("-isolated-" + configuration.getTenantId())) + "-consumer")); + consumerBuilder.groupId(groupId); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(ruleEngineAdmin); consumerBuilder.statsService(consumerStatsService); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java index f23c7e47f3..e49074f92d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java @@ -77,13 +77,25 @@ public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory TbQueueProducer> createToOtaPackageStateServiceMsgProducer(); /** - * Used to consume messages by TB Core Service + * Used to consume messages by TB Rule Engine Service * * @return * @param configuration */ TbQueueConsumer> createToRuleEngineMsgConsumer(Queue configuration); + /** + * Used to consume messages by TB Rule Engine Service + * Intended usage for consumer per partition strategy + * + * @return TbQueueConsumer + * @param configuration + * @param partitionId as a suffix for consumer name + */ + default TbQueueConsumer> createToRuleEngineMsgConsumer(Queue configuration, Integer partitionId) { + return createToRuleEngineMsgConsumer(configuration); + } + /** * Used to consume high priority messages by TB Core Service * diff --git a/common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaAdminTest.java b/common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaAdminTest.java new file mode 100644 index 0000000000..2007747dc9 --- /dev/null +++ b/common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaAdminTest.java @@ -0,0 +1,148 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.kafka; + +import com.google.common.collect.MapDifference; +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.admin.AdminClientConfig; +import org.apache.kafka.clients.admin.ConsumerGroupListing; +import org.apache.kafka.clients.consumer.OffsetAndMetadata; +import org.apache.kafka.common.TopicPartition; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ExecutionException; + +@Slf4j +class TbKafkaAdminTest { + + Properties props; + AdminClient admin; + + @BeforeEach + void setUp() { + props = new Properties(); + props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); + admin = AdminClient.create(props); + } + + @AfterEach + void tearDown() { + admin.close(); + } + + @Disabled + @Test + void testListOffsets() throws ExecutionException, InterruptedException { + log.info("Getting consumer groups list..."); + Collection consumerGroupListings = admin.listConsumerGroups().all().get(); + consumerGroupListings = consumerGroupListings.stream().sorted(Comparator.comparing(ConsumerGroupListing::groupId)).toList(); + for (ConsumerGroupListing consumerGroup : consumerGroupListings) { + String groupId = consumerGroup.groupId(); + log.info("=== consumer group: {}", groupId); + Map consumerOffsets = admin.listConsumerGroupOffsets(groupId) + .partitionsToOffsetAndMetadata().get(); + + // Printing the fetched offsets + consumerOffsets.forEach((tp, om) -> log.info(tp.topic() + " partition " + tp.partition() + " offset " + om.offset())); + if (groupId.startsWith("re-") && groupId.endsWith("consumer")) { + log.info("****** Migrating groupId [{}] ...", groupId); + + for (var consumerOffset : consumerOffsets.entrySet()) { + var tp = consumerOffset.getKey(); + var om = consumerOffset.getValue(); + final Integer tbPartitionId = parsePartitionIdFromTopicName(tp.topic()); + if (tbPartitionId == null || tbPartitionId < 0) { + continue; + } + + String newGroupId = groupId + "-" + tbPartitionId; + log.info("Getting offsets for consumer groupId [{}]", newGroupId); + Map newConsumerOffsets = admin.listConsumerGroupOffsets(newGroupId) + .partitionsToOffsetAndMetadata().get(); + + if (!newConsumerOffsets.isEmpty()) { + log.info("Found existing new group ConsumerOffsets {}", newConsumerOffsets); + } + + var existingOffset = newConsumerOffsets.get(tp); + if (existingOffset == null) { + log.info("topic offset does not exists in the new node group, all found offsets"); + } else if (existingOffset.offset() >= om.offset()) { + log.info("topic offset {} >= than old node group offset {}", existingOffset.offset(), om.offset()); + continue; + } else { + log.info("SHOULD alter topic offset [{}] less than old node group offset [{}]", existingOffset.offset(), om.offset()); + } + + Map newOffsets = Map.of(tp, om); + + log.warn("@@@@@ alterConsumerGroupOffsets [{}] with new offsets [{}]", newGroupId, newOffsets); + admin.alterConsumerGroupOffsets(newGroupId, newOffsets).all().whenComplete((res, err) -> { + if (err != null) { + log.error("Failed to alterConsumerGroupOffsets for groupId [{}], new offsets [{}]", newGroupId, newOffsets, err); + } else { + log.info("Updated new consumer group [{}], offsets [{}]", newGroupId, newOffsets); + } + }).get(); // Handle asynchronously as appropriate + + //Verify + + Map resultedConsumerOffsets = admin.listConsumerGroupOffsets(newGroupId) + .partitionsToOffsetAndMetadata().get(); + + MapDifference diff = Maps.difference(newOffsets, resultedConsumerOffsets); + + if (!diff.areEqual()) { + log.error("Verify failed for groupId [{}], current offset {} is not the same as expected {}", newGroupId, resultedConsumerOffsets, newOffsets); + } else { + log.info("Verify passed for groupId [{}]", newGroupId); + } + + } + + } + } + + } + + Integer parsePartitionIdFromTopicName(String topic) { + if (topic == null) { + return null; + } + int dotIndex = topic.lastIndexOf('.'); + if (dotIndex <= 0) { + return null; + } + + String indexStr = topic.substring(dotIndex + 1); + try { + return Integer.parseInt(indexStr); + } catch (Throwable t) { + log.warn("Can't parse partition Id from topic name [{}]", topic, t); + } + return null; + } + +} diff --git a/common/queue/src/test/java/org/thingsboard/server/queue/util/PropertyUtilsTest.java b/common/queue/src/test/java/org/thingsboard/server/queue/util/PropertyUtilsTest.java index ae5bbc9192..79e41aa986 100644 --- a/common/queue/src/test/java/org/thingsboard/server/queue/util/PropertyUtilsTest.java +++ b/common/queue/src/test/java/org/thingsboard/server/queue/util/PropertyUtilsTest.java @@ -49,10 +49,10 @@ class PropertyUtilsTest { @Test void givenKafkaTopicProperties_whenGetConfig_thenReturnMappedValues() { - assertThat(PropertyUtils.getProps("retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1")) + assertThat(PropertyUtils.getProps("retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1")) .isEqualTo(Map.of( "retention.ms", "604800000", - "segment.bytes", "26214400", + "segment.bytes", "52428800", "retention.bytes", "1048576000", "partitions", "1", "min.insync.replicas", "1" diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java index 1b9f111ac1..2190380065 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java @@ -19,8 +19,8 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; +import org.thingsboard.server.coapserver.TbCoapTransportComponent; import org.thingsboard.server.common.transport.TransportContext; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.coap.adaptors.JsonCoapAdaptor; @@ -36,7 +36,7 @@ import java.util.concurrent.ConcurrentMap; * Created by ashvayka on 18.10.18. */ @Slf4j -@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')") +@TbCoapTransportComponent @Component @Getter public class CoapTransportContext extends TransportContext { @@ -44,6 +44,22 @@ public class CoapTransportContext extends TransportContext { @Value("${transport.sessions.report_timeout}") private long sessionReportTimeout; + @Getter + @Value("${transport.coap.timeout}") + private Long timeout; + + @Getter + @Value("${transport.coap.piggyback_timeout}") + private Long piggybackTimeout; + + @Getter + @Value("${transport.coap.psm_activity_timer:10000}") + private long psmActivityTimer; + + @Getter + @Value("${transport.coap.paging_transmission_window:10000}") + private long pagingTransmissionWindow; + @Autowired private JsonCoapAdaptor jsonCoapAdaptor; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index ebc52fc034..67ae1b9ae7 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -75,8 +75,8 @@ public class CoapTransportResource extends AbstractCoapTransportResource { this.setObservable(true); // enable observing this.addObserver(new CoapResourceObserver()); this.dtlsSessionsMap = coapServerService.getDtlsSessionsMap(); - this.timeout = coapServerService.getTimeout(); - this.piggybackTimeout = coapServerService.getPiggybackTimeout(); + this.timeout = ctx.getTimeout(); + this.piggybackTimeout = ctx.getPiggybackTimeout(); this.clients = ctx.getClientContext(); long sessionReportTimeout = ctx.getSessionReportTimeout(); ctx.getScheduler().scheduleAtFixedRate(clients::reportActivity, new Random().nextInt((int) sessionReportTimeout), sessionReportTimeout, TimeUnit.MILLISECONDS); diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java index ada19e2ccb..07e7c1b4b4 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java @@ -15,24 +15,24 @@ */ package org.thingsboard.server.transport.coap; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.CoapServer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.coapserver.CoapServerService; -import org.thingsboard.server.coapserver.TbCoapServerComponent; +import org.thingsboard.server.coapserver.TbCoapTransportComponent; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.TbTransportService; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.transport.coap.efento.CoapEfentoTransportResource; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import java.net.UnknownHostException; @Service("CoapTransportService") -@TbCoapServerComponent +@TbCoapTransportComponent @Slf4j public class CoapTransportService implements TbTransportService { diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java index 8dafc18a88..6f117dbf29 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java @@ -21,11 +21,11 @@ import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Response; import org.eclipse.californium.core.observe.ObserveRelation; import org.eclipse.californium.core.server.resources.CoapExchange; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.context.annotation.Lazy; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.thingsboard.server.coapserver.CoapServerContext; +import org.thingsboard.server.coapserver.TbCoapTransportComponent; import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; @@ -81,7 +81,7 @@ import static org.eclipse.californium.core.coap.Message.NONE; @Slf4j @Service -@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')") +@TbCoapTransportComponent public class DefaultCoapClientContext implements CoapClientContext { private final CoapServerContext config; @@ -266,7 +266,7 @@ public class DefaultCoapClientContext implements CoapClientContext { } if (psmActivityTimer == null || psmActivityTimer == 0L) { - psmActivityTimer = config.getPsmActivityTimer(); + psmActivityTimer = transportContext.getPsmActivityTimer(); } timeout = psmActivityTimer; @@ -277,7 +277,7 @@ public class DefaultCoapClientContext implements CoapClientContext { } if (pagingTransmissionWindow == null || pagingTransmissionWindow == 0L) { - pagingTransmissionWindow = config.getPagingTransmissionWindow(); + pagingTransmissionWindow = transportContext.getPagingTransmissionWindow(); } timeout = pagingTransmissionWindow; } @@ -757,7 +757,7 @@ public class DefaultCoapClientContext implements CoapClientContext { } if (psmActivityTimer == null || psmActivityTimer == 0L) { - psmActivityTimer = config.getPsmActivityTimer(); + psmActivityTimer = transportContext.getPsmActivityTimer(); } return timeSinceLastUplink <= psmActivityTimer; } else { @@ -767,7 +767,7 @@ public class DefaultCoapClientContext implements CoapClientContext { } if (pagingTransmissionWindow == null || pagingTransmissionWindow == 0L) { - pagingTransmissionWindow = config.getPagingTransmissionWindow(); + pagingTransmissionWindow = transportContext.getPagingTransmissionWindow(); } boolean allowed = timeSinceLastUplink <= pagingTransmissionWindow; if (!allowed) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index 141aa83b7e..5672ae33c3 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -61,7 +61,6 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; -import java.util.stream.Stream; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LWM2M_OBJECT_VERSION_DEFAULT; @@ -417,14 +416,14 @@ public class LwM2mClient { private static Set clientSupportContentFormat(Registration registration) { Set contentFormats = new HashSet<>(); - contentFormats.add(ContentFormat.DEFAULT); Attribute ct = Arrays.stream(registration.getObjectLinks()) .filter(link -> link.getUriReference().equals("/")) .findFirst() .map(link -> link.getAttributes().get("ct")).orElse(null); - if (ct != null) { - Set codes = Stream.of(ct.getValue()).collect(Collectors.toSet()); - contentFormats.addAll(codes); + if (ct != null && ct.getValue() instanceof Collection) { + contentFormats.addAll((Collection) ct.getValue()); + } else { + contentFormats.add(ContentFormat.DEFAULT); } return contentFormats; } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java index af127dba88..77e427fc77 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java @@ -98,7 +98,6 @@ import java.util.Set; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -742,16 +741,18 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im private static ContentFormat getRequestContentFormat(LwM2mClient client, String versionedId, LwM2mModelProvider modelProvider) { LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(versionedId)); - if (pathIds.isResource() || pathIds.isResourceInstance()) { + if (pathIds.isResourceInstance() || pathIds.isResource()) { ResourceModel resourceModel = client.getResourceModel(versionedId, modelProvider); if (resourceModel != null && (pathIds.isResourceInstance() || (pathIds.isResource() && !resourceModel.multiple))) { + ContentFormat[] desiredFormats; if (OBJLNK.equals(resourceModel.type)) { - return ContentFormat.LINK; + desiredFormats = new ContentFormat[]{ContentFormat.LINK, ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON}; } else if (OPAQUE.equals(resourceModel.type)) { - return ContentFormat.OPAQUE; - } else { - return findFirstContentFormatForComp(client.getClientSupportContentFormats(), client.getDefaultContentFormat(), ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON); - } + desiredFormats = new ContentFormat[]{ContentFormat.OPAQUE, ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON}; + } else { + desiredFormats = new ContentFormat[]{ContentFormat.CBOR, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON}; + } + return findFirstContentFormatForComp(client.getClientSupportContentFormats(), client.getDefaultContentFormat(), desiredFormats); } else { return getContentFormatForComplex(client); } @@ -762,9 +763,9 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im private static ContentFormat getContentFormatForComplex(LwM2mClient client) { if (LwM2m.LwM2mVersion.V1_0.equals(client.getRegistration().getLwM2mVersion())) { - return ContentFormat.TLV; + return client.getDefaultContentFormat(); } else if (LwM2m.LwM2mVersion.V1_1.equals(client.getRegistration().getLwM2mVersion())) { - ContentFormat result = findFirstContentFormatForComp(client.getClientSupportContentFormats(), null, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON, ContentFormat.TLV, ContentFormat.JSON); + ContentFormat result = findFirstContentFormatForComp(client.getClientSupportContentFormats(), client.getDefaultContentFormat(), ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON, ContentFormat.TLV, ContentFormat.JSON); if (result != null) { return result; } else { @@ -783,37 +784,21 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im } } - private ContentFormat findFirstContentFormatForComposite (Set clientSupportContentFormats) { - ContentFormat contentFormat = findFirstContentFormatForComp(clientSupportContentFormats, null, ContentFormat.SENML_JSON, ContentFormat.SENML_CBOR); + private ContentFormat findFirstContentFormatForComposite (Set clientSupportContentFormats) { + ContentFormat contentFormat = findFirstContentFormatForComp(clientSupportContentFormats, null, ContentFormat.SENML_CBOR, ContentFormat.SENML_JSON); if (contentFormat != null) { return contentFormat; } else { throw new RuntimeException("This device does not support Composite Operation"); } } - private static ContentFormat findFirstContentFormatForComp(Set clientSupportContentFormats, ContentFormat defaultValue, ContentFormat... desiredFormats) { - AtomicReference compositeContentFormat = new AtomicReference<>(); - clientSupportContentFormats.forEach(c -> { - if (c instanceof Collection) { - for (ContentFormat contentFormat : desiredFormats) { - if (((Collection) c).contains(contentFormat)) { - compositeContentFormat.set(contentFormat); - break; - } - } - } else if (compositeContentFormat.get() == null && c instanceof ContentFormat) { - compositeContentFormat.set(Arrays.stream(desiredFormats).filter(f -> f.equals(c)).findFirst().get()); - } - }); - return compositeContentFormat.get() != null ? compositeContentFormat.get() : defaultValue; - } - - private static boolean containsObserveComposite(List l1, List l2) { - for (T elem : l1) { - if (l2.contains(elem)) { - return true; + private static ContentFormat findFirstContentFormatForComp(Set clientSupportContentFormats, ContentFormat defaultValue, ContentFormat... desiredFormats) { + List desiredFormatsList = Arrays.asList(desiredFormats); + for (ContentFormat c : clientSupportContentFormats) { + if (desiredFormatsList.contains(c)) { + return c; } } - return false; + return defaultValue; } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java index e035b2a861..9556180300 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java @@ -71,6 +71,10 @@ public class MqttTransportContext extends TransportContext { @Value("${transport.mqtt.timeout:10000}") private long timeout; + @Getter + @Value("${transport.mqtt.disconnect_timeout:1000}") + private long disconnectTimeout; + @Getter @Value("${transport.mqtt.proxy_enabled:false}") private boolean proxyEnabled; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index b48cb39ca0..eeeb287dc8 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -31,6 +31,7 @@ import io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader; import io.netty.handler.codec.mqtt.MqttPubAckMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttQoS; +import io.netty.handler.codec.mqtt.MqttReasonCodes; import io.netty.handler.codec.mqtt.MqttSubAckMessage; import io.netty.handler.codec.mqtt.MqttSubAckPayload; import io.netty.handler.codec.mqtt.MqttSubscribeMessage; @@ -68,7 +69,6 @@ import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; -import org.thingsboard.server.common.transport.service.DefaultTransportService; import org.thingsboard.server.common.transport.service.SessionMetaData; import org.thingsboard.server.common.transport.util.SslUtil; import org.thingsboard.server.gen.transport.TransportProtos; @@ -82,7 +82,6 @@ import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx; import org.thingsboard.server.transport.mqtt.session.GatewaySessionHandler; import org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher; import org.thingsboard.server.transport.mqtt.session.SparkplugNodeSessionHandler; -import org.thingsboard.server.transport.mqtt.util.ReturnCode; import org.thingsboard.server.transport.mqtt.util.ReturnCodeResolver; import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugRpcRequestHeader; @@ -196,23 +195,53 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement processMqttMsg(ctx, message); } else { log.error("[{}] Message decoding failed: {}", sessionId, message.decoderResult().cause().getMessage()); - closeCtx(ctx); + closeCtx(ctx, MqttReasonCodes.Disconnect.MALFORMED_PACKET); } } else { log.debug("[{}] Received non mqtt message: {}", sessionId, msg.getClass().getSimpleName()); - closeCtx(ctx); + closeCtx(ctx, (MqttMessage) null); } } finally { ReferenceCountUtil.safeRelease(msg); } } - private void closeCtx(ChannelHandlerContext ctx) { + private void closeCtx(ChannelHandlerContext ctx, MqttReasonCodes.Disconnect returnCode) { + closeCtx(ctx, returnCode.byteValue()); + } + + private void closeCtx(ChannelHandlerContext ctx, MqttConnectReturnCode returnCode) { + closeCtx(ctx, ReturnCodeResolver.getConnectionReturnCode(deviceSessionCtx.getMqttVersion(), returnCode).byteValue()); + } + + private void closeCtx(ChannelHandlerContext ctx, byte returnCode) { + closeCtx(ctx, createMqttDisconnectMsg(deviceSessionCtx, returnCode)); + } + + private void closeCtx(ChannelHandlerContext ctx, MqttMessage msg) { if (!rpcAwaitingAck.isEmpty()) { log.debug("[{}] Cleanup RPC awaiting ack map due to session close!", sessionId); rpcAwaitingAck.clear(); } - ctx.close(); + + if (ctx.channel() == null) { + log.debug("[{}] Channel is null, closing ctx...", sessionId); + ctx.close(); + } else if (ctx.channel().isOpen()) { + if (msg != null && MqttVersion.MQTT_5 == deviceSessionCtx.getMqttVersion()) { + ChannelFuture channelFuture = ctx.writeAndFlush(msg).addListener(future -> ctx.close()); + scheduler.schedule(() -> { + if (!channelFuture.isDone()) { + log.debug("[{}] Closing channel due to timeout!", sessionId); + ctx.close(); + } + }, context.getDisconnectTimeout(), TimeUnit.MILLISECONDS); + } else { + ctx.close(); + } + } else { + log.debug("[{}] Channel is already closed!", sessionId); + } } InetSocketAddress getAddress(ChannelHandlerContext ctx) { @@ -231,7 +260,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement void processMqttMsg(ChannelHandlerContext ctx, MqttMessage msg) { if (msg.fixedHeader() == null) { log.info("[{}:{}] Invalid message received", address.getHostName(), address.getPort()); - closeCtx(ctx); + closeCtx(ctx, MqttReasonCodes.Disconnect.PROTOCOL_ERROR); return; } deviceSessionCtx.setChannel(ctx); @@ -268,21 +297,23 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } else { log.debug("[{}] Unsupported topic for provisioning requests: {}!", sessionId, topicName); - closeCtx(ctx); + ack(ctx, msgId, MqttReasonCodes.PubAck.TOPIC_NAME_INVALID); + closeCtx(ctx, MqttReasonCodes.Disconnect.TOPIC_NAME_INVALID); } } catch (RuntimeException e) { log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); - closeCtx(ctx); + ack(ctx, msgId, MqttReasonCodes.PubAck.IMPLEMENTATION_SPECIFIC_ERROR); + closeCtx(ctx, MqttReasonCodes.Disconnect.IMPLEMENTATION_SPECIFIC_ERROR); } catch (AdaptorException e) { log.debug("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); - closeCtx(ctx); + sendResponseForAdaptorErrorOrCloseContext(ctx, topicName, msgId); } break; case PINGREQ: ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0))); break; case DISCONNECT: - closeCtx(ctx); + closeCtx(ctx, MqttReasonCodes.Disconnect.NORMAL_DISCONNECT); break; } } @@ -292,7 +323,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement if (queueSize >= context.getMessageQueueSizePerDeviceLimit()) { log.info("Closing current session because msq queue size for device {} exceed limit {} with msgQueueSize counter {} and actual queue size {}", deviceSessionCtx.getDeviceId(), context.getMessageQueueSizePerDeviceLimit(), queueSize, deviceSessionCtx.getMsgQueueSize()); - closeCtx(ctx); + closeCtx(ctx, MqttReasonCodes.Disconnect.QUOTA_EXCEEDED); return; } @@ -329,7 +360,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } break; case DISCONNECT: - closeCtx(ctx); + closeCtx(ctx, MqttReasonCodes.Disconnect.NORMAL_DISCONNECT); break; case PUBACK: int msgId = ((MqttPubAckMessage) msg).variableHeader().messageId(); @@ -389,15 +420,15 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement gatewaySessionHandler.onDeviceDisconnect(mqttMsg); break; default: - ack(ctx, msgId, ReturnCode.TOPIC_NAME_INVALID); + ack(ctx, msgId, MqttReasonCodes.PubAck.TOPIC_NAME_INVALID); } } catch (RuntimeException e) { log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); - ack(ctx, msgId, ReturnCode.IMPLEMENTATION_SPECIFIC); - closeCtx(ctx); + ack(ctx, msgId, MqttReasonCodes.PubAck.IMPLEMENTATION_SPECIFIC_ERROR); + closeCtx(ctx, MqttReasonCodes.Disconnect.IMPLEMENTATION_SPECIFIC_ERROR); } catch (AdaptorException e) { log.debug("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); - sendAckOrCloseSession(ctx, topicName, msgId); + sendResponseForAdaptorErrorOrCloseContext(ctx, topicName, msgId); } } @@ -433,11 +464,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } catch (RuntimeException e) { log.error("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); - ack(ctx, msgId, ReturnCode.IMPLEMENTATION_SPECIFIC); - closeCtx(ctx); + ack(ctx, msgId, MqttReasonCodes.PubAck.IMPLEMENTATION_SPECIFIC_ERROR); + closeCtx(ctx, MqttReasonCodes.Disconnect.IMPLEMENTATION_SPECIFIC_ERROR); } catch (AdaptorException | ThingsboardException | InvalidProtocolBufferException e) { log.error("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); - sendAckOrCloseSession(ctx, topicName, msgId); + sendResponseForAdaptorErrorOrCloseContext(ctx, topicName, msgId); } } @@ -530,11 +561,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement attrReqTopicType = TopicType.V2; } else { transportService.recordActivity(deviceSessionCtx.getSessionInfo()); - ack(ctx, msgId, ReturnCode.TOPIC_NAME_INVALID); + ack(ctx, msgId, MqttReasonCodes.PubAck.TOPIC_NAME_INVALID); } } catch (AdaptorException e) { log.debug("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); - sendAckOrCloseSession(ctx, topicName, msgId); + sendResponseForAdaptorErrorOrCloseContext(ctx, topicName, msgId); } } @@ -548,13 +579,13 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } - private void sendAckOrCloseSession(ChannelHandlerContext ctx, String topicName, int msgId) { + private void sendResponseForAdaptorErrorOrCloseContext(ChannelHandlerContext ctx, String topicName, int msgId) { if ((deviceSessionCtx.isSendAckOnValidationException() || MqttVersion.MQTT_5.equals(deviceSessionCtx.getMqttVersion())) && msgId > 0) { log.debug("[{}] Send pub ack on invalid publish msg [{}][{}]", sessionId, topicName, msgId); - ctx.writeAndFlush(createMqttPubAckMsg(deviceSessionCtx, msgId, ReturnCode.PAYLOAD_FORMAT_INVALID)); + ctx.writeAndFlush(createMqttPubAckMsg(deviceSessionCtx, msgId, MqttReasonCodes.PubAck.PAYLOAD_FORMAT_INVALID.byteValue())); } else { log.info("[{}] Closing current session due to invalid publish msg [{}][{}]", sessionId, topicName, msgId); - closeCtx(ctx); + closeCtx(ctx, MqttReasonCodes.Disconnect.PAYLOAD_FORMAT_INVALID); } } @@ -593,7 +624,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } - private void ack(ChannelHandlerContext ctx, int msgId, ReturnCode returnCode) { + private void ack(ChannelHandlerContext ctx, int msgId, MqttReasonCodes.PubAck returnCode) { + ack(ctx, msgId, returnCode.byteValue()); + } + + private void ack(ChannelHandlerContext ctx, int msgId, byte returnCode) { if (msgId > 0) { ctx.writeAndFlush(createMqttPubAckMsg(deviceSessionCtx, msgId, returnCode)); } @@ -604,13 +639,13 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onSuccess(Void dummy) { log.trace("[{}] Published msg: {}", sessionId, msg); - ack(ctx, msgId, ReturnCode.SUCCESS); + ack(ctx, msgId, MqttReasonCodes.PubAck.SUCCESS); } @Override public void onError(Throwable e) { log.trace("[{}] Failed to publish msg: {}", sessionId, msg, e); - closeCtx(ctx); + closeCtx(ctx, MqttReasonCodes.Disconnect.IMPLEMENTATION_SPECIFIC_ERROR); } }; } @@ -629,7 +664,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onSuccess(TransportProtos.ProvisionDeviceResponseMsg provisionResponseMsg) { log.trace("[{}] Published msg: {}", sessionId, msg); - ack(ctx, msgId, ReturnCode.SUCCESS); + ack(ctx, msgId, MqttReasonCodes.PubAck.SUCCESS); try { if (deviceSessionCtx.getProvisionPayloadType().equals(TransportPayloadType.JSON)) { deviceSessionCtx.getContext().getJsonMqttAdaptor().convertToPublish(deviceSessionCtx, provisionResponseMsg).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); @@ -645,8 +680,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onError(Throwable e) { log.trace("[{}] Failed to publish msg: {}", sessionId, msg, e); - ack(ctx, msgId, ReturnCode.IMPLEMENTATION_SPECIFIC); - closeCtx(ctx); + ack(ctx, msgId, MqttReasonCodes.PubAck.IMPLEMENTATION_SPECIFIC_ERROR); + closeCtx(ctx, MqttReasonCodes.Disconnect.IMPLEMENTATION_SPECIFIC_ERROR); } } @@ -681,13 +716,13 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onError(Throwable e) { log.trace("[{}] Failed to get firmware: {}", sessionId, msg, e); - closeCtx(ctx); + closeCtx(ctx, MqttReasonCodes.Disconnect.IMPLEMENTATION_SPECIFIC_ERROR); } } private void sendOtaPackage(ChannelHandlerContext ctx, int msgId, String firmwareId, String requestId, int chunkSize, int chunk, OtaPackageType type) { log.trace("[{}] Send firmware [{}] to device!", sessionId, firmwareId); - ack(ctx, msgId, ReturnCode.SUCCESS); + ack(ctx, msgId, MqttReasonCodes.PubAck.SUCCESS); try { byte[] firmwareChunk = context.getOtaPackageDataCache().get(firmwareId, chunkSize, chunk); deviceSessionCtx.getPayloadAdaptor() @@ -703,13 +738,12 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement deviceSessionCtx.getChannel().writeAndFlush(deviceSessionCtx .getPayloadAdaptor() .createMqttPublishMsg(deviceSessionCtx, MqttTopics.DEVICE_FIRMWARE_ERROR_TOPIC, error.getBytes())); - closeCtx(ctx); + closeCtx(ctx, MqttReasonCodes.Disconnect.IMPLEMENTATION_SPECIFIC_ERROR); } private void processSubscribe(ChannelHandlerContext ctx, MqttSubscribeMessage mqttMsg) { if (!checkConnected(ctx, mqttMsg)) { - int returnCode = ReturnCodeResolver.getSubscriptionReturnCode(deviceSessionCtx.getMqttVersion(), ReturnCode.NOT_AUTHORIZED_5); - ctx.writeAndFlush(createSubAckMessage(mqttMsg.variableHeader().messageId(), Collections.singletonList(returnCode))); + ctx.writeAndFlush(createSubAckMessage(mqttMsg.variableHeader().messageId(), Collections.singletonList(MqttReasonCodes.SubAck.NOT_AUTHORIZED.byteValue() & 0xFF))); return; } log.trace("[{}] Processing subscription [{}]!", sessionId, mqttMsg.variableHeader().messageId()); @@ -718,7 +752,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement for (MqttTopicSubscription subscription : mqttMsg.payload().topicSubscriptions()) { String topic = subscription.topicName(); MqttQoS reqQoS = subscription.qualityOfService(); - if (deviceSessionCtx.isDeviceSubscriptionAttributesTopic(topic)){ + if (deviceSessionCtx.isDeviceSubscriptionAttributesTopic(topic)) { processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V1); activityReported = true; continue; @@ -789,13 +823,13 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement break; default: log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topic, reqQoS); - grantedQoSList.add(ReturnCodeResolver.getSubscriptionReturnCode(deviceSessionCtx.getMqttVersion(), ReturnCode.TOPIC_FILTER_INVALID)); + grantedQoSList.add(ReturnCodeResolver.getSubscriptionReturnCode(deviceSessionCtx.getMqttVersion(), MqttReasonCodes.SubAck.TOPIC_FILTER_INVALID)); break; } } } catch (Exception e) { log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topic, reqQoS, e); - grantedQoSList.add(ReturnCodeResolver.getSubscriptionReturnCode(deviceSessionCtx.getMqttVersion(), ReturnCode.IMPLEMENTATION_SPECIFIC)); + grantedQoSList.add(ReturnCodeResolver.getSubscriptionReturnCode(deviceSessionCtx.getMqttVersion(), MqttReasonCodes.SubAck.IMPLEMENTATION_SPECIFIC_ERROR)); } } if (!activityReported) { @@ -832,7 +866,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private void processUnsubscribe(ChannelHandlerContext ctx, MqttUnsubscribeMessage mqttMsg) { if (!checkConnected(ctx, mqttMsg)) { - ctx.writeAndFlush(createUnSubAckMessage(mqttMsg.variableHeader().messageId(), Collections.singletonList(ReturnCode.NOT_AUTHORIZED_5.shortValue()))); + ctx.writeAndFlush(createUnSubAckMessage(mqttMsg.variableHeader().messageId(), + Collections.singletonList((short) MqttReasonCodes.UnsubAck.NOT_AUTHORIZED.byteValue()))); return; } boolean activityReported = false; @@ -843,7 +878,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement if (mqttQoSMap.containsKey(matcher)) { mqttQoSMap.remove(matcher); try { - short resultValue = ReturnCode.SUCCESS.shortValue(); + short resultValue = MqttReasonCodes.UnsubAck.SUCCESS.byteValue(); switch (topicName) { case MqttTopics.DEVICE_ATTRIBUTES_TOPIC: case MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC: @@ -884,16 +919,16 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } default: log.trace("[{}] Failed to process unsubscription [{}] to [{}]", sessionId, mqttMsg.variableHeader().messageId(), topicName); - resultValue = ReturnCode.TOPIC_FILTER_INVALID.shortValue(); + resultValue = MqttReasonCodes.UnsubAck.TOPIC_FILTER_INVALID.byteValue(); } unSubResults.add(resultValue); } catch (Exception e) { log.debug("[{}] Failed to process unsubscription [{}] to [{}]", sessionId, mqttMsg.variableHeader().messageId(), topicName); - unSubResults.add(ReturnCode.IMPLEMENTATION_SPECIFIC.shortValue()); + unSubResults.add((short) MqttReasonCodes.UnsubAck.IMPLEMENTATION_SPECIFIC_ERROR.byteValue()); } } else { log.debug("[{}] Failed to process unsubscription [{}] to [{}] - Subscription not found", sessionId, mqttMsg.variableHeader().messageId(), topicName); - unSubResults.add(ReturnCode.NO_SUBSCRIPTION_EXISTED.shortValue()); + unSubResults.add((short)MqttReasonCodes.UnsubAck.NO_SUBSCRIPTION_EXISTED.byteValue()); } } if (!activityReported) { @@ -918,7 +953,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement deviceSessionCtx.setMqttVersion(getMqttVersion(msg.variableHeader().version())); if (DataConstants.PROVISION.equals(userName) || DataConstants.PROVISION.equals(clientId)) { deviceSessionCtx.setProvisionOnly(true); - ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SUCCESS, msg)); + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_ACCEPTED, msg)); } else { X509Certificate cert; if (sslHandler != null && (cert = getX509Certificate()) != null) { @@ -952,8 +987,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onError(Throwable e) { log.trace("[{}] Failed to process credentials: {}", address, userName, e); - ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SERVER_UNAVAILABLE_5, connectMessage)); - closeCtx(ctx); + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE_5, connectMessage)); + closeCtx(ctx, MqttReasonCodes.Disconnect.SERVER_BUSY); } }); } @@ -975,15 +1010,15 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onError(Throwable e) { log.trace("[{}] Failed to process credentials: {}", address, sha3Hash, e); - ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SERVER_UNAVAILABLE_5, connectMessage)); - closeCtx(ctx); + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE_5, connectMessage)); + closeCtx(ctx, MqttReasonCodes.Disconnect.IMPLEMENTATION_SPECIFIC_ERROR); } }); } catch (Exception e) { context.onAuthFailure(address); - ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.NOT_AUTHORIZED_5, connectMessage)); + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED_5, connectMessage)); log.trace("[{}] X509 auth failure: {}", sessionId, address, e); - closeCtx(ctx); + closeCtx(ctx, MqttReasonCodes.Disconnect.NOT_AUTHORIZED); } } @@ -1000,7 +1035,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement return null; } - private MqttConnAckMessage createMqttConnAckMsg(ReturnCode returnCode, MqttConnectMessage msg) { + private MqttConnAckMessage createMqttConnAckMsg(MqttConnectReturnCode returnCode, MqttConnectMessage msg) { MqttMessageBuilders.ConnAckBuilder connAckBuilder = MqttMessageBuilders.connAck(); connAckBuilder.sessionPresent(!msg.variableHeader().isCleanSession()); MqttConnectReturnCode finalReturnCode = ReturnCodeResolver.getConnectionReturnCode(deviceSessionCtx.getMqttVersion(), returnCode); @@ -1031,18 +1066,18 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement log.error("[{}] Unexpected Exception", sessionId, cause); } - closeCtx(ctx); + closeCtx(ctx, MqttReasonCodes.Disconnect.SERVER_SHUTTING_DOWN); if (cause instanceof OutOfMemoryError) { log.error("Received critical error. Going to shutdown the service."); System.exit(1); } } - private static MqttSubAckMessage createSubAckMessage(Integer msgId, List grantedQoSList) { + private static MqttSubAckMessage createSubAckMessage(Integer msgId, List reasonCodes) { MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(SUBACK, false, AT_MOST_ONCE, false, 0); MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(msgId); - MqttSubAckPayload mqttSubAckPayload = new MqttSubAckPayload(grantedQoSList); + MqttSubAckPayload mqttSubAckPayload = new MqttSubAckPayload(reasonCodes); return new MqttSubAckMessage(mqttFixedHeader, mqttMessageIdVariableHeader, mqttSubAckPayload); } @@ -1061,14 +1096,22 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } - public static MqttMessage createMqttPubAckMsg(DeviceSessionCtx deviceSessionCtx, int requestId, ReturnCode returnCode) { + public static MqttMessage createMqttPubAckMsg(DeviceSessionCtx deviceSessionCtx, int requestId, byte returnCode) { MqttMessageBuilders.PubAckBuilder pubAckMsgBuilder = MqttMessageBuilders.pubAck().packetId(requestId); if (MqttVersion.MQTT_5.equals(deviceSessionCtx.getMqttVersion())) { - pubAckMsgBuilder.reasonCode(returnCode.byteValue()); + pubAckMsgBuilder.reasonCode(returnCode); } return pubAckMsgBuilder.build(); } + public static MqttMessage createMqttDisconnectMsg(DeviceSessionCtx deviceSessionCtx, byte returnCode) { + MqttMessageBuilders.DisconnectBuilder disconnectBuilder = MqttMessageBuilders.disconnect(); + if (MqttVersion.MQTT_5.equals(deviceSessionCtx.getMqttVersion())) { + disconnectBuilder.reasonCode(returnCode); + } + return disconnectBuilder.build(); + } + private boolean checkConnected(ChannelHandlerContext ctx, MqttMessage msg) { if (deviceSessionCtx.isConnected()) { return true; @@ -1115,8 +1158,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } catch (Exception e) { log.trace("[{}][{}] Failed to fetch sparkplugDevice connect, sparkplugTopicName", sessionId, deviceSessionCtx.getDeviceInfo().getDeviceName(), e); - ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SERVER_UNAVAILABLE_5, connectMessage)); - closeCtx(ctx); + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE_5, connectMessage)); + closeCtx(ctx, MqttReasonCodes.Disconnect.IMPLEMENTATION_SPECIFIC_ERROR); } } @@ -1160,20 +1203,20 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private void onValidateDeviceResponse(ValidateDeviceCredentialsResponse msg, ChannelHandlerContext ctx, MqttConnectMessage connectMessage) { if (!msg.hasDeviceInfo()) { context.onAuthFailure(address); - ReturnCode returnCode = ReturnCode.NOT_AUTHORIZED_5; + MqttConnectReturnCode returnCode = MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED_5; if (sslHandler == null || getX509Certificate() == null) { String username = connectMessage.payload().userName(); byte[] passwordBytes = connectMessage.payload().passwordInBytes(); String clientId = connectMessage.payload().clientIdentifier(); if ((username != null && passwordBytes != null && clientId != null) || (username == null ^ passwordBytes == null)) { - returnCode = ReturnCode.BAD_USERNAME_OR_PASSWORD; + returnCode = MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USERNAME_OR_PASSWORD; } else if (!StringUtils.isBlank(clientId)) { - returnCode = ReturnCode.CLIENT_IDENTIFIER_NOT_VALID; + returnCode = MqttConnectReturnCode.CONNECTION_REFUSED_CLIENT_IDENTIFIER_NOT_VALID; } } ctx.writeAndFlush(createMqttConnAckMsg(returnCode, connectMessage)); - closeCtx(ctx); + closeCtx(ctx, returnCode); } else { context.onAuthSuccess(address); deviceSessionCtx.setDeviceInfo(msg.getDeviceInfo()); @@ -1188,7 +1231,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } else { checkGatewaySession(sessionMetaData); } - ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SUCCESS, connectMessage)); + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_ACCEPTED, connectMessage)); deviceSessionCtx.setConnected(true); log.debug("[{}] Client connected!", sessionId); transportService.getCallbackExecutor().execute(() -> processMsgQueue(ctx)); //this callback will execute in Producer worker thread and hard or blocking work have to be submitted to the separate thread. @@ -1198,11 +1241,13 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void onError(Throwable e) { if (e instanceof TbRateLimitsException) { log.trace("[{}] Failed to submit session event: {}", sessionId, e.getMessage()); + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_CONNECTION_RATE_EXCEEDED, connectMessage)); + closeCtx(ctx, MqttReasonCodes.Disconnect.MESSAGE_RATE_TOO_HIGH); } else { log.warn("[{}] Failed to submit session event", sessionId, e); + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE_5, connectMessage)); + closeCtx(ctx, MqttReasonCodes.Disconnect.IMPLEMENTATION_SPECIFIC_ERROR); } - ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SERVER_UNAVAILABLE_5, connectMessage)); - closeCtx(ctx); } }); } @@ -1231,8 +1276,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement SparkplugTopic sparkplugTopic = new SparkplugTopic(sparkplugSessionHandler.getSparkplugTopicNode(), SparkplugMessageType.NCMD); sparkplugSessionHandler.createSparkplugMqttPublishMsg(tsKvProto, - sparkplugTopic.toString(), - sparkplugSessionHandler.getNodeBirthMetrics().get(tsKvProto.getKv().getKey())) + sparkplugTopic.toString(), + sparkplugSessionHandler.getNodeBirthMetrics().get(tsKvProto.getKv().getKey())) .ifPresent(sparkplugSessionHandler::writeAndFlush); } }); @@ -1250,7 +1295,19 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void onRemoteSessionCloseCommand(UUID sessionId, TransportProtos.SessionCloseNotificationProto sessionCloseNotification) { log.trace("[{}] Received the remote command to close the session: {}", sessionId, sessionCloseNotification.getMessage()); transportService.deregisterSession(deviceSessionCtx.getSessionInfo()); - closeCtx(deviceSessionCtx.getChannel()); + MqttReasonCodes.Disconnect returnCode = MqttReasonCodes.Disconnect.IMPLEMENTATION_SPECIFIC_ERROR; + switch (sessionCloseNotification.getReason()) { + case CREDENTIALS_UPDATED: + returnCode = MqttReasonCodes.Disconnect.ADMINISTRATIVE_ACTION; + break; + case MAX_CONCURRENT_SESSIONS_LIMIT_REACHED: + returnCode = MqttReasonCodes.Disconnect.SESSION_TAKEN_OVER; + break; + case SESSION_TIMEOUT: + returnCode = MqttReasonCodes.Disconnect.MAXIMUM_CONNECT_TIME; + break; + } + closeCtx(deviceSessionCtx.getChannel(), returnCode); } @Override @@ -1287,8 +1344,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement SparkplugTopic sparkplugTopic = new SparkplugTopic(sparkplugSessionHandler.getSparkplugTopicNode(), messageType); sparkplugSessionHandler.createSparkplugMqttPublishMsg(tsKvProto, - sparkplugTopic.toString(), - sparkplugSessionHandler.getNodeBirthMetrics().get(tsKvProto.getKv().getKey())) + sparkplugTopic.toString(), + sparkplugSessionHandler.getNodeBirthMetrics().get(tsKvProto.getKv().getKey())) .ifPresent(payload -> sendToDeviceRpcRequest(payload, rpcRequest, deviceSessionCtx.getSessionInfo())); } else { sendErrorRpcResponse(deviceSessionCtx.getSessionInfo(), rpcRequest.getRequestId(), @@ -1369,7 +1426,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void onDeviceDeleted(DeviceId deviceId) { context.onAuthFailure(address); ChannelHandlerContext ctx = deviceSessionCtx.getChannel(); - closeCtx(ctx); + closeCtx(ctx, MqttReasonCodes.Disconnect.ADMINISTRATIVE_ACTION); } public void sendErrorRpcResponse(TransportProtos.SessionInfoProto sessionInfo, int requestId, ThingsboardErrorCode result, String errorMsg) { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/BackwardCompatibilityAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/BackwardCompatibilityAdaptor.java index e85decf419..c4cfd13dba 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/BackwardCompatibilityAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/BackwardCompatibilityAdaptor.java @@ -20,8 +20,8 @@ import io.netty.handler.codec.mqtt.MqttPublishMessage; import lombok.AllArgsConstructor; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.adaptor.AdaptorException; +import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.mqtt.session.MqttDeviceAwareSessionContext; @@ -146,6 +146,12 @@ public class BackwardCompatibilityAdaptor implements MqttTransportAdaptor { return Optional.empty(); } + @Override + public Optional convertToGatewayDeviceDisconnectPublish(MqttDeviceAwareSessionContext ctx, String deviceName, int reasonCode) throws AdaptorException { + log.warn("[{}] invoked not implemented adaptor method! Device name: {} ReasonCode: {}", ctx.getSessionId(), deviceName, reasonCode); + return Optional.empty(); + } + @Override public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, byte[] firmwareChunk, String requestId, int chunk, OtaPackageType firmwareType) throws AdaptorException { return protoAdaptor.convertToPublish(ctx, firmwareChunk, requestId, chunk, firmwareType); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java index 90174616e6..8cd215f1f1 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java @@ -154,6 +154,11 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, JsonConverter.toJson(provisionResponse))); } + @Override + public Optional convertToGatewayDeviceDisconnectPublish(MqttDeviceAwareSessionContext ctx, String deviceName, int reasonCode) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_DISCONNECT_TOPIC, JsonConverter.toGatewayDeviceDisconnectJson(deviceName, reasonCode))); + } + @Override public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, byte[] firmwareChunk, String requestId, int chunk, OtaPackageType firmwareType) { return Optional.of(createMqttPublishMsg(ctx, String.format(DEVICE_SOFTWARE_FIRMWARE_RESPONSES_TOPIC_FORMAT, firmwareType.getKeyPrefix(), requestId, chunk), firmwareChunk)); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java index 34d80411e5..9cf15f9de1 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java @@ -80,6 +80,8 @@ public interface MqttTransportAdaptor { Optional convertToPublish(MqttDeviceAwareSessionContext ctx, byte[] firmwareChunk, String requestId, int chunk, OtaPackageType firmwareType) throws AdaptorException; + Optional convertToGatewayDeviceDisconnectPublish(MqttDeviceAwareSessionContext ctx, String deviceName, int reasonCode) throws AdaptorException; + default MqttPublishMessage createMqttPublishMsg(MqttDeviceAwareSessionContext ctx, String topic, byte[] payloadInBytes) { MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, false, ctx.getQoSForTopic(topic), false, 0); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java index 25113dedca..2fc75df026 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java @@ -173,6 +173,15 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, provisionResponse.toByteArray())); } + @Override + public Optional convertToGatewayDeviceDisconnectPublish(MqttDeviceAwareSessionContext ctx, String deviceName, int reasonCode) { + TransportProtos.GatewayDisconnectDeviceMsg gatewayDeviceDisconnectMsg = TransportProtos.GatewayDisconnectDeviceMsg.newBuilder() + .setDeviceName(deviceName) + .setReasonCode(reasonCode) + .build(); + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_DISCONNECT_TOPIC, gatewayDeviceDisconnectMsg.toByteArray())); + } + @Override public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, byte[] firmwareChunk, String requestId, int chunk, OtaPackageType firmwareType) throws AdaptorException { return Optional.of(createMqttPublishMsg(ctx, String.format(DEVICE_SOFTWARE_FIRMWARE_RESPONSES_TOPIC_FORMAT, firmwareType.getKeyPrefix(), requestId, chunk), firmwareChunk)); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java index d033774e93..3b3aaef983 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -15,9 +15,7 @@ */ package org.thingsboard.server.transport.mqtt.session; - import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; @@ -33,11 +31,15 @@ import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage; +import io.netty.handler.codec.mqtt.MqttReasonCodes; +import io.netty.handler.codec.mqtt.MqttVersion; import lombok.Getter; import lombok.Setter; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.util.CollectionUtils; import org.springframework.util.ConcurrentReferenceHashMap; +import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.adaptor.ProtoConverter; @@ -46,6 +48,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; @@ -59,10 +62,8 @@ import org.thingsboard.server.transport.mqtt.MqttTransportHandler; import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; -import org.thingsboard.server.transport.mqtt.util.ReturnCode; import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState; -import jakarta.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -76,6 +77,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE; @@ -95,10 +97,16 @@ public abstract class AbstractGatewaySessionHandler deviceCreationLockMap; private final ConcurrentMap devices; @@ -193,10 +201,6 @@ public abstract class AbstractGatewaySessionHandler() { - @Override - public void onSuccess(@Nullable T result) { - ack(msg, ReturnCode.SUCCESS); - log.trace("[{}][{}][{}] onDeviceConnectOk: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName); - } - - @Override - public void onFailure(Throwable t) { - logDeviceCreationError(t, deviceName); - } - }, context.getExecutor()); + process(onDeviceConnect(deviceName, deviceType), + result -> { + ack(msg, MqttReasonCodes.PubAck.SUCCESS); + log.trace("[{}][{}][{}] onDeviceConnectOk: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName); + }, + t -> logDeviceCreationError(t, deviceName)); } public void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional deviceProfileOpt) { @@ -363,39 +361,30 @@ public abstract class AbstractGatewaySessionHandler deviceEntry : jsonObj.entrySet()) { - String deviceName = deviceEntry.getKey(); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback<>() { - @Override - public void onSuccess(@Nullable T deviceCtx) { - if (!deviceEntry.getValue().isJsonArray()) { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } - try { - TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(deviceEntry.getValue().getAsJsonArray()); - processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); - } catch (Throwable e) { - log.warn("[{}][{}][{}] Failed to convert telemetry: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); - channel.close(); - } - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}][{}][{}] Failed to process device telemetry command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t); - } - }, context.getExecutor()); + validateJsonObject(json); + for (Map.Entry deviceEntry : json.getAsJsonObject().entrySet()) { + if (!deviceEntry.getValue().isJsonArray()) { + log.warn("{}[{}]", CAN_T_PARSE_VALUE, json); + continue; } - } else { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + String deviceName = deviceEntry.getKey(); + process(deviceName, deviceCtx -> processPostTelemetryMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId), + t -> failedToProcessLog(deviceName, TELEMETRY, t)); + } + } + + private void processPostTelemetryMsg(T deviceCtx, JsonElement msg, String deviceName, int msgId) { + try { + TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(msg.getAsJsonArray()); + transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg)); + } catch (Throwable e) { + log.warn("[{}][{}][{}] Failed to convert telemetry: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, msg, e); + ackOrClose(msgId); } } @@ -403,40 +392,29 @@ public abstract class AbstractGatewaySessionHandler deviceMsgList = telemetryMsgProto.getMsgList(); - if (!CollectionUtils.isEmpty(deviceMsgList)) { - deviceMsgList.forEach(telemetryMsg -> { - String deviceName = checkDeviceName(telemetryMsg.getDeviceName()); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback<>() { - @Override - public void onSuccess(@Nullable T deviceCtx) { - TransportProtos.PostTelemetryMsg msg = telemetryMsg.getMsg(); - try { - TransportProtos.PostTelemetryMsg postTelemetryMsg = ProtoConverter.validatePostTelemetryMsg(msg.toByteArray()); - processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); - } catch (Throwable e) { - log.warn("[{}][{}][{}] Failed to convert telemetry: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, msg, e); - channel.close(); - } - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}][{}][{}] Failed to process device telemetry command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t); - } - }, context.getExecutor()); - }); - } else { + if (CollectionUtils.isEmpty(deviceMsgList)) { log.debug("[{}][{}][{}] Devices telemetry messages is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId); throw new IllegalArgumentException("[" + sessionId + "] Devices telemetry messages is empty for [" + gateway.getDeviceId() + "]"); } + + deviceMsgList.forEach(telemetryMsg -> { + String deviceName = checkDeviceName(telemetryMsg.getDeviceName()); + process(deviceName, deviceCtx -> processPostTelemetryMsg(deviceCtx, telemetryMsg.getMsg(), deviceName, msgId), + t -> failedToProcessLog(deviceName, TELEMETRY, t)); + }); } catch (RuntimeException | InvalidProtocolBufferException e) { throw new AdaptorException(e); } } - public void processPostTelemetryMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostTelemetryMsg postTelemetryMsg, String deviceName, int msgId) { - transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg)); + protected void processPostTelemetryMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostTelemetryMsg msg, String deviceName, int msgId) { + try { + TransportProtos.PostTelemetryMsg postTelemetryMsg = ProtoConverter.validatePostTelemetryMsg(msg.toByteArray()); + transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg)); + } catch (Throwable e) { + log.warn("[{}][{}][{}] Failed to convert telemetry: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, msg, e); + ackOrClose(msgId); + } } public TransportProtos.PostTelemetryMsg postTelemetryMsgCreated(TransportProtos.KeyValueProto keyValueProto, long ts) { @@ -452,34 +430,27 @@ public abstract class AbstractGatewaySessionHandler deviceEntry : jsonObj.entrySet()) { - String deviceName = deviceEntry.getKey(); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback<>() { - @Override - public void onSuccess(@Nullable T deviceCtx) { - if (!deviceEntry.getValue().isJsonObject()) { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } - try { - DeviceId deviceId = deviceCtx.getDeviceId(); - TransportProtos.ClaimDeviceMsg claimDeviceMsg = JsonConverter.convertToClaimDeviceProto(deviceId, deviceEntry.getValue()); - processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); - } catch (Throwable e) { - log.warn("[{}][{}][{}] Failed to convert claim message: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); - } - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}][{}][{}] Failed to process device claiming command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t); - } - }, context.getExecutor()); + validateJsonObject(json); + for (Map.Entry deviceEntry : json.getAsJsonObject().entrySet()) { + if (!deviceEntry.getValue().isJsonObject()) { + log.warn("{}[{}]", CAN_T_PARSE_VALUE, json); + continue; } - } else { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + + String deviceName = deviceEntry.getKey(); + process(deviceName, deviceCtx -> processClaimDeviceMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId), + t -> failedToProcessLog(deviceName, CLAIMING, t)); + } + } + + private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, JsonElement claimRequest, String deviceName, int msgId) { + try { + DeviceId deviceId = deviceCtx.getDeviceId(); + TransportProtos.ClaimDeviceMsg claimDeviceMsg = JsonConverter.convertToClaimDeviceProto(deviceId, claimRequest); + transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); + } catch (Throwable e) { + log.warn("[{}][{}][{}] Failed to convert claim message: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, claimRequest, e); + ackOrClose(msgId); } } @@ -487,70 +458,54 @@ public abstract class AbstractGatewaySessionHandler claimMsgList = claimMsgProto.getMsgList(); - if (!CollectionUtils.isEmpty(claimMsgList)) { - claimMsgList.forEach(claimDeviceMsg -> { - String deviceName = checkDeviceName(claimDeviceMsg.getDeviceName()); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback<>() { - @Override - public void onSuccess(@Nullable T deviceCtx) { - TransportApiProtos.ClaimDevice claimRequest = claimDeviceMsg.getClaimRequest(); - if (claimRequest == null) { - throw new IllegalArgumentException("Claim request for device: " + deviceName + " is null!"); - } - try { - DeviceId deviceId = deviceCtx.getDeviceId(); - TransportProtos.ClaimDeviceMsg claimDeviceMsg = ProtoConverter.convertToClaimDeviceProto(deviceId, claimRequest.toByteArray()); - processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); - } catch (Throwable e) { - log.warn("[{}][{}][{}] Failed to convert claim message: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, claimRequest, e); - } - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}][{}][{}] Failed to process device claiming command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t); - } - }, context.getExecutor()); - }); - } else { + if (CollectionUtils.isEmpty(claimMsgList)) { log.debug("[{}][{}][{}] Devices claim messages is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId); throw new IllegalArgumentException("[" + sessionId + "] Devices claim messages is empty for [" + gateway.getDeviceId() + "]"); } + + claimMsgList.forEach(claimDeviceMsg -> { + String deviceName = checkDeviceName(claimDeviceMsg.getDeviceName()); + process(deviceName, deviceCtx -> processClaimDeviceMsg(deviceCtx, claimDeviceMsg.getClaimRequest(), deviceName, msgId), + t -> failedToProcessLog(deviceName, CLAIMING, t)); + }); } catch (RuntimeException | InvalidProtocolBufferException e) { throw new AdaptorException(e); } } - private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.ClaimDeviceMsg claimDeviceMsg, String deviceName, int msgId) { - transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); + private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, TransportApiProtos.ClaimDevice claimRequest, String deviceName, int msgId) { + try { + DeviceId deviceId = deviceCtx.getDeviceId(); + TransportProtos.ClaimDeviceMsg claimDeviceMsg = ProtoConverter.convertToClaimDeviceProto(deviceId, claimRequest.toByteArray()); + transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); + } catch (Throwable e) { + log.warn("[{}][{}][{}] Failed to convert claim message: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, claimRequest, e); + ackOrClose(msgId); + } } private void onDeviceAttributesJson(int msgId, ByteBuf payload) throws AdaptorException { JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); - if (json.isJsonObject()) { - JsonObject jsonObj = json.getAsJsonObject(); - for (Map.Entry deviceEntry : jsonObj.entrySet()) { - String deviceName = deviceEntry.getKey(); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback<>() { - @Override - public void onSuccess(@Nullable T deviceCtx) { - if (!deviceEntry.getValue().isJsonObject()) { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } - TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(deviceEntry.getValue().getAsJsonObject()); - processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}][{}][{}] Failed to process device attributes command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t); - } - }, context.getExecutor()); + validateJsonObject(json); + for (Map.Entry deviceEntry : json.getAsJsonObject().entrySet()) { + if (!deviceEntry.getValue().isJsonObject()) { + log.warn("{}[{}]", CAN_T_PARSE_VALUE, json); + continue; } - } else { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + + String deviceName = deviceEntry.getKey(); + process(deviceName, deviceCtx -> processPostAttributesMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId), + t -> failedToProcessLog(deviceName, ATTRIBUTE, t)); + } + } + + private void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, JsonElement msg, String deviceName, int msgId) { + try { + TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(msg.getAsJsonObject()); + transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); + } catch (Throwable e) { + log.warn("[{}][{}][{}] Failed to process device attributes command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, msg, e); + ackOrClose(msgId); } } @@ -558,66 +513,50 @@ public abstract class AbstractGatewaySessionHandler attributesMsgList = attributesMsgProto.getMsgList(); - if (!CollectionUtils.isEmpty(attributesMsgList)) { - attributesMsgList.forEach(attributesMsg -> { - String deviceName = checkDeviceName(attributesMsg.getDeviceName()); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback<>() { - @Override - public void onSuccess(@Nullable T deviceCtx) { - TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg(); - if (kvListProto == null) { - throw new IllegalArgumentException("Attributes List for device: " + deviceName + " is empty!"); - } - try { - TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto); - processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); - } catch (Throwable e) { - log.warn("[{}][{}][{}] Failed to process device attributes command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, kvListProto, e); - } - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}][{}][{}] Failed to process device attributes command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t); - } - }, context.getExecutor()); - }); - } else { + if (CollectionUtils.isEmpty(attributesMsgList)) { log.debug("[{}][{}][{}] Devices attributes keys list is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId); throw new IllegalArgumentException("[" + sessionId + "] Devices attributes keys list is empty for [" + gateway.getDeviceId() + "]"); } + + attributesMsgList.forEach(attributesMsg -> { + String deviceName = checkDeviceName(attributesMsg.getDeviceName()); + process(deviceName, deviceCtx -> processPostAttributesMsg(deviceCtx, attributesMsg.getMsg(), deviceName, msgId), + t -> failedToProcessLog(deviceName, ATTRIBUTE, t)); + }); } catch (RuntimeException | InvalidProtocolBufferException e) { throw new AdaptorException(e); } } - protected void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostAttributeMsg postAttributeMsg, String deviceName, int msgId) { - transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); + protected void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostAttributeMsg kvListProto, String deviceName, int msgId) { + try { + TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto); + transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); + } catch (Throwable e) { + log.warn("[{}][{}][{}] Failed to process device attributes command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, kvListProto, e); + ackOrClose(msgId); + } } private void onDeviceAttributesRequestJson(MqttPublishMessage msg) throws AdaptorException { JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, msg.payload()); - if (json.isJsonObject()) { - JsonObject jsonObj = json.getAsJsonObject(); - int requestId = jsonObj.get("id").getAsInt(); - String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); - boolean clientScope = jsonObj.get("client").getAsBoolean(); - Set keys; - if (jsonObj.has("key")) { - keys = Collections.singleton(jsonObj.get("key").getAsString()); - } else { - JsonArray keysArray = jsonObj.get("keys").getAsJsonArray(); - keys = new HashSet<>(); - for (JsonElement keyObj : keysArray) { - keys.add(keyObj.getAsString()); - } - } - TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); - processGetAttributeRequestMessage(msg, deviceName, requestMsg); + validateJsonObject(json); + JsonObject jsonObj = json.getAsJsonObject(); + int requestId = jsonObj.get("id").getAsInt(); + String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); + boolean clientScope = jsonObj.get("client").getAsBoolean(); + Set keys; + if (jsonObj.has("key")) { + keys = Collections.singleton(jsonObj.get("key").getAsString()); } else { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + JsonArray keysArray = jsonObj.get("keys").getAsJsonArray(); + keys = new HashSet<>(); + for (JsonElement keyObj : keysArray) { + keys.add(keyObj.getAsString()); + } } + TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); + processGetAttributeRequestMessage(msg, deviceName, requestMsg); } private void onDeviceAttributesRequestProto(MqttPublishMessage mqttMsg) throws AdaptorException { @@ -637,26 +576,16 @@ public abstract class AbstractGatewaySessionHandler() { - @Override - public void onSuccess(@Nullable T deviceCtx) { - Integer requestId = jsonObj.get("id").getAsInt(); - String data = jsonObj.get("data").toString(); - TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() - .setRequestId(requestId).setPayload(data).build(); - processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); - } + validateJsonObject(json); + JsonObject jsonObj = json.getAsJsonObject(); + String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); + Integer requestId = jsonObj.get("id").getAsInt(); + String data = jsonObj.get("data").toString(); + onDeviceRpcResponse(requestId, data, deviceName, msgId); + } - @Override - public void onFailure(Throwable t) { - log.debug("[{}][{}][{}] Failed to process device Rpc response command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t); - } - }, context.getExecutor()); - } else { + private static void validateJsonObject(JsonElement json) { + if (!json.isJsonObject()) { throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); } } @@ -665,46 +594,36 @@ public abstract class AbstractGatewaySessionHandler() { - @Override - public void onSuccess(@Nullable T deviceCtx) { - Integer requestId = gatewayRpcResponseMsg.getId(); - String data = gatewayRpcResponseMsg.getData(); - TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() - .setRequestId(requestId).setPayload(data).build(); - processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}][{}][{}] Failed to process device Rpc response command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t); - } - }, context.getExecutor()); + Integer requestId = gatewayRpcResponseMsg.getId(); + String data = gatewayRpcResponseMsg.getData(); + onDeviceRpcResponse(requestId, data, deviceName, msgId); } catch (RuntimeException | InvalidProtocolBufferException e) { throw new AdaptorException(e); } } - private void processRpcResponseMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg, String deviceName, int msgId) { + private void onDeviceRpcResponse(Integer requestId, String data, String deviceName, int msgId) { + process(deviceName, deviceCtx -> processRpcResponseMsg(deviceCtx, requestId, data, deviceName, msgId), + t -> failedToProcessLog(deviceName, RPC_RESPONSE, t)); + } + + private void processRpcResponseMsg(MqttDeviceAwareSessionContext deviceCtx, Integer requestId, String data, String deviceName, int msgId) { + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() + .setRequestId(requestId).setPayload(data).build(); transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(channel, deviceName, msgId, rpcResponseMsg)); } private void processGetAttributeRequestMessage(MqttPublishMessage mqttMsg, String deviceName, TransportProtos.GetAttributeRequestMsg requestMsg) { int msgId = getMsgId(mqttMsg); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback<>() { - @Override - public void onSuccess(@Nullable T deviceCtx) { - transportService.process(deviceCtx.getSessionInfo(), requestMsg, getPubAckCallback(channel, deviceName, msgId, requestMsg)); - } + process(deviceName, deviceCtx -> processGetAttributeRequestMessage(deviceCtx, requestMsg, deviceName, msgId), + t -> { + failedToProcessLog(deviceName, ATTRIBUTES_REQUEST, t); + ack(mqttMsg, MqttReasonCodes.PubAck.IMPLEMENTATION_SPECIFIC_ERROR); + }); + } - @Override - public void onFailure(Throwable t) { - ack(mqttMsg, ReturnCode.IMPLEMENTATION_SPECIFIC); - log.debug("[{}][{}][{}] Failed to process device attributes request command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, t); - } - }, context.getExecutor()); + private void processGetAttributeRequestMessage(T deviceCtx, TransportProtos.GetAttributeRequestMsg requestMsg, String deviceName, int msgId) { + transportService.process(deviceCtx.getSessionInfo(), requestMsg, getPubAckCallback(channel, deviceName, msgId, requestMsg)); } private TransportProtos.GetAttributeRequestMsg toGetAttributeRequestMsg(int requestId, boolean clientScope, Set keys) { @@ -719,16 +638,6 @@ public abstract class AbstractGatewaySessionHandler checkDeviceConnected(String deviceName) { - T ctx = devices.get(deviceName); - if (ctx == null) { - log.debug("[{}][{}][{}] Missing device [{}] for the gateway session", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName); - return onDeviceConnect(deviceName, DEFAULT_DEVICE_TYPE); - } else { - return Futures.immediateFuture(ctx); - } - } - protected String checkDeviceName(String deviceName) { if (StringUtils.isEmpty(deviceName)) { throw new RuntimeException("Device name is empty!"); @@ -754,10 +663,22 @@ public abstract class AbstractGatewaySessionHandler 0) { - writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, returnCode)); + writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, returnCode.byteValue())); + } + } + + protected void ackOrClose(int msgId) { + if (MqttVersion.MQTT_5.equals(deviceSessionCtx.getMqttVersion())) { + ack(msgId, MqttReasonCodes.PubAck.PAYLOAD_FORMAT_INVALID); + } else { + channel.close(); } } @@ -786,16 +707,56 @@ public abstract class AbstractGatewaySessionHandler 0) { - ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, ReturnCode.SUCCESS)); + ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, MqttReasonCodes.PubAck.SUCCESS.byteValue())); + } else { + log.trace("[{}][{}][{}] Wrong msg id: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, msg); + ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, MqttReasonCodes.PubAck.UNSPECIFIED_ERROR.byteValue())); + closeDeviceSession(deviceName, MqttReasonCodes.Disconnect.MALFORMED_PACKET); } } @Override public void onError(Throwable e) { log.trace("[{}][{}][{}] Failed to publish msg: [{}] for device: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, msg, deviceName, e); + if (e instanceof TbRateLimitsException) { + closeDeviceSession(deviceName, MqttReasonCodes.Disconnect.MESSAGE_RATE_TOO_HIGH); + } else { + closeDeviceSession(deviceName, MqttReasonCodes.Disconnect.UNSPECIFIED_ERROR); + } ctx.close(); } }; } + protected void process(String deviceName, Consumer onSuccess, Consumer onFailure) { + ListenableFuture deviceCtxFuture = onDeviceConnect(deviceName, DEFAULT_DEVICE_TYPE); + process(deviceCtxFuture, onSuccess, onFailure); + } + + @SneakyThrows + protected void process(ListenableFuture deviceCtxFuture, Consumer onSuccess, Consumer onFailure) { + if (deviceCtxFuture.isDone()) { + onSuccess.accept(deviceCtxFuture.get()); + } else { + DonAsynchron.withCallback(deviceCtxFuture, onSuccess, onFailure, context.getExecutor()); + } + } + + protected void failedToProcessLog(String deviceName, String msgType, Throwable t) { + log.debug("[{}][{}][{}] Failed to process device {} command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, msgType, deviceName, t); + } + + + private void closeDeviceSession(String deviceName, MqttReasonCodes.Disconnect returnCode) { + try { + if (MqttVersion.MQTT_5.equals(deviceSessionCtx.getMqttVersion())) { + MqttTransportAdaptor adaptor = deviceSessionCtx.getPayloadAdaptor(); + int returnCodeValue = returnCode.byteValue() & 0xFF; + Optional deviceDisconnectPublishMsg = adaptor.convertToGatewayDeviceDisconnectPublish(deviceSessionCtx, deviceName, returnCodeValue); + deviceDisconnectPublishMsg.ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + } + } catch (Exception e) { + log.trace("Failed to send device disconnect to gateway session", e); + } + } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java index 1f389fced1..e06d954678 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.transport.mqtt.session; -import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -26,15 +25,16 @@ import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.codec.mqtt.MqttTopicSubscription; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.ResponseCode; import org.springframework.util.CollectionUtils; -import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; -import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; -import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.adaptor.AdaptorException; import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.adaptor.ProtoConverter; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportProtos; @@ -43,7 +43,6 @@ import org.thingsboard.server.transport.mqtt.MqttTransportHandler; import org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType; import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic; -import jakarta.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -51,7 +50,6 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.ONLINE; @@ -68,7 +66,9 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopi @Slf4j public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler { + @Getter private final SparkplugTopic sparkplugTopicNode; + @Getter private final Map nodeBirthMetrics; private final MqttTransportHandler parent; @@ -85,10 +85,6 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler metric))); } - public Map getNodeBirthMetrics() { - return this.nodeBirthMetrics; - } - public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; byte[] bytes = getBytes(inbound.payload()); @@ -135,69 +131,46 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler contextListenableFuture, - int msgId, List postTelemetryMsgList, String deviceName) throws AdaptorException { - try { - int finalMsgId = msgId; - postTelemetryMsgList.forEach(telemetryMsg -> { - Futures.addCallback(contextListenableFuture, - new FutureCallback<>() { - @Override - public void onSuccess(@Nullable MqttDeviceAwareSessionContext deviceCtx) { - try { - processPostTelemetryMsg(deviceCtx, telemetryMsg, deviceName, finalMsgId); - } catch (Throwable e) { - log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, telemetryMsg, e); - channel.close(); - } - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - }); - } catch (RuntimeException e) { - throw new AdaptorException(e); - } + int msgId, List postTelemetryMsgList, String deviceName) { + process(contextListenableFuture, deviceCtx -> { + for (TransportProtos.PostTelemetryMsg telemetryMsg : postTelemetryMsgList) { + try { + processPostTelemetryMsg(deviceCtx, telemetryMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, telemetryMsg, e); + ackOrClose(msgId); + } + } + }, + t -> log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t)); } private void onDeviceAttributesProto(ListenableFuture contextListenableFuture, int msgId, List attributesMsgList, String deviceName) throws AdaptorException { try { - if (!CollectionUtils.isEmpty(attributesMsgList)) { - attributesMsgList.forEach(attributesMsg -> { - Futures.addCallback(contextListenableFuture, - new FutureCallback<>() { - @Override - public void onSuccess(@Nullable MqttDeviceAwareSessionContext deviceCtx) { - TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg(); - try { - TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto); - processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); - } catch (Throwable e) { - log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e); - } - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - }); - } else { + if (CollectionUtils.isEmpty(attributesMsgList)) { log.debug("[{}] Devices attributes keys list is empty for: [{}]", sessionId, gateway.getDeviceId()); } + process(contextListenableFuture, deviceCtx -> { + for (TransportApiProtos.AttributesMsg attributesMsg : attributesMsgList) { + TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg(); + try { + TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto); + processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e); + } + } + }, + t -> log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t)); } catch (RuntimeException e) { throw new AdaptorException(e); } } public void handleSparkplugSubscribeMsg(List grantedQoSList, MqttTopicSubscription subscription, - MqttQoS reqQoS) throws ThingsboardException, AdaptorException, - ExecutionException, InterruptedException { - SparkplugTopic sparkplugTopic = parseTopicSubscribe(subscription.topicName()); + MqttQoS reqQoS) throws ThingsboardException { + SparkplugTopic sparkplugTopic = parseTopicSubscribe(subscription.topicFilter()); if (sparkplugTopic.getGroupId() == null) { // TODO SUBSCRIBE NameSpace } else if (sparkplugTopic.getType() == null) { @@ -238,9 +211,7 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler keyValueProtoOpt = fromSparkplugBMetricToKeyValueProto(key, protoMetric); - if (keyValueProtoOpt.isPresent()) { - msgs.add(postTelemetryMsgCreated(keyValueProtoOpt.get(), ts)); - } + keyValueProtoOpt.ifPresent(kvProto -> msgs.add(postTelemetryMsgCreated(kvProto, ts))); } } @@ -304,10 +275,6 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler createSparkplugMqttPublishMsg(TransportProtos.TsKvProto tsKvProto, String sparkplugTopic, SparkplugBProto.Payload.Metric metricBirth) { @@ -328,7 +295,6 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler toServerRpcPendingMap = new ConcurrentHashMap<>(); - public DefaultTransportService(PartitionService partitionService, - TbServiceInfoProvider serviceInfoProvider, - TbTransportQueueFactory queueProvider, - TbQueueProducerProvider producerProvider, - TopicService topicService, - StatsFactory statsFactory, - TransportDeviceProfileCache deviceProfileCache, - TransportTenantProfileCache tenantProfileCache, - TransportRateLimitService rateLimitService, - SchedulerComponent scheduler, TransportResourceCache transportResourceCache, - ApplicationEventPublisher eventPublisher, NotificationRuleProcessor notificationRuleProcessor, - EntityLimitsCache entityLimitsCache) { - this.partitionService = partitionService; - this.serviceInfoProvider = serviceInfoProvider; - this.queueProvider = queueProvider; - this.producerProvider = producerProvider; - this.topicService = topicService; - this.statsFactory = statsFactory; - this.deviceProfileCache = deviceProfileCache; - this.tenantProfileCache = tenantProfileCache; - this.rateLimitService = rateLimitService; - this.scheduler = scheduler; - this.transportResourceCache = transportResourceCache; - this.eventPublisher = eventPublisher; - this.notificationRuleProcessor = notificationRuleProcessor; - this.entityLimitsCache = entityLimitsCache; - } - @PostConstruct public void init() { super.init(); @@ -452,8 +428,13 @@ public class DefaultTransportService extends TransportActivityManager implements @Override public void process(TenantId tenantId, TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg requestMsg, TransportServiceCallback callback) { - TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setGetOrCreateDeviceRequestMsg(requestMsg).build()); log.trace("Processing msg: {}", requestMsg); + DeviceId gatewayid = new DeviceId(new UUID(requestMsg.getGatewayIdMSB(), requestMsg.getGatewayIdLSB())); + if (!checkLimits(tenantId, gatewayid, null, requestMsg.getDeviceName(), requestMsg, callback, 0)) { + return; + } + + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setGetOrCreateDeviceRequestMsg(requestMsg).build()); var key = new EntityLimitKey(tenantId, StringUtils.truncate(requestMsg.getDeviceName(), 256)); if (entityLimitsCache.get(key)) { transportCallbackExecutor.submit(() -> callback.onError(new RuntimeException(DataConstants.MAXIMUM_NUMBER_OF_DEVICES_REACHED))); @@ -857,6 +838,14 @@ public class DefaultTransportService extends TransportActivityManager implements gatewayId = new DeviceId(new UUID(sessionInfo.getGatewayIdMSB(), sessionInfo.getGatewayIdLSB())); } + return checkLimits(tenantId, gatewayId, deviceId, sessionInfo.getDeviceName(), msg, callback, dataPoints); + } + + private boolean checkLimits(TenantId tenantId, DeviceId gatewayId, DeviceId deviceId, String deviceName, Object msg, TransportServiceCallback callback, int dataPoints) { + if (log.isTraceEnabled()) { + log.trace("[{}][{}] Processing msg: {}", tenantId, deviceName, msg); + } + var rateLimitedPair = rateLimitService.checkLimits(tenantId, gatewayId, deviceId, dataPoints); if (rateLimitedPair == null) { return true; @@ -868,13 +857,16 @@ public class DefaultTransportService extends TransportActivityManager implements if (rateLimitedEntityType == EntityType.DEVICE || rateLimitedEntityType == EntityType.TENANT) { LimitedApi limitedApi = - rateLimitedEntityType == EntityType.TENANT ? LimitedApi.TRANSPORT_MESSAGES_PER_TENANT : + rateLimitedEntityType == EntityType.TENANT ? LimitedApi.TRANSPORT_MESSAGES_PER_TENANT : rateLimitedPair.getSecond() ? LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY : LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE; + + EntityId limitLevel = rateLimitedEntityType == EntityType.DEVICE ? deviceId == null ? gatewayId : deviceId : tenantId; + notificationRuleProcessor.process(RateLimitsTrigger.builder() .tenantId(tenantId) .api(limitedApi) - .limitLevel(rateLimitedEntityType == EntityType.DEVICE ? deviceId : tenantId) - .limitLevelEntityName(rateLimitedEntityType == EntityType.DEVICE ? sessionInfo.getDeviceName() : null) + .limitLevel(limitLevel) + .limitLevelEntityName(rateLimitedEntityType == EntityType.DEVICE ? deviceName : null) .build()); } return false; @@ -1109,19 +1101,6 @@ public class DefaultTransportService extends TransportActivityManager implements tbCoreMsgProducer.send(tpi, new TbProtoQueueMsg<>(routingKey, msg), wrappedCallback); } - private void sendToRuleEngine(TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) { - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, tbMsg.getOriginator()); - if (log.isTraceEnabled()) { - log.trace("[{}][{}] Pushing to topic {} message {}", tenantId, tbMsg.getOriginator(), tpi.getFullTopicName(), tbMsg); - } - ToRuleEngineMsg msg = ToRuleEngineMsg.newBuilder().setTbMsg(TbMsg.toByteString(tbMsg)) - .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) - .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()).build(); - ruleEngineProducerStats.incrementTotal(); - StatsCallback wrappedCallback = new StatsCallback(callback, ruleEngineProducerStats); - ruleEngineMsgProducer.send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), wrappedCallback); - } - private void sendToRuleEngine(TenantId tenantId, DeviceId deviceId, CustomerId customerId, TransportProtos.SessionInfoProto sessionInfo, JsonObject json, TbMsgMetaData metaData, TbMsgType tbMsgType, TbQueueCallback callback) { DeviceProfileId deviceProfileId = new DeviceProfileId(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); @@ -1139,7 +1118,8 @@ public class DefaultTransportService extends TransportActivityManager implements } TbMsg tbMsg = TbMsg.newMsg(queueName, tbMsgType, deviceId, customerId, metaData, gson.toJson(json), ruleChainId, null); - sendToRuleEngine(tenantId, tbMsg, callback); + ruleEngineProducerService.sendToRuleEngine(ruleEngineMsgProducer, tenantId, tbMsg, new StatsCallback(callback, ruleEngineProducerStats)); + ruleEngineProducerStats.incrementTotal(); } private class TransportTbQueueCallback implements TbQueueCallback { diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java index 4e8a2cd503..57e95cc56f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java @@ -22,7 +22,6 @@ import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.mobile.AndroidConfig; import org.thingsboard.server.common.data.mobile.BadgePosition; -import org.thingsboard.server.common.data.mobile.BadgeStyle; import org.thingsboard.server.common.data.mobile.IosConfig; import org.thingsboard.server.common.data.mobile.MobileAppSettings; import org.thingsboard.server.common.data.mobile.QRCodeConfig; @@ -98,7 +97,6 @@ public class BaseMobileAppSettingsService extends AbstractCachedEntityService returnCodeByteValue = new ArrayList<>(); + MqttClientCallback callbackForDisconnectWithReturnCode = getCallbackWrapperForDisconnectWithReturnCode(returnCodeByteValue); + mqttClient.setCallback(callbackForDisconnectWithReturnCode); + mqttClient.disconnect(); + Thread.sleep(1000); + assertThat(returnCodeByteValue.size()).isEqualTo(1); + MqttReasonCodes.Disconnect returnCode = MqttReasonCodes.Disconnect.valueOf(returnCodeByteValue.get(0)); + assertThat(returnCode).isEqualTo(MqttReasonCodes.Disconnect.NORMAL_DISCONNECT); + } + + @Test + public void clientSessionTakenOverDisconnect() throws Exception { + DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); + + MqttMessageListener listener = new MqttMessageListener(); + MqttClient mqttClient = getMqttClient(deviceCredentials, listener, MqttVersion.MQTT_5); + final List returnCodeByteValue = new ArrayList<>(); + MqttClientCallback callbackForDisconnectWithReturnCode = getCallbackWrapperForDisconnectWithReturnCode(returnCodeByteValue); + mqttClient.setCallback(callbackForDisconnectWithReturnCode); + + Thread.sleep(1000); + + MqttMessageListener dummyListener = new MqttMessageListener(); + MqttClient dummyMqttClient = getMqttClient(deviceCredentials, dummyListener, MqttVersion.MQTT_5); + final List returnCodeByteValueSecondClient = new ArrayList<>(); + MqttClientCallback callbackForDisconnectWithReturnCodeDummy = getCallbackWrapperForDisconnectWithReturnCode(returnCodeByteValueSecondClient); + dummyMqttClient.setCallback(callbackForDisconnectWithReturnCodeDummy); + + Awaitility + .await() + .alias("Check device disconnect.") + .atMost(TIMEOUT*timeoutMultiplier, TimeUnit.SECONDS) + .until(() -> returnCodeByteValue.size() > 0); + + assertThat(returnCodeByteValueSecondClient).isEmpty(); + assertThat(returnCodeByteValue).isNotEmpty(); + + MqttReasonCodes.Disconnect returnCode = MqttReasonCodes.Disconnect.valueOf(returnCodeByteValue.get(0)); + + dummyMqttClient.disconnect(); + + assertThat(returnCode).isEqualTo(MqttReasonCodes.Disconnect.SESSION_TAKEN_OVER); + } + + @Test + public void clientPublishForRegularTopicByProvisionClient() throws Exception { + MqttClient mqttClient = getMqttClient("provision", new MqttMessageListener(), MqttVersion.MQTT_5); + final List returnCodeByteValue = new ArrayList<>(); + MqttClientCallback callbackForDisconnectWithReturnCode = getCallbackWrapperForDisconnectWithReturnCode(returnCodeByteValue); + mqttClient.setCallback(callbackForDisconnectWithReturnCode); + mqttClient.publish("v1/devices/me/telemetry", Unpooled.wrappedBuffer("test".getBytes()), MqttQoS.AT_LEAST_ONCE).get(); + Thread.sleep(1000); + assertThat(returnCodeByteValue).isNotEmpty(); + MqttReasonCodes.Disconnect returnCode = MqttReasonCodes.Disconnect.valueOf(returnCodeByteValue.get(0)); + assertThat(returnCode).isEqualTo(MqttReasonCodes.Disconnect.TOPIC_NAME_INVALID); + } + + @Test + public void clientConnectWithBadCredentials() throws Exception { + MqttClient mqttClient = getMqttClient("unknownAccessToken", new MqttMessageListener(), MqttVersion.MQTT_5, false); + final List returnCodeByteValue = new ArrayList<>(); + MqttClientCallback callbackForDisconnectWithReturnCode = getCallbackWrapperForDisconnectWithReturnCode(returnCodeByteValue); + mqttClient.setCallback(callbackForDisconnectWithReturnCode); + try { + mqttClient.connect(TRANSPORT_HOST, TRANSPORT_PORT).get(1, TimeUnit.SECONDS); + } catch (TimeoutException ignored) { + } + assertThat(returnCodeByteValue).isNotEmpty(); + MqttConnectReturnCode returnCode = MqttConnectReturnCode.valueOf(returnCodeByteValue.get(0)); + assertThat(returnCode).isIn(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD, MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USERNAME_OR_PASSWORD); + } + private RuleChainId createRootRuleChainForRpcResponse() throws Exception { RuleChain newRuleChain = new RuleChain(); newRuleChain.setName("testRuleChain"); @@ -477,8 +568,34 @@ public class MqttClientTest extends AbstractContainerTest { return defaultRuleChain.get().getId(); } + private MqttClientCallback getCallbackWrapperForDisconnectWithReturnCode(List returnCodeByteValueWrapper) { + return new MqttClientCallback() { + @Override + public void connectionLost(Throwable cause) { + } + + @Override + public void onSuccessfulReconnect() { + } + + @Override + public void onDisconnect(MqttMessage mqttDisconnectMessage) { + log.info("Disconnected with reason: {}", mqttDisconnectMessage); + returnCodeByteValueWrapper.add(((MqttReasonCodeAndPropertiesVariableHeader) mqttDisconnectMessage.variableHeader()).reasonCode()); + } + }; + } + private MqttClient getMqttClient(DeviceCredentials deviceCredentials, MqttMessageListener listener) throws InterruptedException, ExecutionException { - return getMqttClient(deviceCredentials.getCredentialsId(), listener); + return getMqttClient(deviceCredentials.getCredentialsId(), listener, MqttVersion.MQTT_3_1_1, true); + } + + private MqttClient getMqttClient(DeviceCredentials deviceCredentials, MqttMessageListener listener, MqttVersion mqttVersion) throws InterruptedException, ExecutionException { + return getMqttClient(deviceCredentials.getCredentialsId(), listener, mqttVersion, true); + } + + private MqttClient getMqttClient(DeviceCredentials deviceCredentials, MqttMessageListener listener, MqttVersion mqttVersion, boolean connect) throws InterruptedException, ExecutionException { + return getMqttClient(deviceCredentials.getCredentialsId(), listener, mqttVersion, connect); } private String getOwnerId() { @@ -486,12 +603,23 @@ public class MqttClientTest extends AbstractContainerTest { } private MqttClient getMqttClient(String username, MqttMessageListener listener) throws InterruptedException, ExecutionException { + return getMqttClient(username, listener, MqttVersion.MQTT_3_1_1, true); + } + + private MqttClient getMqttClient(String username, MqttMessageListener listener, MqttVersion mqttVersion) throws InterruptedException, ExecutionException { + return getMqttClient(username, listener, mqttVersion, true); + } + + private MqttClient getMqttClient(String username, MqttMessageListener listener, MqttVersion mqttVersion, boolean connect) throws InterruptedException, ExecutionException { MqttClientConfig clientConfig = new MqttClientConfig(); clientConfig.setOwnerId(getOwnerId()); clientConfig.setClientId("MQTT client from test"); clientConfig.setUsername(username); + clientConfig.setProtocolVersion(mqttVersion); MqttClient mqttClient = MqttClient.create(clientConfig, listener, handlerExecutor); - mqttClient.connect("localhost", 1883).get(); + if (connect) { + mqttClient.connect(TRANSPORT_HOST, TRANSPORT_PORT).get(); + } return mqttClient; } diff --git a/msa/js-executor/config/default.yml b/msa/js-executor/config/default.yml index b2e84d0d46..0e15413e31 100644 --- a/msa/js-executor/config/default.yml +++ b/msa/js-executor/config/default.yml @@ -35,7 +35,7 @@ kafka: requestTimeout: "30000" # The default value in kafkajs is: 30000 connectionTimeout: "1000" # The default value in kafkajs is: 1000 compression: "gzip" # gzip or uncompressed - topic_properties: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100;min.insync.replicas:1" + topic_properties: "retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1" use_confluent_cloud: false client_id: "kafkajs" #inject pod name to easy identify the client using /opt/kafka/bin/kafka-consumer-groups.sh ssl: diff --git a/msa/vc-executor/src/main/resources/tb-vc-executor.yml b/msa/vc-executor/src/main/resources/tb-vc-executor.yml index a92a7eea2b..a55a0e73dc 100644 --- a/msa/vc-executor/src/main/resources/tb-vc-executor.yml +++ b/msa/vc-executor/src/main/resources/tb-vc-executor.yml @@ -132,13 +132,13 @@ queue: # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) topic-properties: # Kafka properties for Core topics - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Notifications topics - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Core topics - version-control: "${TB_QUEUE_KAFKA_VC_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + version-control: "${TB_QUEUE_KAFKA_VC_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Housekeeper tasks topic - housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" consumer-stats: # Prints lag between consumer group offset and last messages offset in Kafka topics enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java index c7ff2746bd..f14d9afa5a 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java @@ -84,6 +84,9 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler case PUBCOMP: handlePubcomp(msg); break; + case DISCONNECT: + handleDisconnect(msg); + break; } } else { log.error("[{}] Message decoding failed: {}", client.getClientConfig().getClientId(), msg.decoderResult().cause().getMessage()); @@ -204,6 +207,9 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler // Don't start reconnect logic here break; } + if (this.client.getCallback() != null) { + this.client.getCallback().onConnAck(message); + } } private void handleSubAck(MqttSubAckMessage message) { @@ -224,6 +230,9 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler if (!pendingSubscription.getFuture().isDone()) { pendingSubscription.getFuture().setSuccess(null); } + if (this.client.getCallback() != null) { + this.client.getCallback().onSubAck(message); + } } private void handlePublish(Channel channel, MqttPublishMessage message) { @@ -267,6 +276,9 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler this.client.getServerSubscriptions().remove(unsubscription.getTopic()); unsubscription.getFuture().setSuccess(null); this.client.getPendingServerUnsubscribes().remove(message.variableHeader().messageId()); + if (this.client.getCallback() != null) { + this.client.getCallback().onUnsubAck(message); + } } private void handlePuback(MqttPubAckMessage message) { @@ -278,6 +290,9 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler pendingPublish.onPubackReceived(); this.client.getPendingPublishes().remove(message.variableHeader().messageId()); pendingPublish.getPayload().release(); + if (this.client.getCallback() != null) { + this.client.getCallback().onPubAck(message); + } } private void handlePubrec(Channel channel, MqttMessage message) { @@ -301,7 +316,7 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler future = Futures.transform(future, x -> { this.client.getQos2PendingIncomingPublishes().remove(incomingQos2Publish.getIncomingPublish().variableHeader().packetId()); return null; - }, MoreExecutors.directExecutor()); + }, MoreExecutors.directExecutor()); } future.addListener(() -> { MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0); @@ -319,6 +334,12 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler pendingPublish.onPubcompReceived(); } + private void handleDisconnect(MqttMessage message) { + if (this.client.getCallback() != null) { + this.client.getCallback().onDisconnect(message); + } + } + @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { try { diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java index 42acc05f42..f7963ad4b1 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java @@ -15,6 +15,12 @@ */ package org.thingsboard.mqtt; +import io.netty.handler.codec.mqtt.MqttConnAckMessage; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttPubAckMessage; +import io.netty.handler.codec.mqtt.MqttSubAckMessage; +import io.netty.handler.codec.mqtt.MqttUnsubAckMessage; + /** * Created by Valerii Sosliuk on 12/30/2017. */ @@ -32,4 +38,19 @@ public interface MqttClientCallback { * */ void onSuccessfulReconnect(); + + default void onConnAck(MqttConnAckMessage connAckMessage) { + } + + default void onPubAck(MqttPubAckMessage pubAckMessage) { + } + + default void onSubAck(MqttSubAckMessage pubAckMessage) { + } + + default void onUnsubAck(MqttUnsubAckMessage unsubAckMessage) { + } + + default void onDisconnect(MqttMessage mqttDisconnectMessage) { + } } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java index 6781bb171a..86ce95e350 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java @@ -45,6 +45,7 @@ import io.netty.handler.timeout.IdleStateHandler; import io.netty.util.concurrent.DefaultPromise; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ListeningExecutor; @@ -87,6 +88,7 @@ final class MqttClientImpl implements MqttClient { private volatile boolean reconnect = false; private String host; private int port; + @Getter private MqttClientCallback callback; private final ListeningExecutor handlerExecutor; @@ -426,7 +428,12 @@ final class MqttClientImpl implements MqttClient { disconnected = true; if (this.channel != null) { MqttMessage message = new MqttMessage(new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0)); - this.sendAndFlushPacket(message).addListener(future1 -> channel.close()); + ChannelFuture channelFuture = this.sendAndFlushPacket(message); + eventLoop.schedule(() -> { + if (!channelFuture.isDone()) { + this.channel.close(); + } + }, 500, TimeUnit.MILLISECONDS); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java index f03d1e46ba..44005f9d65 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java @@ -250,8 +250,9 @@ class AlarmState { newAlarm.setCleared(false); newAlarm.setSeverity(severity); long startTs = dataSnapshot.getTs(); - if (startTs == 0L) { - startTs = System.currentTimeMillis(); + long currentTime = System.currentTimeMillis(); + if (startTs == 0L || startTs > currentTime) { + startTs = currentTime; } newAlarm.setStartTs(startTs); newAlarm.setEndTs(startTs); diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 17a6a53aca..f2ea0c1685 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -130,13 +130,11 @@ redis: # Determines the behavior when a thread requests a connection from the pool but there are no available connections and the pool cannot create more due to the maxTotal configuration blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}" -# COAP server parameters transport: + # Local CoAP transport parameters coap: - # CoAP bind-address - bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" - # CoAP bind port - bind_port: "${COAP_BIND_PORT:5683}" + # Enable/disable coap transport protocol. + enabled: "${COAP_ENABLED:true}" # CoaP processing timeout in milliseconds timeout: "${COAP_TIMEOUT:10000}" # CoaP piggyback response timeout in milliseconds @@ -145,57 +143,6 @@ transport: psm_activity_timer: "${COAP_PSM_ACTIVITY_TIMER:10000}" # Default PSM Activity Timer if not specified in device profile paging_transmission_window: "${COAP_PAGING_TRANSMISSION_WINDOW:10000}" - dtls: - # Enable/disable DTLS 1.2 support - enabled: "${COAP_DTLS_ENABLED:false}" - # RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000 - retransmission_timeout: "${COAP_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}" - # CoAP DTLS bind address - bind_address: "${COAP_DTLS_BIND_ADDRESS:0.0.0.0}" - # CoAP DTLS bind port - bind_port: "${COAP_DTLS_BIND_PORT:5684}" - # Server DTLS credentials - # Server DTLS credentials - # CoAP DTLS connection ID length. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off - # Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used - connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:}" - credentials: - # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) - type: "${COAP_DTLS_CREDENTIALS_TYPE:PEM}" - # PEM server credentials - pem: - # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) - cert_file: "${COAP_DTLS_PEM_CERT:coapserver.pem}" - # Path to the server certificate private key file. Optional by default. Required if the private key is not present in server certificate file; - key_file: "${COAP_DTLS_PEM_KEY:coapserver_key.pem}" - # Server certificate private key password (optional) - key_password: "${COAP_DTLS_PEM_KEY_PASSWORD:server_key_password}" - # Keystore server credentials - keystore: - # Type of the key store (JKS or PKCS12) - type: "${COAP_DTLS_KEY_STORE_TYPE:JKS}" - # Path to the key store that holds the SSL certificate - store_file: "${COAP_DTLS_KEY_STORE:coapserver.jks}" - # Password used to access the key store - store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}" - # Key alias - key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}" - # Password used to access the key - key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}" - x509: - # Skip certificate validity check for client certificates. - skip_validity_check_for_client_cert: "${TB_COAP_X509_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" - # Inactivity timeout of DTLS session. Used to clean cache - dtls_session_inactivity_timeout: "${TB_COAP_X509_DTLS_SESSION_INACTIVITY_TIMEOUT:86400000}" - # Interval of periodic eviction of the timed-out DTLS sessions - dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}" sessions: # Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device. # The parameter value is in milliseconds. @@ -224,6 +171,65 @@ transport: # Interval of transport statistics logging print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" +# CoAP server parameters +coap: + # Enable/disable coap transport protocol. + enabled: "${COAP_SERVER_ENABLED:true}" + # CoAP bind-address + bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" + # CoAP bind port + bind_port: "${COAP_BIND_PORT:5683}" + dtls: + # Enable/disable DTLS 1.2 support + enabled: "${COAP_DTLS_ENABLED:false}" + # RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000 + retransmission_timeout: "${COAP_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}" + # CoAP DTLS bind address + bind_address: "${COAP_DTLS_BIND_ADDRESS:0.0.0.0}" + # CoAP DTLS bind port + bind_port: "${COAP_DTLS_BIND_PORT:5684}" + # CoAP DTLS connection ID length. RFC 9146, Connection Identifier for DTLS 1.2 + # Default: off + # Control usage of DTLS connection ID length (CID). + # - 'off' to deactivate it. + # - 'on' to activate Connection ID support (same as CID 0 or more 0). + # - A positive value defines generated CID size in bytes. + # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). + # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used + # - A value that are > 4: MultiNodeConnectionIdGenerator is used + connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:}" + # Server DTLS credentials + credentials: + # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) + type: "${COAP_DTLS_CREDENTIALS_TYPE:PEM}" + # PEM server credentials + pem: + # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) + cert_file: "${COAP_DTLS_PEM_CERT:coapserver.pem}" + # Path to the server certificate private key file. Optional by default. Required if the private key is not present in the server certificate file; + key_file: "${COAP_DTLS_PEM_KEY:coapserver_key.pem}" + # Server certificate private key password (optional) + key_password: "${COAP_DTLS_PEM_KEY_PASSWORD:server_key_password}" + # Keystore server credentials + keystore: + # Type of the key store (JKS or PKCS12) + type: "${COAP_DTLS_KEY_STORE_TYPE:JKS}" + # Path to the key store that holds the SSL certificate + store_file: "${COAP_DTLS_KEY_STORE:coapserver.jks}" + # Password used to access the key store + store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}" + # Key alias + key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}" + # Password used to access the key + key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}" + x509: + # Skip certificate validity check for client certificates. + skip_validity_check_for_client_cert: "${TB_COAP_X509_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" + # Inactivity timeout of DTLS session. Used to cleanup cache + dtls_session_inactivity_timeout: "${TB_COAP_X509_DTLS_SESSION_INACTIVITY_TIMEOUT:86400000}" + # Interval of periodic eviction of the timed-out DTLS sessions + dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}" + # Queue configuration parameters queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) @@ -292,15 +298,15 @@ queue: # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) topic-properties: # Kafka properties for Rule Engine - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Core topics - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Transport Api topics - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" # Kafka properties for Notifications topics - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Housekeeper tasks topic - housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" aws_sqs: # Use the default credentials provider for AWS SQS use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 4252dfac06..9a5615478d 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -265,15 +265,15 @@ queue: # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) topic-properties: # Kafka properties for Rule Engine - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Core topics - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Transport Api topics - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" # Kafka properties for Notifications topics - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Housekeeper tasks topic - housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" aws_sqs: # Use default credentials provider for AWS SQS use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index cf1ba51240..bf6a159106 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -152,8 +152,6 @@ transport: client_side_rpc: # Processing timeout interval of the RPC command on the CLIENT SIDE. Time in milliseconds timeout: "${CLIENT_SIDE_RPC_TIMEOUT:60000}" - # Enable/disable http/mqtt/coap transport protocols (has higher priority than certain protocol's 'enabled' property) - api_enabled: "${TB_TRANSPORT_API_ENABLED:true}" log: # Enable/Disable log of transport messages to telemetry. For example, logging of LwM2M registration update enabled: "${TB_TRANSPORT_LOG_ENABLED:true}" @@ -370,15 +368,15 @@ queue: # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) topic-properties: # Kafka properties for Rule Engine - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Core topics - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Transport Api topics - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" # Kafka properties for Notifications topics - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Housekeeper tasks topic - housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" aws_sqs: # Use the default credentials provider for AWS SQS use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index d210138852..140839ccc3 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -143,6 +143,8 @@ transport: proxy_enabled: "${MQTT_PROXY_PROTOCOL_ENABLED:false}" # MQTT processing timeout in milliseconds timeout: "${MQTT_TIMEOUT:10000}" + # MQTT disconnect timeout in milliseconds. The time to wait for the client to disconnect after the server sends a disconnect message. + disconnect_timeout: "${MQTT_DISCONNECT_TIMEOUT:1000}" msg_queue_size_per_device_limit: "${MQTT_MSG_QUEUE_SIZE_PER_DEVICE_LIMIT:100}" # messages await in the queue before device connected state. This limit works on low level before TenantProfileLimits mechanism netty: # Netty leak detector level @@ -297,15 +299,15 @@ queue: # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) topic-properties: # Kafka properties for Rule Engine - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Core topics - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Transport Api topics - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" # Kafka properties for Notifications topics - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Housekeeper tasks topic - housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" aws_sqs: # Use the default credentials provider for AWS SQS use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 76f6abdb61..85a125b351 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -247,15 +247,15 @@ queue: # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) topic-properties: # Kafka properties for Rule Engine - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Core topics - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Transport Api topics - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" # Kafka properties for Notifications topics - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Housekeeper tasks topic - housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" consumer-stats: # Prints lag between consumer group offset and last messages offset in Kafka topics enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" diff --git a/ui-ngx/src/app/core/auth/auth.actions.ts b/ui-ngx/src/app/core/auth/auth.actions.ts index cd44bcde7e..86c80e2d8b 100644 --- a/ui-ngx/src/app/core/auth/auth.actions.ts +++ b/ui-ngx/src/app/core/auth/auth.actions.ts @@ -27,6 +27,7 @@ export enum AuthActionTypes { UPDATE_AUTH_USER = '[Auth] Update Auth User', UPDATE_LAST_PUBLIC_DASHBOARD_ID = '[Auth] Update Last Public Dashboard Id', UPDATE_HAS_REPOSITORY = '[Auth] Change Has Repository', + UPDATE_MOBILE_QR_ENABLED = '[Auth] Update Mobile QR Enabled', UPDATE_OPENED_MENU_SECTION = '[Preferences] Update Opened Menu Section', PUT_USER_SETTINGS = '[Preferences] Put user settings', DELETE_USER_SETTINGS = '[Preferences] Delete user settings', @@ -72,6 +73,12 @@ export class ActionAuthUpdateHasRepository implements Action { constructor(readonly payload: { hasRepository: boolean }) {} } +export class ActionUpdateMobileQrCodeEnabled implements Action { + readonly type = AuthActionTypes.UPDATE_MOBILE_QR_ENABLED; + + constructor(readonly payload: { mobileQrEnabled: boolean }) {} +} + export class ActionPreferencesUpdateOpenedMenuSection implements Action { readonly type = AuthActionTypes.UPDATE_OPENED_MENU_SECTION; @@ -93,4 +100,4 @@ export class ActionPreferencesDeleteUserSettings implements Action { export type AuthActions = ActionAuthAuthenticated | ActionAuthUnauthenticated | ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId | ActionAuthUpdateHasRepository | ActionPreferencesUpdateOpenedMenuSection | ActionPreferencesPutUserSettings | ActionPreferencesDeleteUserSettings | - ActionAuthUpdateAuthUser; + ActionAuthUpdateAuthUser | ActionUpdateMobileQrCodeEnabled; diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index cc9d01532a..73e0d04eb1 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -70,6 +70,9 @@ export const authReducer = ( case AuthActionTypes.UPDATE_HAS_REPOSITORY: return { ...state, ...action.payload}; + case AuthActionTypes.UPDATE_MOBILE_QR_ENABLED: + return { ...state, ...action.payload}; + case AuthActionTypes.UPDATE_OPENED_MENU_SECTION: const openedMenuSections = new Set(state.userSettings.openedMenuSections); if (action.payload.opened) { diff --git a/ui-ngx/src/app/core/services/help.service.ts b/ui-ngx/src/app/core/services/help.service.ts index c47241a79b..9c5ea720ba 100644 --- a/ui-ngx/src/app/core/services/help.service.ts +++ b/ui-ngx/src/app/core/services/help.service.ts @@ -19,7 +19,7 @@ import { HttpClient } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; import { Observable, of } from 'rxjs'; import { catchError, map, mergeMap, tap } from 'rxjs/operators'; -import { helpBaseUrl as siteBaseUrl } from '@shared/models/constants'; +import { docPlatformPrefix, helpBaseUrl as siteBaseUrl } from '@shared/models/constants'; import { UiSettingsService } from '@core/http/ui-settings.service'; const localHelpBaseUrl = '/assets'; @@ -35,6 +35,7 @@ const NOT_FOUND_CONTENT: HelpData = { export class HelpService { private siteBaseUrl = siteBaseUrl; + private docPlatformPrefix = docPlatformPrefix; private helpCache: {[lang: string]: {[key: string]: string}} = {}; constructor( @@ -104,10 +105,15 @@ export class HelpService { } private processVariables(helpData: HelpData): string { - const baseUrlReg = /\${siteBaseUrl}/g; - helpData.content = helpData.content.replace(baseUrlReg, this.siteBaseUrl); - const helpBaseUrlReg = /\${helpBaseUrl}/g; - return helpData.content.replace(helpBaseUrlReg, helpData.helpBaseUrl); + const variables = { + siteBaseUrl: this.siteBaseUrl, + docPlatformPrefix: this.docPlatformPrefix, + helpBaseUrl: helpData.helpBaseUrl + }; + + const regExp = new RegExp(Object.keys(variables).map(key => `\\\${${key}}`).join('|'), 'g'); + + return helpData.content.replace(regExp, (match) => variables[match.slice(2, -1)]); } private processIncludes(content: string): Observable { diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html index e13a0c9fa6..d6edb9b4cd 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html @@ -28,34 +28,31 @@
-
-
-
- - alias.name - - - {{ 'alias.name-required' | translate }} - - - {{ 'alias.duplicate-alias' | translate }} - - -
- - - -
+
+
+ + alias.name + + + {{ 'alias.name-required' | translate }} + + + {{ 'alias.duplicate-alias' | translate }} + + +
+ + {{ 'alias.resolve-multiple' | translate }} +
- - -
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.scss index 51a25742b7..969729c373 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.scss @@ -13,16 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import '../../../../../scss/constants'; + :host { .tb-resolve-multiple-switch { - padding-left: 10px; - - .resolve-multiple-switch { - margin: 0; - } - - .resolve-multiple-label { - margin: 5px 0; + padding: 18px 0 0 18px; + @media #{$mat-xs} { + padding: 0 0 18px 0; } } -} \ No newline at end of file +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 06b4924a7d..70022fff1b 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -1350,9 +1350,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC }); } - exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { + exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string) { $event.stopPropagation(); - this.importExport.exportWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget); + this.importExport.exportWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget, widgetTitle); } widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts index 2f18de2e6e..b2ed609c29 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts @@ -49,7 +49,7 @@ export interface IDashboardController { openDashboardState(stateId: string, openRightLayout: boolean); addWidget($event: Event, layoutCtx: DashboardPageLayoutContext); editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); - exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); + exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string); removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss index 1285b9568c..c75d485151 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss @@ -21,7 +21,7 @@ right: 0; bottom: 0; background: #fff; - z-index: 1; + z-index: 5; } .widget-preview-section { position: absolute; diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts index 9b4aee95e5..36fb4d2427 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts @@ -226,8 +226,8 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo this.layoutCtx.dashboardCtrl.editWidget($event, this.layoutCtx, widget); } - onExportWidget($event: Event, widget: Widget): void { - this.layoutCtx.dashboardCtrl.exportWidget($event, this.layoutCtx, widget); + onExportWidget($event: Event, widget: Widget, widgetTitle: string): void { + this.layoutCtx.dashboardCtrl.exportWidget($event, this.layoutCtx, widget, widgetTitle); } onRemoveWidget($event: Event, widget: Widget): void { diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts index 6cc1ec7dbe..ce968d0e47 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts @@ -54,7 +54,7 @@ import { IAliasController, IStateController } from '@app/core/api/widget-api.mod import { Widget, WidgetPosition } from '@app/shared/models/widget.models'; import { MatMenuTrigger } from '@angular/material/menu'; import { SafeStyle } from '@angular/platform-browser'; -import { distinct } from 'rxjs/operators'; +import { distinct, take } from 'rxjs/operators'; import { ResizeObserver } from '@juggle/resize-observer'; import { UtilsService } from '@core/services/utils.service'; import { WidgetComponentAction, WidgetComponentActionType } from '@home/components/widget/widget-container.component'; @@ -436,7 +436,11 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo $event.stopPropagation(); } if (this.isExportActionEnabled && this.callbacks && this.callbacks.onExportWidget) { - this.callbacks.onExportWidget($event, widget.widget); + widget.title$.pipe( + take(1) + ).subscribe((widgetTitle) => { + this.callbacks.onExportWidget($event, widget.widget, widgetTitle); + }); } } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html index 845c3bd2e8..61d22dcff4 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html @@ -130,67 +130,57 @@ -
- -
- +
+
+
alias.root-entity
+ + {{ 'alias.root-state-entity' | translate }} - -
-
- + + alias.state-entity-parameter-name + + + + +
+ -
-
- - alias.state-entity-parameter-name - - -
- - - +
+ + relation.direction + + + {{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }} + + + + + alias.max-relation-level + +
+ + {{ 'alias.last-level-relation' | translate }} +
-
-
- - - -
-
-
- - relation.direction - - - {{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }} - - - - - alias.max-relation-level - - +
+
relation.relation-filters
+ +
-
relation.relation-filters
- - - queue.custom-properties - - queue.custom-properties-hint - - - queue.description - - queue.description-hint - +
+ + {{ 'queue.duplicate-msg-to-all-partitions' | translate }} + + + queue.custom-properties + + queue.custom-properties-hint + + + queue.description + + queue.description-hint + +
diff --git a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts index f54d99e332..c9a09a262d 100644 --- a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts +++ b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts @@ -118,7 +118,8 @@ export class QueueFormComponent implements ControlValueAccessor, OnInit, OnDestr topic: [''], additionalInfo: this.fb.group({ description: [''], - customProperties: [''] + customProperties: [''], + duplicateMsgToAllPartitions: [false] }) }); this.valueChange$ = this.queueFormGroup.valueChanges.subscribe(() => { @@ -160,6 +161,10 @@ export class QueueFormComponent implements ControlValueAccessor, OnInit, OnDestr this.queueFormGroup.patchValue(this.modelValue, {emitEvent: false}); this.queueFormGroup.get('additionalInfo').get('description') .patchValue(this.modelValue.additionalInfo?.description, {emitEvent: false}); + this.queueFormGroup.get('additionalInfo').get('customProperties') + .patchValue(this.modelValue.additionalInfo?.customProperties, {emitEvent: false}); + this.queueFormGroup.get('additionalInfo').get('duplicateMsgToAllPartitions') + .patchValue(this.modelValue.additionalInfo?.duplicateMsgToAllPartitions, {emitEvent: false}); this.submitStrategyTypeChanged(); if (!this.disabled && !this.queueFormGroup.valid) { this.updateModel(); diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html index 60358c6b2d..055b5df22e 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.html @@ -18,13 +18,17 @@
+
{{ 'relation.relation-type' | translate }}
{{ 'entity.entity-types' | translate }}
-
+ + {{ 'relation.not' | translate}} + ; + @Input() + @coerceBoolean() + enableNotOption = false; + relationFiltersFormGroup: UntypedFormGroup; private destroy$ = new Subject(); @@ -118,10 +123,14 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa } private createRelationFilterFormGroup(filter: RelationEntityTypeFilter): AbstractControl { - return this.fb.group({ + const formGroup = this.fb.group({ relationType: [filter ? filter.relationType : null], entityTypes: [filter ? filter.entityTypes : []] }); + if (this.enableNotOption) { + formGroup.addControl('negate', this.fb.control(filter ? filter.negate : false)); + } + return formGroup; } private updateModel() { diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts index 230811fbd1..8f2398cab2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts @@ -118,7 +118,7 @@ export class WidgetActionDialogComponent extends DialogComponent { - if (this.action?.actionSourceId === 'cellClick' && isDefinedAndNotNull(this.action.columnIndex) && - this.widgetActionFormGroup.get('columnIndex').value === null) { - this.columnIndexPlaceholderText = `${this.action.columnIndex} (${this.translate.instant('widget-config.not-set')})`; - this.columnIndexSelect.focus(); + if (this.action?.actionSourceId === 'cellClick') { + this.widgetActionFormGroup.get('columnIndex').enable(); + if (isDefinedAndNotNull(this.action.columnIndex) && this.widgetActionFormGroup.get('columnIndex').value === null) { + this.columnIndexPlaceholderText = `${this.action.columnIndex} (${this.translate.instant('widget-config.not-set')})`; + this.columnIndexSelect.focus(); + } } }); } @@ -253,9 +254,6 @@ export class WidgetActionDialogComponent extends DialogComponent + +
+
+ + {{ 'admin.mobile-app.use-system-settings' | translate }} + +
+
+
+
+
widget-config.appearance
+
{{ 'widgets.mobile-app-qr-code.configuration-hint' | translate }}
+
+ + {{ 'admin.mobile-app.badges' | translate }} + + + + + {{ badgePosition.value | translate }} + + + +
+
+ + {{ 'admin.mobile-app.label' | translate }} + + + + + warning + + +
+
+ + {{ 'widgets.liquid-level-card.title' | translate }} + +
+ + + + + + + +
+
+
+ + {{ 'widgets.liquid-level-card.icon' | translate }} + +
+ + + + + + + + +
+
+
+
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.ts new file mode 100644 index 0000000000..25f091df7b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/mobile-app-qr-code-basic-config.component.ts @@ -0,0 +1,182 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, Injector } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { WidgetConfig } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { isUndefined } from '@core/utils'; +import { cssSizeToStrSize, resolveCssSize } from '@shared/models/widget-settings.models'; +import { + mobileAppQrCodeWidgetDefaultSettings, + MobileAppQrCodeWidgetSettings +} from '@home/components/widget/lib/cards/mobile-app-qr-code-widget.models'; +import { badgePositionTranslationsMap } from '@app/shared/models/mobile-app.models'; + +@Component({ + selector: 'tb-mobile-app-qr-code-basic-config', + templateUrl: './mobile-app-qr-code-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class MobileAppQrCodeBasicConfigComponent extends BasicWidgetConfigComponent { + + mobileAppQrCodeWidgetConfigForm: UntypedFormGroup; + badgePositionTranslationsMap = badgePositionTranslationsMap; + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private cd: ChangeDetectorRef, + private $injector: Injector, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.mobileAppQrCodeWidgetConfigForm; + } + + protected setupConfig(widgetConfig: WidgetConfigComponentData) { + super.setupConfig(widgetConfig); + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: MobileAppQrCodeWidgetSettings = {...mobileAppQrCodeWidgetDefaultSettings, ...(configData.config.settings || {})}; + const iconSize = resolveCssSize(configData.config.iconSize); + + this.mobileAppQrCodeWidgetConfigForm = this.fb.group({ + useSystemSettings: [settings.useSystemSettings], + + badgeEnabled: [settings.qrCodeConfig.badgeEnabled], + badgePosition: [settings.qrCodeConfig.badgePosition], + qrCodeLabelEnabled: [settings.qrCodeConfig.qrCodeLabelEnabled], + qrCodeLabel: [settings.qrCodeConfig.qrCodeLabel], + + showTitle: [configData.config.showTitle, []], + title: [configData.config.title, []], + titleFont: [configData.config.titleFont, []], + titleColor: [configData.config.titleColor, []], + showTitleIcon: [configData.config.showTitleIcon, []], + iconSize: [iconSize[0], [Validators.min(0)]], + iconSizeUnit: [iconSize[1], []], + titleIcon: [configData.config.titleIcon, []], + iconColor: [configData.config.iconColor, []], + + background: [settings.background, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.showTitle = config.showTitle; + this.widgetConfig.config.title = config.title; + this.widgetConfig.config.titleFont = config.titleFont; + this.widgetConfig.config.titleColor = config.titleColor; + + this.widgetConfig.config.showTitleIcon = config.showTitleIcon; + this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit); + this.widgetConfig.config.titleIcon = config.titleIcon; + this.widgetConfig.config.iconColor = config.iconColor; + + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + this.widgetConfig.config.settings.useSystemSettings = config.useSystemSettings; + this.widgetConfig.config.settings.qrCodeConfig = { + badgeEnabled: config.badgeEnabled, + badgePosition: config.badgePosition, + qrCodeLabelEnabled: config.qrCodeLabelEnabled, + qrCodeLabel: config.qrCodeLabel + }; + + this.widgetConfig.config.settings.background = config.background; + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + + this.widgetConfig.config.actions = config.actions; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showTitleIcon', 'badgeEnabled', 'qrCodeLabelEnabled', 'showTitleIcon', 'showTitle']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const useSystemSettings = this.mobileAppQrCodeWidgetConfigForm.get('useSystemSettings').value; + + if (!useSystemSettings) { + const badgeEnabled = this.mobileAppQrCodeWidgetConfigForm.get('badgeEnabled').value; + const qrCodeLabelEnabled = this.mobileAppQrCodeWidgetConfigForm.get('qrCodeLabelEnabled').value; + const showTitleIcon: boolean = this.mobileAppQrCodeWidgetConfigForm.get('showTitleIcon').value; + const showTitle: boolean = this.mobileAppQrCodeWidgetConfigForm.get('showTitle').value; + + if (badgeEnabled) { + this.mobileAppQrCodeWidgetConfigForm.get('badgePosition').enable({emitEvent: false}); + } else { + this.mobileAppQrCodeWidgetConfigForm.get('badgePosition').disable({emitEvent: false}); + } + if (qrCodeLabelEnabled) { + this.mobileAppQrCodeWidgetConfigForm.get('qrCodeLabel').enable({emitEvent: false}); + } else { + this.mobileAppQrCodeWidgetConfigForm.get('qrCodeLabel').disable({emitEvent: false}); + } + if (showTitle) { + this.mobileAppQrCodeWidgetConfigForm.get('title').enable({emitEvent: false}); + this.mobileAppQrCodeWidgetConfigForm.get('titleFont').enable({emitEvent: false}); + this.mobileAppQrCodeWidgetConfigForm.get('titleColor').enable({emitEvent: false}); + this.mobileAppQrCodeWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false}); + if (showTitleIcon) { + this.mobileAppQrCodeWidgetConfigForm.get('titleIcon').enable({emitEvent: false}); + this.mobileAppQrCodeWidgetConfigForm.get('iconColor').enable({emitEvent: false}); + this.mobileAppQrCodeWidgetConfigForm.get('iconSize').enable({emitEvent: false}); + this.mobileAppQrCodeWidgetConfigForm.get('iconSizeUnit').enable({emitEvent: false}); + } else { + this.mobileAppQrCodeWidgetConfigForm.get('titleIcon').disable({emitEvent: false}); + this.mobileAppQrCodeWidgetConfigForm.get('iconColor').disable({emitEvent: false}); + this.mobileAppQrCodeWidgetConfigForm.get('iconSize').disable({emitEvent: false}); + this.mobileAppQrCodeWidgetConfigForm.get('iconSizeUnit').disable({emitEvent: false}); + } + } else { + this.mobileAppQrCodeWidgetConfigForm.get('title').disable({emitEvent: false}); + this.mobileAppQrCodeWidgetConfigForm.get('titleFont').disable({emitEvent: false}); + this.mobileAppQrCodeWidgetConfigForm.get('titleColor').disable({emitEvent: false}); + this.mobileAppQrCodeWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false}); + this.mobileAppQrCodeWidgetConfigForm.get('titleIcon').disable({emitEvent: false}); + this.mobileAppQrCodeWidgetConfigForm.get('iconColor').disable({emitEvent: false}); + this.mobileAppQrCodeWidgetConfigForm.get('iconSize').disable({emitEvent: false}); + this.mobileAppQrCodeWidgetConfigForm.get('iconSizeUnit').disable({emitEvent: false}); + } + } + } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.enableFullscreen = buttons.includes('fullscreen'); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html index 3d0fe04e4c..51ef353a10 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html @@ -27,7 +27,9 @@
-
{{ 'widgets.chart.series' | translate }}
+
+ {{ 'widgets.chart.series' | translate }} +
{{ 'widgets.chart.series' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.html new file mode 100644 index 0000000000..84deb5ceaf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.html @@ -0,0 +1,175 @@ + + + + + + +
+
widgets.gauge.gauge-appearance
+ + + {{ digitalGaugeLayoutTranslationMap.get(layout) | translate }} + + + +
+
{{ 'widgets.gauge.donut-start-angle' | translate }}
+ + + +
+ +
+ + {{ 'widgets.gauge.min-and-max-value' | translate }} + +
+
widgets.gauge.min-value-short
+ + + +
widgets.gauge.max-value-short
+ + + + + + + +
+
+ +
+ + {{ 'widgets.gauge.value' | translate }} + +
+ + + + +
widget-config.decimals-suffix
+
+ + + + +
+
+ +
+ + {{ 'widgets.gauge.label' | translate }} + +
+ + + + + + + +
+
+ +
+
widgets.gauge.default-color
+ + +
+ +
+
widgets.gauge.gauge-bar-background
+ + +
+ +
+
{{ 'widgets.gauge.bar-color' | translate }}
+ + +
+ +
+
+
widget-config.card-appearance
+
+
{{ 'widget-config.background-color' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts new file mode 100644 index 0000000000..a91511ff13 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts @@ -0,0 +1,255 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { + Datasource, + datasourcesHasAggregation, + datasourcesHasOnlyComparisonAggregation, + WidgetConfig, +} from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { + getTimewindowConfig, + setTimewindowConfig +} from '@home/components/widget/config/timewindow-config-panel.component'; +import { formatValue, isUndefined } from '@core/utils'; +import { Component } from '@angular/core'; +import { + convertLevelColorsSettingsToColorProcessor, + defaultDigitalSimpleGaugeOptions, + digitalGaugeLayoutImages, + digitalGaugeLayouts, + digitalGaugeLayoutTranslations, + DigitalGaugeSettings, + DigitalGaugeType +} from '@home/components/widget/lib/digital-gauge.models'; +import { ColorSettings, ColorType } from '@shared/models/widget-settings.models'; + +@Component({ + selector: 'tb-digital-simple-gauge-basic-config', + templateUrl: './digital-simple-gauge-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) + +export class DigitalSimpleGaugeBasicConfigComponent extends BasicWidgetConfigComponent { + + public get displayTimewindowConfig(): boolean { + const datasources = this.simpleGaugeWidgetConfigForm.get('datasources').value; + return datasourcesHasAggregation(datasources); + } + + public onlyHistoryTimewindow(): boolean { + const datasources = this.simpleGaugeWidgetConfigForm.get('datasources').value; + return datasourcesHasOnlyComparisonAggregation(datasources); + } + + public get datasource(): Datasource { + const datasources: Datasource[] = this.widgetConfig.config.datasources; + if (datasources && datasources.length) { + return datasources[0]; + } else { + return null; + } + } + + digitalGaugeType = DigitalGaugeType; + digitalGaugeLayouts = digitalGaugeLayouts; + + digitalGaugeLayoutTranslationMap = digitalGaugeLayoutTranslations; + digitalGaugeLayoutImageMap = digitalGaugeLayoutImages; + + simpleGaugeWidgetConfigForm: UntypedFormGroup; + + valuePreviewFn = this._valuePreviewFn.bind(this, true); + previewFn = this._valuePreviewFn.bind(this, false); + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + protected fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.simpleGaugeWidgetConfigForm; + } + + protected setupDefaults(configData: WidgetConfigComponentData) { + super.setupDefaults(configData); + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: DigitalGaugeSettings = {...defaultDigitalSimpleGaugeOptions, ...(configData.config.settings || {})}; + + convertLevelColorsSettingsToColorProcessor(settings); + + this.simpleGaugeWidgetConfigForm = this.fb.group({ + timewindowConfig: [getTimewindowConfig(configData.config), []], + datasources: [configData.config.datasources, []], + + gaugeType: [settings.gaugeType, []], + donutStartAngle: [settings.donutStartAngle, []], + + showMinMax: [settings.showMinMax, []], + minValue: [settings.minValue, []], + maxValue: [settings.maxValue, []], + minMaxFont: [settings.minMaxFont, []], + minMaxColor: [settings.minMaxFont?.color, []], + + showValue: [settings.showValue, []], + decimals: [configData.config.decimals, []], + units: [configData.config.units, []], + valueFont: [settings.valueFont, []], + valueColor: [settings.valueFont?.color, []], + + showTitle: [settings.showTitle, []], + title: [settings.title, []], + titleFont: [settings.titleFont, []], + titleColor: [settings.titleFont?.color, []], + + defaultColor: [settings.defaultColor, []], + gaugeColor: [settings.gaugeColor, []], + barColor: [settings.barColor, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + backgroundColor: [configData.config.backgroundColor, []], + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); + this.widgetConfig.config.datasources = config.datasources; + this.widgetConfig.config.actions = config.actions; + + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + this.widgetConfig.config.backgroundColor = config.backgroundColor; + + this.widgetConfig.config.settings.gaugeType = config.gaugeType; + this.widgetConfig.config.settings.donutStartAngle = config.donutStartAngle; + + this.widgetConfig.config.settings.showMinMax = config.showMinMax; + this.widgetConfig.config.settings.minValue = config.minValue; + this.widgetConfig.config.settings.maxValue = config.maxValue; + this.widgetConfig.config.settings.minMaxFont = config.minMaxFont; + this.widgetConfig.config.settings.minMaxFont.color = config.minMaxColor; + + this.widgetConfig.config.settings.showValue = config.showValue; + this.widgetConfig.config.units = config.units; + this.widgetConfig.config.decimals = config.decimals; + this.widgetConfig.config.settings.valueFont = config.valueFont; + this.widgetConfig.config.settings.valueFont.color = config.valueColor; + + this.widgetConfig.config.settings.showTitle = config.showTitle; + this.widgetConfig.config.settings.title = config.title; + this.widgetConfig.config.settings.titleFont = config.titleFont; + this.widgetConfig.config.settings.titleFont.color = config.titleColor; + + this.widgetConfig.config.settings.defaultColor = config.defaultColor; + this.widgetConfig.config.settings.gaugeColor = config.gaugeColor; + this.widgetConfig.config.settings.barColor = config.barColor; + const barColor: ColorSettings = config.barColor; + + if (barColor.type === ColorType.range) { + this.widgetConfig.config.settings.useFixedLevelColor = true; + this.widgetConfig.config.settings.fixedLevelColors = + barColor.rangeList.advancedMode ? barColor.rangeList.rangeAdvanced : barColor.rangeList.range; + } else { + this.widgetConfig.config.settings.useFixedLevelColor = false; + } + if (barColor.gradient?.gradient?.length) { + this.widgetConfig.config.settings.levelColors = barColor.gradient.gradient; + } + + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['gaugeType', 'showValue', 'showTitle', 'showMinMax']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const isDonut = this.simpleGaugeWidgetConfigForm.get('gaugeType').value === this.digitalGaugeType.donut; + + if (isDonut) { + this.simpleGaugeWidgetConfigForm.get('donutStartAngle').enable(); + this.simpleGaugeWidgetConfigForm.get('showMinMax').disable({emitEvent: false}); + this.simpleGaugeWidgetConfigForm.get('minValue').enable(); + this.simpleGaugeWidgetConfigForm.get('maxValue').enable(); + this.simpleGaugeWidgetConfigForm.get('minMaxFont').disable(); + this.simpleGaugeWidgetConfigForm.get('minMaxColor').disable(); + } else { + this.simpleGaugeWidgetConfigForm.get('donutStartAngle').disable(); + this.simpleGaugeWidgetConfigForm.get('showMinMax').enable({emitEvent: false}); + if (this.simpleGaugeWidgetConfigForm.get('showMinMax').value) { + this.simpleGaugeWidgetConfigForm.get('minValue').enable(); + this.simpleGaugeWidgetConfigForm.get('maxValue').enable(); + this.simpleGaugeWidgetConfigForm.get('minMaxFont').enable(); + this.simpleGaugeWidgetConfigForm.get('minMaxColor').enable(); + } else { + this.simpleGaugeWidgetConfigForm.get('minValue').disable(); + this.simpleGaugeWidgetConfigForm.get('maxValue').disable(); + this.simpleGaugeWidgetConfigForm.get('minMaxFont').disable(); + this.simpleGaugeWidgetConfigForm.get('minMaxColor').disable(); + } + } + + if (this.simpleGaugeWidgetConfigForm.get('showValue').value) { + this.simpleGaugeWidgetConfigForm.get('decimals').enable(); + this.simpleGaugeWidgetConfigForm.get('units').enable(); + this.simpleGaugeWidgetConfigForm.get('valueFont').enable(); + this.simpleGaugeWidgetConfigForm.get('valueColor').enable(); + } else { + this.simpleGaugeWidgetConfigForm.get('decimals').disable(); + this.simpleGaugeWidgetConfigForm.get('units').disable(); + this.simpleGaugeWidgetConfigForm.get('valueFont').disable(); + this.simpleGaugeWidgetConfigForm.get('valueColor').disable(); + } + + if (this.simpleGaugeWidgetConfigForm.get('showTitle').value) { + this.simpleGaugeWidgetConfigForm.get('title').enable(); + this.simpleGaugeWidgetConfigForm.get('titleFont').enable(); + this.simpleGaugeWidgetConfigForm.get('titleColor').enable(); + } else { + this.simpleGaugeWidgetConfigForm.get('title').disable(); + this.simpleGaugeWidgetConfigForm.get('titleFont').disable(); + this.simpleGaugeWidgetConfigForm.get('titleColor').disable(); + } + } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.enableFullscreen = buttons.includes('fullscreen'); + } + + private _valuePreviewFn(units: boolean): string { + return formatValue(22, 0, units ? this.simpleGaugeWidgetConfigForm.get('units').value : null, true); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html index 3cbc1d4a6c..694fb9d1c5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html @@ -99,18 +99,36 @@ [autoScale]="batteryLevelWidgetConfigForm.get('autoScaleValueSize').value" [previewText]="valuePreviewFn"> - +
{{ 'widgets.battery-level.battery-level-color' | translate }}
- +
{{ 'widgets.battery-level.battery-shape-color' | translate }}
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.ts index 8244fc85db..c9a5bf176a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.ts @@ -22,6 +22,7 @@ import { BasicWidgetConfigComponent } from '@home/components/widget/config/widge import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { DataKey, + Datasource, datasourcesHasAggregation, datasourcesHasOnlyComparisonAggregation, WidgetConfig, @@ -74,6 +75,15 @@ export class BatteryLevelBasicConfigComponent extends BasicWidgetConfigComponent return [BatteryLevelLayout.vertical_divided, BatteryLevelLayout.horizontal_divided].includes(layout); } + public get datasource(): Datasource { + const datasources: Datasource[] = this.widgetConfig.config.datasources; + if (datasources && datasources.length) { + return datasources[0]; + } else { + return null; + } + } + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts index dd875834f7..4a7e019a48 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts @@ -17,33 +17,19 @@ import * as CanvasGauges from 'canvas-gauges'; import { FontStyle, FontWeight } from '@home/components/widget/lib/settings.models'; import tinycolor from 'tinycolor2'; -import { ColorFormats } from 'tinycolor2'; -import { isDefined, isDefinedAndNotNull, isString, isUndefined, padValue } from '@core/utils'; +import { isDefined, isDefinedAndNotNull, isUndefined, padValue } from '@core/utils'; +import { ColorProcessor, constantColor } from '@shared/models/widget-settings.models'; import GenericOptions = CanvasGauges.GenericOptions; import BaseGauge = CanvasGauges.BaseGauge; export type GaugeType = 'arc' | 'donut' | 'horizontalBar' | 'verticalBar'; -export interface DigitalGaugeColorRange { - pct: number; - color: ColorFormats.RGBA; - rgbString: string; -} - -export interface ColorLevelSetting { - value: number; - color: string; -} - -export type levelColors = Array; - export interface CanvasDigitalGaugeOptions extends GenericOptions { gaugeType?: GaugeType; gaugeWithScale?: number; dashThickness?: number; roundedLineCap?: boolean; gaugeColor?: string; - levelColors?: levelColors; symbol?: string; hideValue?: boolean; hideMinMax?: boolean; @@ -65,8 +51,6 @@ export interface CanvasDigitalGaugeOptions extends GenericOptions { donutStartAngle?: number; donutEndAngle?: number; - colorsRange?: DigitalGaugeColorRange[]; - neonColorsRange?: DigitalGaugeColorRange[]; neonColorTitle?: string; neonColorLabel?: string; neonColorValue?: string; @@ -83,10 +67,12 @@ export interface CanvasDigitalGaugeOptions extends GenericOptions { colorTicks?: string; tickWidth?: number; - labelTimestamp?: string + labelTimestamp?: string; unitTitle?: string; showUnitTitle?: boolean; showTimestamp?: boolean; + + barColorProcessor: ColorProcessor; } const defaultDigitalGaugeOptions: CanvasDigitalGaugeOptions = { ...GenericOptions, @@ -97,7 +83,7 @@ const defaultDigitalGaugeOptions: CanvasDigitalGaugeOptions = { ...GenericOption roundedLineCap: false, gaugeColor: '#777', - levelColors: ['blue'], + barColorProcessor: ColorProcessor.fromSettings(constantColor('blue')), symbol: '', hideValue: false, @@ -189,7 +175,7 @@ export class Drawings { options['font' + target + 'Size'] * baseSize + 'px ' + options['font' + target]; } - static normalizedValue(options: CanvasGauges.GenericOptions): {normal: number, indented: number} { + static normalizedValue(options: CanvasGauges.GenericOptions): {normal: number; indented: number} { const value = options.value; const min = options.minValue; const max = options.maxValue; @@ -244,38 +230,6 @@ export class CanvasDigitalGauge extends BaseGauge { } } - const colorsCount = options.levelColors.length; - const isColorProperty = isString(options.levelColors[0]); - const inc = colorsCount > 1 ? (1 / (colorsCount - 1)) : 1; - options.colorsRange = []; - if (options.neonGlowBrightness) { - options.neonColorsRange = []; - } - for (let i = 0; i < options.levelColors.length; i++) { - const levelColor: any = options.levelColors[i]; - if (levelColor !== null) { - let percentage: number; - if (isColorProperty) { - percentage = inc * i; - } else { - percentage = CanvasDigitalGauge.normalizeValue(levelColor.value, options.minValue, options.maxValue); - } - let tColor = tinycolor(isColorProperty ? levelColor : levelColor.color); - options.colorsRange.push({ - pct: percentage, - color: tColor.toRgb(), - rgbString: tColor.toRgbString() - }); - if (options.neonGlowBrightness) { - tColor = tinycolor(isColorProperty ? levelColor : levelColor.color).brighten(options.neonGlowBrightness); - options.neonColorsRange.push({ - pct: percentage, - color: tColor.toRgb(), - rgbString: tColor.toRgbString() - }); - } - } - } options.ticksValue = []; for (const tick of options.ticks) { if (tick !== null) { @@ -456,12 +410,12 @@ export class CanvasDigitalGauge extends BaseGauge { let color = this.contextProgressClone.currentColor; const options = this.options as CanvasDigitalGaugeOptions; if (!color) { - const progress = (Drawings.normalizedValue(options).normal - options.minValue) / - (options.maxValue - options.minValue); + options.barColorProcessor.update(options.value); + const calculateColor = tinycolor(options.barColorProcessor.color); if (options.neonGlowBrightness) { - color = getProgressColor(progress, options.neonColorsRange); + color = calculateColor.brighten(options.neonGlowBrightness).toRgbString(); } else { - color = getProgressColor(progress, options.colorsRange); + color = calculateColor.toRgbString(); } } return color; @@ -569,7 +523,7 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D, const labelHeight = determineFontHeight(options, 'Label', bd.fontSizeFactor).height; const total = valueHeight + labelHeight; bd.labelY = bd.Cy + total / 2; - bd.timeseriesLabelY = determineTimeseriesLabelY(options, bd.labelY, bd.fontSizeFactor) + bd.timeseriesLabelY = determineTimeseriesLabelY(options, bd.labelY, bd.fontSizeFactor); bd.valueY = bd.Cy - total / 2 + valueHeight / 2; } else { bd.valueY = bd.Cy; @@ -578,7 +532,7 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D, bd.titleY = bd.Cy - bd.Ro - 12 * bd.fontSizeFactor; bd.valueY = bd.Cy; bd.labelY = bd.Cy + (8 + options.fontLabelSize) * bd.fontSizeFactor; - bd.timeseriesLabelY = determineTimeseriesLabelY(options, bd.labelY, bd.fontSizeFactor) + bd.timeseriesLabelY = determineTimeseriesLabelY(options, bd.labelY, bd.fontSizeFactor); bd.minY = bd.maxY = bd.labelY; if (options.roundedLineCap) { bd.minY += bd.strokeWidth / 2; @@ -599,7 +553,7 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D, if (options.hideMinMax && !options.showUnitTitle && !options.showTimestamp) { bd.labelY = bd.barBottom; - bd.timeseriesLabelY = determineTimeseriesLabelY(options, bd.labelY, bd.fontSizeFactor) + bd.timeseriesLabelY = determineTimeseriesLabelY(options, bd.labelY, bd.fontSizeFactor); bd.barLeft = bd.origBaseX + options.fontMinMaxSize / 3 * bd.fontSizeFactor; bd.barRight = bd.origBaseX + w + /*bd.width*/ -options.fontMinMaxSize / 3 * bd.fontSizeFactor; } else { @@ -612,7 +566,7 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D, bd.barLeft = bd.minX; bd.barRight = bd.maxX; bd.labelY = bd.barBottom + (8 + options.fontLabelSize) * bd.fontSizeFactor; - bd.timeseriesLabelY = determineTimeseriesLabelY(options, bd.labelY, bd.fontSizeFactor) + bd.timeseriesLabelY = determineTimeseriesLabelY(options, bd.labelY, bd.fontSizeFactor); bd.minY = bd.maxY = bd.labelY; } } else if (options.gaugeType === 'verticalBar') { @@ -623,11 +577,11 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D, bd.barTop = bd.valueY + 8 * bd.fontSizeFactor; bd.labelY = bd.baseY + bd.height; - bd.timeseriesLabelY = determineTimeseriesLabelY(options, bd.labelY, bd.fontSizeFactor) + bd.timeseriesLabelY = determineTimeseriesLabelY(options, bd.labelY, bd.fontSizeFactor); if (options.showUnitTitle || options.showTimestamp) { bd.barBottom = bd.labelY - options.fontLabelSize * bd.fontSizeFactor; } else { - bd.barBottom = bd.labelY + bd.barBottom = bd.labelY; } bd.minX = bd.maxX = bd.baseX + bd.width / 2 + bd.strokeWidth / 2 + options.fontMinMaxSize / 3 * bd.fontSizeFactor; bd.minY = bd.barBottom; @@ -637,7 +591,7 @@ function barDimensions(context: DigitalGaugeCanvasRenderingContext2D, } if (options.dashThickness) { - let circumference; + let circumference: number; if (options.gaugeType === 'donut') { circumference = Math.PI * bd.Rm * 2; } else if (options.gaugeType === 'arc') { @@ -763,7 +717,10 @@ function drawDigitalTitle(context: DigitalGaugeCanvasRenderingContext2D, options context.restore(); } -function drawDigitalLabel(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions, text: string, nameTextY: string) { +function drawDigitalLabel(context: DigitalGaugeCanvasRenderingContext2D, + options: CanvasDigitalGaugeOptions, + text: string, + nameTextY: string) { if (!text || text === '') { return; } @@ -823,31 +780,6 @@ function drawDigitalValue(context: DigitalGaugeCanvasRenderingContext2D, options context.restore(); } -function getProgressColor(progress: number, colorsRange: DigitalGaugeColorRange[]): string { - - if (progress === 0 || colorsRange.length === 1) { - return colorsRange[0].rgbString; - } - - for (let j = 1; j < colorsRange.length; j++) { - if (progress <= colorsRange[j].pct) { - const lower = colorsRange[j - 1]; - const upper = colorsRange[j]; - const range = upper.pct - lower.pct; - const rangePct = (progress - lower.pct) / range; - const pctLower = 1 - rangePct; - const pctUpper = rangePct; - const color = tinycolor({ - r: Math.floor(lower.color.r * pctLower + upper.color.r * pctUpper), - g: Math.floor(lower.color.g * pctLower + upper.color.g * pctUpper), - b: Math.floor(lower.color.b * pctLower + upper.color.b * pctUpper) - }); - return color.toRgbString(); - } - } - return colorsRange[colorsRange.length - 1].rgbString; -} - function drawArcGlow(context: DigitalGaugeCanvasRenderingContext2D, Cx: number, Cy: number, Ri: number, Rm: number, Ro: number, color: string, progress: number, isDonut: boolean, @@ -953,12 +885,14 @@ function drawTickBar(context: DigitalGaugeCanvasRenderingContext2D, tickValues: function drawProgress(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions, progress: number) { - let neonColor; + let neonColor: string; context.save(); + options.barColorProcessor.update(options.value); + const color = tinycolor(options.barColorProcessor.color); if (options.neonGlowBrightness) { - context.currentColor = neonColor = getProgressColor(progress, options.neonColorsRange); + context.currentColor = neonColor = color.brighten(options.neonGlowBrightness).toRgbString(); } else { - context.currentColor = context.strokeStyle = getProgressColor(progress, options.colorsRange); + context.currentColor = context.strokeStyle = color.toRgbString(); } const {barLeft, barRight, barTop, baseX, width, barBottom, Cx, Cy, Rm, Ro, Ri, strokeWidth} = diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widget.models.ts index 1b280d68c1..df18b8ac49 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widget.models.ts @@ -14,34 +14,41 @@ /// limitations under the License. /// -import { - AndroidConfig, - BadgePosition, - BadgeStyle, - IosConfig, - QRCodeConfig -} from '@shared/models/mobile-app.models'; +import { BadgePosition, QRCodeConfig } from '@shared/models/mobile-app.models'; +import { BackgroundType } from '@shared/models/widget-settings.models'; +import { WidgetConfig } from '@shared/models/widget.models'; -export interface MobileAppQrCodeWidgetSettings { +export interface MobileAppQrCodeWidgetSettings extends WidgetConfig { useSystemSettings: boolean; - androidConfig: Pick; - iosConfig: Pick; qrCodeConfig: Omit; } export const mobileAppQrCodeWidgetDefaultSettings: MobileAppQrCodeWidgetSettings = { useSystemSettings: true, - androidConfig: { - enabled: true - }, - iosConfig: { - enabled: true - }, qrCodeConfig: { badgeEnabled: true, - badgeStyle: BadgeStyle.ORIGINAL, badgePosition: BadgePosition.RIGHT, qrCodeLabelEnabled: true, qrCodeLabel: 'Scan to connect or download mobile app' + }, + title: 'Download mobile app', + titleFont: { + family: 'Roboto', + size: 16, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '1.5' + }, + showTitleIcon: false, + iconSize: '40', + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts index be6be1bc1b..5b3fb52689 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts @@ -21,7 +21,8 @@ import { filterIncludingColorRanges, Font, simpleDateFormat, - sortedColorRange + sortedColorRange, + ValueSourceType } from '@shared/models/widget-settings.models'; import { LegendPosition } from '@shared/models/widget.models'; import { @@ -37,7 +38,6 @@ import { TimeSeriesChartSettings, TimeSeriesChartThreshold, timeSeriesChartThresholdDefaultSettings, - TimeSeriesChartThresholdType, TimeSeriesChartTooltipWidgetSettings, TimeSeriesChartVisualMapPiece, TimeSeriesChartXAxisSettings, @@ -50,7 +50,8 @@ import { ChartAnimationSettings, chartColorScheme, ChartFillType, - ChartLabelPosition, ChartLineType, + ChartLabelPosition, + ChartLineType, ChartShape } from '@home/components/widget/lib/chart/chart.models'; @@ -219,7 +220,7 @@ export const rangeChartDefaultSettings: RangeChartWidgetSettings = { export const rangeChartTimeSeriesSettings = (settings: RangeChartWidgetSettings, rangeItems: RangeItem[], decimals: number, units: string): DeepPartial => { let thresholds: DeepPartial[] = settings.showRangeThresholds ? getMarkPoints(rangeItems).map(item => ({ - ...{type: TimeSeriesChartThresholdType.constant, + ...{type: ValueSourceType.constant, yAxisId: 'default', units, decimals, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index e2792c1793..76a3f84899 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -26,7 +26,9 @@ import { DateFormatProcessor, DateFormatSettings, Font, - tsToFormatTimeUnit + tsToFormatTimeUnit, + ValueSourceConfig, + ValueSourceType } from '@shared/models/widget-settings.models'; import { CallbackDataParams, @@ -66,7 +68,6 @@ import { FormattedData, WidgetComparisonSettings } from '@shared/models/widget.models'; -import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { AbstractControl, ValidationErrors } from '@angular/forms'; import { MarkLine2DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel'; import { DatePipe } from '@angular/common'; @@ -88,7 +89,8 @@ import { chartColorScheme, ChartFillSettings, ChartFillType, - ChartLabelPosition, ChartLineType, + ChartLabelPosition, + ChartLineType, ChartShape, createChartTextStyle, createLinearOpacityGradient, @@ -522,23 +524,6 @@ export const timeSeriesThresholdLabelPositionTranslations = new Map( - [ - [TimeSeriesChartThresholdType.constant, 'widgets.time-series-chart.threshold.type-constant'], - [TimeSeriesChartThresholdType.latestKey, 'widgets.time-series-chart.threshold.type-latest-key'], - [TimeSeriesChartThresholdType.entity, 'widgets.time-series-chart.threshold.type-entity'] - ] -); - - export enum TimeSeriesChartStateSourceType { constant = 'constant', range = 'range' @@ -737,14 +722,7 @@ export const defaultTimeSeriesChartXAxisSettings: TimeSeriesChartXAxisSettings = export type TimeSeriesChartYAxes = {[id: TimeSeriesChartYAxisId]: TimeSeriesChartYAxisSettings}; -export interface TimeSeriesChartThreshold { - type: TimeSeriesChartThresholdType; - value?: number; - latestKey?: string; - latestKeyType?: DataKeyType.attribute | DataKeyType.timeseries; - entityAlias?: string; - entityKey?: string; - entityKeyType?: DataKeyType.attribute | DataKeyType.timeseries; +export interface TimeSeriesChartThreshold extends ValueSourceConfig { yAxisId: TimeSeriesChartYAxisId; units?: string; decimals?: number; @@ -769,17 +747,17 @@ export const timeSeriesChartThresholdValid = (threshold: TimeSeriesChartThreshol return false; } switch (threshold.type) { - case TimeSeriesChartThresholdType.constant: + case ValueSourceType.constant: if (isUndefinedOrNull(threshold.value)) { return false; } break; - case TimeSeriesChartThresholdType.latestKey: + case ValueSourceType.latestKey: if (!threshold.latestKey || !threshold.latestKeyType) { return false; } break; - case TimeSeriesChartThresholdType.entity: + case ValueSourceType.entity: if (!threshold.entityAlias || !threshold.entityKey || !threshold.entityKeyType) { return false; } @@ -799,7 +777,7 @@ export const timeSeriesChartThresholdValidator = (control: AbstractControl): Val }; export const timeSeriesChartThresholdDefaultSettings: TimeSeriesChartThreshold = { - type: TimeSeriesChartThresholdType.constant, + type: ValueSourceType.constant, yAxisId: 'default', units: null, decimals: 0, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 86cf42de0c..913c346712 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -37,7 +37,6 @@ import { TimeSeriesChartThreshold, timeSeriesChartThresholdDefaultSettings, TimeSeriesChartThresholdItem, - TimeSeriesChartThresholdType, timeSeriesChartTooltipFormatter, TimeSeriesChartTooltipTrigger, TimeSeriesChartTooltipValueFormatFunction, @@ -60,7 +59,7 @@ import { getFocusedSeriesIndex, measureAxisNameSize } from '@home/components/widget/lib/chart/echarts-widget.models'; -import { DateFormatProcessor } from '@shared/models/widget-settings.models'; +import { DateFormatProcessor, ValueSourceType } from '@shared/models/widget-settings.models'; import { formattedDataFormDatasourceData, formatValue, isDefinedAndNotNull, isEqual, mergeDeep } from '@core/utils'; import { DataKey, Datasource, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models'; import * as echarts from 'echarts/core'; @@ -255,7 +254,7 @@ export class TbTimeSeriesChart { item.latestData = latestData; } for (const item of this.thresholdItems) { - if (item.settings.type === TimeSeriesChartThresholdType.latestKey && item.latestDataKey) { + if (item.settings.type === ValueSourceType.latestKey && item.latestDataKey) { const data = this.ctx.latestData.find(d => d.dataKey === item.latestDataKey); if (data.data[0]) { item.value = parseThresholdData(data.data[0][1]); @@ -435,7 +434,7 @@ export class TbTimeSeriesChart { let latestDataKey: DataKey = null; let entityDataKey: DataKey = null; let value = null; - if (threshold.type === TimeSeriesChartThresholdType.latestKey) { + if (threshold.type === ValueSourceType.latestKey) { if (this.ctx.datasources.length) { for (const datasource of this.ctx.datasources) { latestDataKey = datasource.latestDataKeys?.find(d => @@ -450,7 +449,7 @@ export class TbTimeSeriesChart { if (!latestDataKey) { continue; } - } else if (threshold.type === TimeSeriesChartThresholdType.entity) { + } else if (threshold.type === ValueSourceType.entity) { const entityAliasId = this.ctx.aliasController.getEntityAliasId(threshold.entityAlias); if (!entityAliasId) { continue; @@ -464,15 +463,16 @@ export class TbTimeSeriesChart { }; if (datasource) { datasource.dataKeys.push(entityDataKey); + } else { + datasource = { + type: DatasourceType.entity, + name: threshold.entityAlias, + aliasName: threshold.entityAlias, + entityAliasId, + dataKeys: [entityDataKey] + }; + thresholdDatasources.push(datasource); } - datasource = { - type: DatasourceType.entity, - name: threshold.entityAlias, - aliasName: threshold.entityAlias, - entityAliasId, - dataKeys: [ entityDataKey ] - }; - thresholdDatasources.push(datasource); } else { // constant value = threshold.value; } @@ -567,7 +567,7 @@ export class TbTimeSeriesChart { let update = false; if (subscription.data) { for (const item of this.thresholdItems) { - if (item.settings.type === TimeSeriesChartThresholdType.entity) { + if (item.settings.type === ValueSourceType.entity) { const data = subscription.data.find(d => d.dataKey.settings?.thresholdItemId === item.id); if (data.data[0]) { item.value = parseThresholdData(data.data[0][1]); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.models.ts index 79976f45cf..ceea08384a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.models.ts @@ -101,10 +101,12 @@ export const countDefaultSettings = (alarmElseEntity: boolean): CountWidgetSetti ? { color: 'rgba(0, 105, 92, 1)', type: ColorType.range, - rangeList: [ - {from: 0, to: 0, color: 'rgba(0, 105, 92, 1)'}, - {from: 1, color: 'rgba(209, 39, 48, 1)'} - ], + rangeList: { + range: [ + {from: 0, to: 0, color: 'rgba(0, 105, 92, 1)'}, + {from: 1, color: 'rgba(209, 39, 48, 1)'} + ] + }, colorFunction: defaultColorFunction } : constantColor('rgba(241, 141, 23, 1)'), diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts index 26a3373cfa..886876041f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts @@ -14,10 +14,19 @@ /// limitations under the License. /// -import { JsonSettingsSchema } from '@shared/models/widget.models'; import { GaugeType } from '@home/components/widget/lib/canvas-digital-gauge'; import { AnimationRule } from '@home/components/widget/lib/analogue-gauge.models'; import { FontSettings } from '@home/components/widget/lib/settings.models'; +import { + AdvancedColorRange, + ColorSettings, + ColorType, + constantColor, + ValueSourceConfig, + ValueSourceType +} from '@shared/models/widget-settings.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { isDefinedAndNotNull } from '@core/utils'; export interface AttributeSourceProperty { valueSource: string; @@ -39,7 +48,32 @@ export interface ColorLevelSetting { export type colorLevel = Array; -export type attributesGaugeType = 'levelColors' | 'ticks'; +export enum DigitalGaugeType { + arc = 'arc', + donut = 'donut', + horizontalBar = 'horizontalBar', + verticalBar = 'verticalBar' +} + +export const digitalGaugeLayouts = Object.keys(DigitalGaugeType) as DigitalGaugeType[]; + +export const digitalGaugeLayoutTranslations = new Map( + [ + [DigitalGaugeType.arc, 'widgets.gauge.gauge-type-arc'], + [DigitalGaugeType.donut, 'widgets.gauge.gauge-type-donut'], + [DigitalGaugeType.horizontalBar, 'widgets.gauge.gauge-type-horizontal-bar'], + [DigitalGaugeType.verticalBar, 'widgets.gauge.gauge-type-vertical-bar'] + ] +); + +export const digitalGaugeLayoutImages = new Map( + [ + [DigitalGaugeType.arc, 'assets/widget/simple-gauge/arc-layout.svg'], + [DigitalGaugeType.donut, 'assets/widget/simple-gauge/donut-layout.svg'], + [DigitalGaugeType.horizontalBar, 'assets/widget/simple-gauge/horizontal-bar-layout.svg'], + [DigitalGaugeType.verticalBar, 'assets/widget/simple-gauge/vertical-bar-layout.svg'] + ] +); export interface DigitalGaugeSettings { minValue?: number; @@ -60,6 +94,9 @@ export interface DigitalGaugeSettings { gaugeWidthScale?: number; defaultColor?: string; gaugeColor?: string; + + barColor?: ColorSettings; + useFixedLevelColor?: boolean; levelColors?: colorLevel; fixedLevelColors?: FixedLevelColors[]; @@ -75,8 +112,76 @@ export interface DigitalGaugeSettings { hideValue?: boolean; hideMinMax?: boolean; showTicks?: boolean; - ticksValue?: AttributeSourceProperty[]; + ticksValue?: ValueSourceConfig[]; ticks?: number[]; colorTicks?: string; tickWidth?: number; } + +export const defaultDigitalSimpleGaugeOptions: DigitalGaugeSettings = { + gaugeType: DigitalGaugeType.donut, + timestampFormat: 'yyyy-MM-dd HH:mm:ss', +}; + +export const backwardCompatibilityFixedLevelColors = (fixedLevelColors: FixedLevelColors[]) => { + const valueSourceWithDataKey: AdvancedColorRange[] = []; + fixedLevelColors.forEach(fixedLevelColor => valueSourceWithDataKey.push({ + from: { + type: fixedLevelColor?.from?.valueSource === 'predefinedValue' ? ValueSourceType.constant : ValueSourceType.entity, + value: fixedLevelColor?.from?.value || null, + entityAlias: fixedLevelColor?.from?.entityAlias || '', + entityKey: fixedLevelColor?.from?.attribute || '', + entityKeyType: DataKeyType.attribute + }, + to: { + type: fixedLevelColor?.to?.valueSource === 'predefinedValue' ? ValueSourceType.constant : ValueSourceType.entity, + value: fixedLevelColor?.to?.value || null, + entityAlias: fixedLevelColor?.to?.entityAlias || '', + entityKey: fixedLevelColor?.to?.attribute || '', + entityKeyType: DataKeyType.attribute + }, + color: fixedLevelColor.color + }) ); + return valueSourceWithDataKey; +}; + +export const backwardCompatibilityTicks = (ticksValue: AttributeSourceProperty[] & ValueSourceConfig[]): ValueSourceConfig[] => { + const ticks: ValueSourceConfig[] = []; + if (ticksValue?.length && isDefinedAndNotNull(ticksValue[0]?.valueSource)) { + ticksValue.forEach(tick => ticks.push({ + type: tick?.valueSource === 'predefinedValue' ? ValueSourceType.constant : ValueSourceType.entity, + value: tick?.value || null, + entityAlias: tick?.entityAlias || '', + entityKey: tick?.attribute || '', + entityKeyType: DataKeyType.attribute + }) ); + } else { + return (ticksValue as ValueSourceConfig[]); + } + return ticks; +}; + +export const convertLevelColorsSettingsToColorProcessor = (settings: DigitalGaugeSettings) => { + if (!settings.barColor) { + settings.barColor = constantColor(settings.gaugeColor); + if (settings.fixedLevelColors?.length) { + settings.barColor.rangeList = { + advancedMode: settings.useFixedLevelColor, + range: null, + rangeAdvanced: backwardCompatibilityFixedLevelColors(settings.fixedLevelColors) + }; + } + if (settings.levelColors?.length) { + settings.barColor.gradient = { + advancedMode: false, + gradient: settings.levelColors as string[], + gradientAdvanced: null + }; + } + if (settings.useFixedLevelColor) { + settings.barColor.type = ColorType.range; + } else if (settings.levelColors.length) { + settings.barColor.type = ColorType.gradient; + } + } +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts index fce1b27697..9031620f84 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts @@ -17,28 +17,17 @@ import * as CanvasGauges from 'canvas-gauges'; import { WidgetContext } from '@home/models/widget-component.models'; import { - attributesGaugeType, - AttributeSourceProperty, - ColorLevelSetting, - DigitalGaugeSettings, - FixedLevelColors + convertLevelColorsSettingsToColorProcessor, + DigitalGaugeSettings } from '@home/components/widget/lib/digital-gauge.models'; import tinycolor from 'tinycolor2'; import { isDefined, isDefinedAndNotNull } from '@core/utils'; import { prepareFontSettings } from '@home/components/widget/lib/settings.models'; import { CanvasDigitalGauge, CanvasDigitalGaugeOptions } from '@home/components/widget/lib/canvas-digital-gauge'; import { DatePipe } from '@angular/common'; -import { - DataKey, - Datasource, - DatasourceData, - DatasourceType, - JsonSettingsSchema, - widgetType -} from '@shared/models/widget.models'; -import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; -import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { EMPTY, Observable } from 'rxjs'; +import { IWidgetSubscription } from '@core/api/widget-api.models'; +import { Subscription } from 'rxjs'; +import { ColorProcessor, createValueSubscription, ValueSourceType } from '@shared/models/widget-settings.models'; import GenericOptions = CanvasGauges.GenericOptions; // @dynamic @@ -71,17 +60,8 @@ export class TbCanvasDigitalGauge { this.localSettings.gaugeWidthScale = settings.gaugeWidthScale || 0.75; this.localSettings.gaugeColor = settings.gaugeColor || tinycolor(keyColor).setAlpha(0.2).toRgbString(); - this.localSettings.useFixedLevelColor = settings.useFixedLevelColor || false; - if (!settings.useFixedLevelColor) { - if (!settings.levelColors || settings.levelColors.length === 0) { - this.localSettings.levelColors = [keyColor]; - } else { - this.localSettings.levelColors = settings.levelColors.slice(); - } - } else { - this.localSettings.levelColors = [keyColor]; - this.localSettings.fixedLevelColors = settings.fixedLevelColors || []; - } + convertLevelColorsSettingsToColorProcessor(settings); + this.localSettings.barColor = settings.barColor; this.localSettings.showTicks = settings.showTicks || false; this.localSettings.ticks = []; @@ -136,12 +116,15 @@ export class TbCanvasDigitalGauge { color: keyColor }); + this.barColorProcessor = ColorProcessor.fromSettings(settings.barColor, this.ctx); + const gaugeData: CanvasDigitalGaugeOptions = { renderTo: gaugeElement, gaugeWidthScale: this.localSettings.gaugeWidthScale, gaugeColor: this.localSettings.gaugeColor, - levelColors: this.localSettings.levelColors, + + barColorProcessor: this.barColorProcessor, colorTicks: this.localSettings.colorTicks, tickWidth: this.localSettings.tickWidth, @@ -205,53 +188,11 @@ export class TbCanvasDigitalGauge { } private localSettings: DigitalGaugeSettings; - private levelColorsSourcesSubscription: IWidgetSubscription; private ticksSourcesSubscription: IWidgetSubscription; - private gauge: CanvasDigitalGauge; - - static generateDatasource(ctx: WidgetContext, datasources: Datasource[], entityAlias: string, - attribute: string, settings: any): Datasource[]{ - const entityAliasId = ctx.aliasController.getEntityAliasId(entityAlias); - if (!entityAliasId) { - throw new Error('Not valid entity aliase name ' + entityAlias); - } - - const datasource = datasources.find((datasourceIteration) => { - return datasourceIteration.entityAliasId === entityAliasId; - }); - - const dataKey: DataKey = { - type: DataKeyType.attribute, - name: attribute, - label: attribute, - settings: [settings], - _hash: Math.random() - }; - - if (datasource) { - const findDataKey = datasource.dataKeys.find((dataKeyIteration) => { - return dataKeyIteration.name === attribute; - }); - - if (findDataKey) { - findDataKey.settings.push(settings); - } else { - datasource.dataKeys.push(dataKey); - } - } else { - const datasourceAttribute: Datasource = { - type: DatasourceType.entity, - name: entityAlias, - aliasName: entityAlias, - entityAliasId, - dataKeys: [dataKey] - }; - datasources.push(datasourceAttribute); - } + private readonly barColorProcessor: ColorProcessor; - return datasources; - } + private gauge: CanvasDigitalGauge; private static toRadians(angle: number): number { return angle * (Math.PI / 180); @@ -259,123 +200,36 @@ export class TbCanvasDigitalGauge { init() { let updateSetting = false; - if (this.localSettings.useFixedLevelColor && this.localSettings.fixedLevelColors?.length > 0) { - this.localSettings.levelColors = this.settingLevelColorsSubscribe(this.localSettings.fixedLevelColors); - updateSetting = true; - } if (this.localSettings.showTicks && this.localSettings.ticksValue?.length) { - this.localSettings.ticks = this.settingTicksSubscribe(this.localSettings.ticksValue); + this.localSettings.ticks = this.localSettings.ticksValue + .map(tick => tick.type === ValueSourceType.constant && isFinite(tick.value) ? tick.value : null); + + createValueSubscription( + this.ctx, + this.localSettings.ticksValue, + this.updateAttribute.bind(this) + ).subscribe((subscription) => { + this.ticksSourcesSubscription = subscription; + }); updateSetting = true; } if (updateSetting) { this.updateSetting(); } - } - - settingLevelColorsSubscribe(options: FixedLevelColors[]): ColorLevelSetting[] { - let levelColorsDatasource: Datasource[] = []; - const predefineLevelColors: ColorLevelSetting[] = []; - - predefineLevelColors.push({ - value: this.localSettings.minValue, - color: this.localSettings.gaugeColor - }); - - function setLevelColor(levelSetting: AttributeSourceProperty, color: string) { - if (levelSetting.valueSource === 'predefinedValue' && isFinite(levelSetting.value)) { - predefineLevelColors.push({ - value: levelSetting.value, - color - }); - } else if (levelSetting.entityAlias && levelSetting.attribute) { - try { - levelColorsDatasource = TbCanvasDigitalGauge.generateDatasource(this.ctx, levelColorsDatasource, - levelSetting.entityAlias, levelSetting.attribute, {color, index: predefineLevelColors.length}); - } catch (e) { - return; - } - predefineLevelColors.push(null); - } - } - - for (const levelColor of options) { - if (levelColor.from) { - setLevelColor.call(this, levelColor.from, levelColor.color); - } - if (levelColor.to) { - setLevelColor.call(this, levelColor.to, levelColor.color); - } - } - - this.subscribeAttributes(levelColorsDatasource, 'levelColors').subscribe((subscription) => { - this.levelColorsSourcesSubscription = subscription; - }); - - return predefineLevelColors; - } - - settingTicksSubscribe(options: AttributeSourceProperty[]): number[] { - let ticksDatasource: Datasource[] = []; - const predefineTicks: number[] = []; - - for (const tick of options) { - if (tick.valueSource === 'predefinedValue' && isFinite(tick.value)) { - predefineTicks.push(tick.value); - } else if (tick.entityAlias && tick.attribute) { - try { - ticksDatasource = TbCanvasDigitalGauge - .generateDatasource(this.ctx, ticksDatasource, tick.entityAlias, tick.attribute, predefineTicks.length); - } catch (e) { - continue; - } - predefineTicks.push(null); - } - } - this.subscribeAttributes(ticksDatasource, 'ticks').subscribe((subscription) => { - this.ticksSourcesSubscription = subscription; + this.barColorProcessor.colorUpdated?.subscribe(() => { + this.gauge.update({} as CanvasDigitalGaugeOptions); }); - - return predefineTicks; } - subscribeAttributes(datasource: Datasource[], typeAttributes: attributesGaugeType): Observable { - if (!datasource.length) { - return EMPTY; - } - - const levelColorsSourcesSubscriptionOptions: WidgetSubscriptionOptions = { - datasources: datasource, - useDashboardTimewindow: false, - type: widgetType.latest, - callbacks: { - onDataUpdated: (subscription) => { - this.updateAttribute(subscription.data, typeAttributes); - } - } - }; - - return this.ctx.subscriptionApi.createSubscription(levelColorsSourcesSubscriptionOptions, true); - } - - updateAttribute(data: Array, typeAttributes: attributesGaugeType) { - for (const keyData of data) { + updateAttribute(subscription: IWidgetSubscription) { + for (const keyData of subscription.data) { if (keyData && keyData.data && keyData.data[0]) { const attrValue = keyData.data[0][1]; if (isFinite(attrValue)) { - for (const setting of keyData.dataKey.settings) { - switch (typeAttributes) { - case 'levelColors': - this.localSettings.levelColors[setting.index] = { - value: attrValue, - color: setting.color - }; - break; - case 'ticks': - this.localSettings.ticks[setting] = attrValue; - break; - } + for (const index of keyData.dataKey.settings.indexes) { + this.localSettings.ticks[index] = attrValue; } } } @@ -385,8 +239,7 @@ export class TbCanvasDigitalGauge { updateSetting() { (this.gauge.options as CanvasDigitalGaugeOptions).ticks = this.localSettings.ticks; - (this.gauge.options as CanvasDigitalGaugeOptions).levelColors = this.localSettings.levelColors; - this.gauge.options = CanvasDigitalGauge.configure(this.gauge.options); + this.gauge.options = CanvasDigitalGauge.configure(this.gauge.options as CanvasDigitalGaugeOptions); this.gauge.update({} as CanvasDigitalGaugeOptions); } @@ -396,7 +249,7 @@ export class TbCanvasDigitalGauge { if (cellData.data.length > 0) { const tvPair = cellData.data[cellData.data.length - 1]; - let timestamp; + let timestamp: number; if (this.localSettings.showTimestamp) { timestamp = tvPair[0]; const filter = this.ctx.$injector.get(DatePipe); @@ -427,6 +280,10 @@ export class TbCanvasDigitalGauge { destroy() { this.gauge.destroy(); + this.barColorProcessor.destroy(); + if (this.ticksSourcesSubscription) { + this.ctx.subscriptionApi.removeSubscription(this.ticksSourcesSubscription.id); + } this.gauge = null; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts index 0cc6d266b0..97f94675ce 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts @@ -191,16 +191,35 @@ export class BatteryLevelWidgetComponent implements OnInit, OnDestroy, AfterView this.showValue = this.settings.showValue; this.autoScaleValueSize = this.showValue && this.settings.autoScaleValueSize; this.valueStyle = textStyle(this.settings.valueFont); - this.valueColor = ColorProcessor.fromSettings(this.settings.valueColor); + this.valueColor = ColorProcessor.fromColorProcessorSettings({ + settings: this.settings.valueColor, + ctx: this.ctx, + minGradientValue: 0, + maxGradientValue: 100 + }); - this.batteryLevelColor = ColorProcessor.fromSettings(this.settings.batteryLevelColor); + this.batteryLevelColor = ColorProcessor.fromColorProcessorSettings({ + settings: this.settings.batteryLevelColor, + ctx: this.ctx, + minGradientValue: 0, + maxGradientValue: 100 + }); - this.batteryShapeColor = ColorProcessor.fromSettings(this.settings.batteryShapeColor); + this.batteryShapeColor = ColorProcessor.fromColorProcessorSettings({ + settings: this.settings.batteryShapeColor, + ctx: this.ctx, + minGradientValue: 0, + maxGradientValue: 100 + }); this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.overlayStyle = overlayStyle(this.settings.background.overlay); this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0; + + this.valueColor.colorUpdated?.subscribe(() => this.cd.markForCheck()); + this.batteryLevelColor.colorUpdated?.subscribe(() => this.cd.markForCheck()); + this.batteryShapeColor.colorUpdated?.subscribe(() => this.cd.markForCheck()); } ngAfterViewInit() { @@ -218,6 +237,10 @@ export class BatteryLevelWidgetComponent implements OnInit, OnDestroy, AfterView if (this.batteryBoxResize$) { this.batteryBoxResize$.disconnect(); } + + this.batteryLevelColor.destroy(); + this.valueColor.destroy(); + this.batteryShapeColor.destroy(); } public onInit() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.models.ts index 081aae6558..281b8fe40f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.models.ts @@ -80,21 +80,25 @@ export const batteryLevelDefaultSettings: BatteryLevelWidgetSettings = { batteryLevelColor: { color: 'rgba(224, 224, 224, 1)', type: ColorType.range, - rangeList: [ - {from: null, to: 25, color: 'rgba(227, 71, 71, 1)'}, - {from: 25, to: 50, color: 'rgba(246, 206, 67, 1)'}, - {from: 50, to: null, color: 'rgba(92, 223, 144, 1)'} - ], + rangeList: { + range: [ + {from: null, to: 25, color: 'rgba(227, 71, 71, 1)'}, + {from: 25, to: 50, color: 'rgba(246, 206, 67, 1)'}, + {from: 50, to: null, color: 'rgba(92, 223, 144, 1)'} + ] + }, colorFunction: defaultColorFunction }, batteryShapeColor: { color: 'rgba(224, 224, 224, 0.32)', type: ColorType.range, - rangeList: [ - {from: null, to: 25, color: 'rgba(227, 71, 71, 0.32)'}, - {from: 25, to: 50, color: 'rgba(246, 206, 67, 0.32)'}, - {from: 50, to: null, color: 'rgba(92, 223, 144, 0.32)'} - ], + rangeList: { + range: [ + {from: null, to: 25, color: 'rgba(227, 71, 71, 0.32)'}, + {from: 25, to: 50, color: 'rgba(246, 206, 67, 0.32)'}, + {from: 50, to: null, color: 'rgba(92, 223, 144, 0.32)'} + ] + }, colorFunction: defaultColorFunction }, background: { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.models.ts index 6b426197d0..cb626a70dc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/signal-strength-widget.models.ts @@ -84,12 +84,14 @@ export const signalStrengthDefaultSettings: SignalStrengthWidgetSettings = { activeBarsColor: { color: 'rgba(92, 223, 144, 1)', type: ColorType.range, - rangeList: [ - {to: -85, color: 'rgba(227, 71, 71, 1)'}, - {from: -85, to: -70, color: 'rgba(255, 122, 0, 1)'}, - {from: -70, to: -55, color: 'rgba(246, 206, 67, 1)'}, - {from: -55, color: 'rgba(92, 223, 144, 1)'} - ], + rangeList: { + range: [ + {to: -85, color: 'rgba(227, 71, 71, 1)'}, + {from: -85, to: -70, color: 'rgba(255, 122, 0, 1)'}, + {from: -70, to: -55, color: 'rgba(246, 206, 67, 1)'}, + {from: -55, color: 'rgba(92, 223, 144, 1)'} + ] + }, colorFunction: defaultColorFunction }, noSignalRssiValue: -100, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html index 2c2623cc0c..6dc33eb07f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html @@ -15,23 +15,24 @@ limitations under the License. --> -
-
- -
- - - - - - - - +
+
+ +
+
+
+
+ + +
+
+
+ {{ mobileAppSettings.qrCodeConfig.qrCodeLabel }}
-
-
- {{ mobileAppSettings.qrCodeConfig.qrCodeLabel }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss index ddb8870d42..b13fbe0f8f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss @@ -17,24 +17,61 @@ @import '../../../../../../scss/constants'; :host { + width: 100%; height: 100%; display: flex; - flex-direction: column; - background-color: transparent; + justify-content: center; } -.tb-qrcode-label { - font-size: 14px; - color: rgba(0, 0, 0, 0.54); +.tb-mobile-app-qrcode-panel { + width: 100%; + height: 100%; + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + gap: 8px; + padding: 16px; + &.preview { + max-width: 300px; + padding: 0; + } + > div:not(.tb-mobile-app-qrcode-overlay) { + z-index: 1; + } + .tb-mobile-app-qrcode-overlay { + position: absolute; + top: 12px; + left: 12px; + bottom: 12px; + right: 12px; + } - @media #{$mat-md-lg} { - font-size: 11px; - line-height: 16px; + div.tb-widget-title { + padding: 0; + } + + .tb-qrcode-label { + align-self: center; + text-align: center; + font-size: 14px; + color: rgba(0, 0, 0, 0.54); } -} -.tb-qrcode { - box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.08); - border-radius: 6px; + .tb-qrcode { + display: flex; + flex: 0.3; + min-width: 100px; + padding: 3px; + border-radius: 6px; + background-color: #fff; + } + + .tb-badges { + display: flex; + flex-direction: column; + flex: 0.4; + gap: 8px; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts index 0f450d47f7..6aeef19b19 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts @@ -14,19 +14,21 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; -import { BadgePosition, BadgeStyle, badgeStyleURLMap, MobileAppSettings } from '@shared/models/mobile-app.models'; +import { BadgePosition, MobileAppSettings } from '@shared/models/mobile-app.models'; import { MobileAppService } from '@core/http/mobile-app.service'; import { WidgetContext } from '@home/models/widget-component.models'; import { UtilsService } from '@core/services/utils.service'; -import { Subject } from 'rxjs'; +import { Observable, Subject } from 'rxjs'; import { MINUTE } from '@shared/models/time/time.models'; -import { MobileAppQrCodeWidgetSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widget.models'; -import { isDefinedAndNotNull } from '@core/utils'; +import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; import { ResizeObserver } from '@juggle/resize-observer'; +import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'tb-mobile-app-qrcode-widget', @@ -35,62 +37,76 @@ import { ResizeObserver } from '@juggle/resize-observer'; }) export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnInit, OnDestroy { + private readonly destroy$ = new Subject(); + private widgetResize$: ResizeObserver; + + private mobileAppSettingsValue: MobileAppSettings; + private deepLink: string; + private deepLinkTTL: number; + private deepLinkTTLTimeoutID: NodeJS.Timeout; + + previewMode = false; + + badgePosition = BadgePosition; + showBadgeContainer = true; + + backgroundStyle$: Observable; + overlayStyle: ComponentStyle = {}; + + qrCodeSVG = ''; + @Input() ctx: WidgetContext; @Input() - set mobileAppSettings(settings: MobileAppSettings | MobileAppQrCodeWidgetSettings) { + widgetTitlePanel: TemplateRef; + + @Input() + set mobileAppSettings(settings: MobileAppSettings) { if (settings) { this.mobileAppSettingsValue = settings; } }; - get mobileAppSettings(): MobileAppSettings | MobileAppQrCodeWidgetSettings { + get mobileAppSettings(): MobileAppSettings { return this.mobileAppSettingsValue; } - @ViewChild('canvas', {static: true}) canvasRef: ElementRef; - - private readonly destroy$ = new Subject(); - private widgetResize$: ResizeObserver; - - badgeStyle = BadgeStyle; - badgePosition = BadgePosition; - badgeStyleURLMap = badgeStyleURLMap; - showBadgeContainer = true; - - private mobileAppSettingsValue: MobileAppSettings | MobileAppQrCodeWidgetSettings; - private deepLinkTTL: number; - private deepLinkTTLTimeoutID: NodeJS.Timeout; - constructor(protected store: Store, protected cd: ChangeDetectorRef, private mobileAppService: MobileAppService, private utilsService: UtilsService, - private elementRef: ElementRef) { + private elementRef: ElementRef, + private imagePipe: ImagePipe, + private sanitizer: DomSanitizer,) { super(store); } ngOnInit(): void { if (!this.mobileAppSettings) { - if (isDefinedAndNotNull(this.ctx.settings.useSystemSettings) && !this.ctx.settings.useSystemSettings) { - this.mobileAppSettings = this.ctx.settings; - } else { - this.mobileAppService.getMobileAppSettings().subscribe((settings => { - this.mobileAppSettings = settings; - this.cd.markForCheck(); - })); - } + this.mobileAppService.getMobileAppSettings().subscribe((settings => { + this.mobileAppSettings = settings; + if (isDefinedAndNotNull(this.ctx.settings.useSystemSettings) && !this.ctx.settings.useSystemSettings) { + this.mobileAppSettings = mergeDeep(this.mobileAppSettings, this.ctx.settings); + } + + this.widgetResize$ = new ResizeObserver(() => { + const showHideBadgeContainer = this.elementRef.nativeElement.offsetWidth > 250; + if (showHideBadgeContainer !== this.showBadgeContainer) { + this.showBadgeContainer = showHideBadgeContainer; + this.cd.markForCheck(); + } + }); + + this.widgetResize$.observe(this.elementRef.nativeElement); + this.backgroundStyle$ = backgroundStyle(this.ctx.settings.background, this.imagePipe, this.sanitizer); + this.overlayStyle = overlayStyle(this.ctx.settings.background.overlay); + this.cd.markForCheck(); + })); + } else { + this.previewMode = true; } this.initMobileAppQRCode(); - this.widgetResize$ = new ResizeObserver(() => { - const showHideBadgeContainer = this.elementRef.nativeElement.offsetWidth > 250; - if (showHideBadgeContainer !== this.showBadgeContainer) { - this.showBadgeContainer = showHideBadgeContainer; - this.cd.markForCheck(); - } - }); - this.widgetResize$.observe(this.elementRef.nativeElement); } ngOnDestroy() { @@ -103,12 +119,22 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI clearTimeout(this.deepLinkTTLTimeoutID); } + navigateByDeepLink($event) { + if ($event) { + $event.stopPropagation(); + } + if (this.ctx.isMobile) { + window.open(this.deepLink, '_blank'); + } + } + private initMobileAppQRCode() { if (this.deepLinkTTLTimeoutID) { clearTimeout(this.deepLinkTTLTimeoutID); this.deepLinkTTLTimeoutID = null; } this.mobileAppService.getMobileAppDeepLink().subscribe(link => { + this.deepLink = link; this.deepLinkTTL = Number(this.utilsService.getQueryParam('ttl', link)) * MINUTE; this.updateQRCode(link); this.deepLinkTTLTimeoutID = setTimeout(() => this.initMobileAppQRCode(), this.deepLinkTTL); @@ -117,7 +143,10 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI private updateQRCode(link: string) { import('qrcode').then((QRCode) => { - QRCode.toCanvas(this.canvasRef.nativeElement, link, { width: 100 }); + QRCode.toString(link, (err, string) => { + this.qrCodeSVG = string; + this.cd.markForCheck(); + }) }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts index 0ed2e76687..4387bb71e5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts @@ -24,6 +24,7 @@ import { isDefined, isNumber } from '@core/utils'; import { CanvasDigitalGaugeOptions } from '@home/components/widget/lib/canvas-digital-gauge'; import tinycolor from 'tinycolor2'; import { ResizeObserver } from '@juggle/resize-observer'; +import { ColorProcessor, gradientColor } from '@shared/models/widget-settings.models'; import GenericOptions = CanvasGauges.GenericOptions; interface KnobSettings { @@ -158,17 +159,16 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { neonGlowBrightness: 0, gaugeWidthScale: 0.4, gaugeColor: 'rgba(0, 0, 0, 0)', - levelColors, minValue: this.minValue, maxValue: this.maxValue, gaugeType: 'donut', dashThickness: 2, donutStartAngle: 3 / 4 * Math.PI, donutEndAngle: 9 / 4 * Math.PI, - animation: false + animation: false, + barColorProcessor: ColorProcessor.fromSettings(gradientColor('rgba(0, 0, 0, 0)', levelColors, this.minValue, this.maxValue), this.ctx) }; - this.knob.on('click', (e) => { if (this.moving) { this.moving = false; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html index 4d930347c8..ea849e4f30 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html @@ -16,71 +16,57 @@ -->
-
+
{{ 'admin.mobile-app.use-system-settings' | translate }}
-
-
admin.mobile-app.applications
-
-
- - {{ 'admin.mobile-app.android' | translate }} - +
+
+
widget-config.card-style
+
{{ 'widgets.mobile-app-qr-code.configuration-hint' | translate }}
+
+
+ + {{ 'admin.mobile-app.badges' | translate }} + + + + + {{ badgePosition.value | translate }} + + + +
-
-
-
- - {{ 'admin.mobile-app.ios' | translate }} - -
-
-
-
-
admin.mobile-app.appearance
-
-
- - {{ 'admin.mobile-app.badges' | translate }} - - - - - {{ badgeStyle.value | translate }} - - - - - - - {{ badgePosition.value | translate }} - - - +
+
+ + {{ 'admin.mobile-app.label' | translate }} + + + + + warning + + +
-
-
- - {{ 'admin.mobile-app.label' | translate }} - - - - - warning - - +
+
+
{{ 'widgets.background.background' | translate }}
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts index bcb70518f5..d4bae4624e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts @@ -19,7 +19,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms import { WidgetSettings, WidgetSettingsComponent } from "@shared/models/widget.models"; import { AppState } from '@core/core.state'; import { Store } from "@ngrx/store"; -import { badgePositionTranslationsMap, badgeStyleTranslationsMap } from '@shared/models/mobile-app.models'; +import { badgePositionTranslationsMap } from '@shared/models/mobile-app.models'; import { mobileAppQrCodeWidgetDefaultSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widget.models'; @Component({ @@ -32,7 +32,6 @@ export class MobileAppQrCodeWidgetSettingsComponent extends WidgetSettingsCompon mobileAppQRCodeWidgetSettingsForm: UntypedFormGroup; badgePositionTranslationsMap = badgePositionTranslationsMap; - badgeStyleTranslationsMap = badgeStyleTranslationsMap; constructor(protected store: Store, private fb: UntypedFormBuilder) { @@ -50,54 +49,35 @@ export class MobileAppQrCodeWidgetSettingsComponent extends WidgetSettingsCompon protected onSettingsSet(settings: WidgetSettings) { this.mobileAppQRCodeWidgetSettingsForm = this.fb.group({ useSystemSettings: [settings.useSystemSettings], - androidConfig: this.fb.group({ - enabled: [settings.androidConfig.enabled], - }), - iosConfig: this.fb.group({ - enabled: [settings.iosConfig.enabled], - }), qrCodeConfig: this.fb.group({ badgeEnabled: [settings.qrCodeConfig.badgeEnabled], - badgeStyle: [{value: settings.qrCodeConfig.badgeStyle, disabled: true}], - badgePosition: [{value: settings.qrCodeConfig.badgePosition, disabled: true}], + badgePosition: [settings.qrCodeConfig.badgePosition], qrCodeLabelEnabled: [settings.qrCodeConfig.qrCodeLabelEnabled], - qrCodeLabel: [settings.qrCodeConfig.qrCodeLabel, [Validators.required]] - }) + qrCodeLabel: [settings.qrCodeConfig.qrCodeLabel] + }), + background: [settings.background] }); } protected validatorTriggers(): string[] { - return ['useSystemSettings', 'androidConfig.enabled', 'iosConfig.enabled', 'qrCodeConfig.badgeEnabled', 'qrCodeConfig.qrCodeLabelEnabled']; + return ['useSystemSettings', 'qrCodeConfig.badgeEnabled', 'qrCodeConfig.qrCodeLabelEnabled']; } protected updateValidators(emitEvent: boolean) { const useSystemSettings = this.mobileAppQRCodeWidgetSettingsForm.get('useSystemSettings').value; - const androidEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.enabled').value; - const iosEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.enabled').value; - const badgeEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').value; - const qrCodeLabelEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').value; - - if (!androidEnabled && !iosEnabled) { - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable({emitEvent: false}); - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); - } - - if (androidEnabled || iosEnabled) { - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false}); + if (!useSystemSettings) { + const badgeEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').value; + const qrCodeLabelEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').value; if (badgeEnabled) { - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').enable({emitEvent: false}); this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').enable({emitEvent: false}); } else { - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable({emitEvent: false}); this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); } - } - - if (qrCodeLabelEnabled && !useSystemSettings) { - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').enable({emitEvent: false}); - } else { - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').disable({emitEvent: false}); + if (qrCodeLabelEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').enable({emitEvent: false}); + } else { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').disable({emitEvent: false}); + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html index cbde25d5e8..4767f393a8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html @@ -209,7 +209,6 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html index aa497b0d3b..ef7ad17878 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html @@ -15,34 +15,22 @@ limitations under the License. --> -
- - -
-
{{ thresholdText() }}
- - - - -
-
- - -
-
widgets.chart.line-width
- - - px - -
-
-
- +
+ +
+ + + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss deleted file mode 100644 index 2ed7a91eb4..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.tb-flot-threshold { - display: flex; - flex-direction: row; - align-items: start; - gap: 4px; - .mat-expansion-panel.tb-settings.tb-flot-threshold-settings { - box-shadow: none; - border-radius: 6px; - border: 1px solid rgba(0, 0, 0, 0.12); - .mat-expansion-panel-header { - height: 56px; - border-radius: 0; - display: flex; - flex-direction: row; - align-items: stretch; - .mat-content { - overflow: hidden; - } - .tb-threshold-header { - flex: 1; - display: flex; - flex-direction: row; - gap: 16px; - align-items: center; - padding-left: 16px; - overflow: hidden; - .mat-divider-vertical { - height: 100%; - } - } - .tb-threshold-text { - flex: 1; - font-size: 16px; - font-style: normal; - font-weight: 400; - line-height: 16px; - letter-spacing: 0.15px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - .mat-expansion-indicator { - margin-right: 22px; - margin-left: 22px; - margin-top: 12px; - } - } - > .mat-expansion-panel-content { - > .mat-expansion-panel-body { - padding: 16px !important; - } - } - &.mat-expanded { - .mat-expansion-panel-header { - border-bottom: 1px solid rgba(0, 0, 0, 0.12); - } - } - } - > .mdc-icon-button { - margin-top: 4px; - color: rgba(0, 0, 0, 0.54); - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts index 1dea0cc0c6..b6b4c88962 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts @@ -26,15 +26,13 @@ import { import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { TranslateService } from '@ngx-translate/core'; -import { isNumber } from '@core/utils'; import { IAliasController } from '@core/api/widget-api.models'; import { TbFlotKeyThreshold } from '@home/components/widget/lib/flot-widget.models'; @Component({ selector: 'tb-flot-threshold', templateUrl: './flot-threshold.component.html', - styleUrls: ['./flot-threshold.component.scss'], + styleUrls: [], providers: [ { provide: NG_VALUE_ACCESSOR, @@ -49,9 +47,6 @@ export class FlotThresholdComponent extends PageComponent implements OnInit, Con @Input() disabled: boolean; - @Input() - expanded = false; - @Input() aliasController: IAliasController; @@ -65,7 +60,6 @@ export class FlotThresholdComponent extends PageComponent implements OnInit, Con public thresholdFormGroup: UntypedFormGroup; constructor(protected store: Store, - private translate: TranslateService, private fb: UntypedFormBuilder) { super(store); } @@ -110,26 +104,8 @@ export class FlotThresholdComponent extends PageComponent implements OnInit, Con ); } - thresholdText(): string { - const value: ValueSourceProperty = this.thresholdFormGroup.get('valueSource').value; - return this.valueSourcePropertyText(value); - } - - private valueSourcePropertyText(source?: ValueSourceProperty): string { - if (source) { - if (source.valueSource === 'predefinedValue') { - return `${isNumber(source.value) ? source.value : 0}`; - } else if (source.valueSource === 'entityAttribute') { - const alias = source.entityAlias || 'Undefined'; - const key = source.attribute || 'Undefined'; - return `${alias}.${key}`; - } - } - return 'Undefined'; - } - private updateModel() { - const value: {valueSource: ValueSourceProperty, lineWidth: number, color: string} = this.thresholdFormGroup.value; + const value: {valueSource: ValueSourceProperty; lineWidth: number; color: string} = this.thresholdFormGroup.value; this.modelValue = { thresholdValueSource: value?.valueSource?.valueSource, thresholdEntityAlias: value?.valueSource?.entityAlias, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.html new file mode 100644 index 0000000000..4bd50a81c6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.html @@ -0,0 +1,56 @@ + +
+
+
+
+
widgets.color.from
+ + +
+
+
widgets.color.to
+ + +
+
+ + +
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.scss similarity index 57% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.scss index c5711899ea..cf4059b0c7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.scss @@ -14,27 +14,29 @@ * limitations under the License. */ :host { - display: block; - .mat-expansion-panel { - box-shadow: none; - &.fixed-color-level { - border: 1px groove rgba(0, 0, 0, .25); - .mat-expansion-panel-header { - padding: 0 24px 0 8px; - &.mat-expanded { - height: 48px; - } - } + .range { + &-container { + display: flex; + flex: 1; + flex-direction: row; + align-items: center; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 6px; } - } -} - -:host ::ng-deep { - .mat-expansion-panel { - &.fixed-color-level { - .mat-expansion-panel-body { - padding: 0 8px 8px; + &-input { + padding: 7px; + &-fields { + display: flex; + flex-direction: column; + flex: 1; + gap: 12px; + border-right: 1px solid rgba(0, 0, 0, 0.12);; } } + &-text { + min-width: 50px; + font-size: 14px; + color: rgba(0, 0, 0, 0.38); + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.ts new file mode 100644 index 0000000000..bfb6433a82 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.ts @@ -0,0 +1,109 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { IAliasController } from '@core/api/widget-api.models'; +import { AdvancedColorRange } from '@shared/models/widget-settings.models'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { Datasource } from '@shared/models/widget.models'; + +@Component({ + selector: 'tb-advanced-range', + templateUrl: './advanced-range.component.html', + styleUrls: ['./advanced-range.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AdvancedRangeComponent), + multi: true + } + ] +}) +export class AdvancedRangeComponent extends PageComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + aliasController: IAliasController; + + @Input() + dataKeyCallbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; + + @Output() + removeAdvancedRange = new EventEmitter(); + + private modelValue: AdvancedColorRange; + + private propagateChange = (v: any) => { }; + + public advancedRangeLevelFormGroup: UntypedFormGroup; + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + ngOnInit(): void { + this.advancedRangeLevelFormGroup = this.fb.group({ + from: [null, []], + to: [null, []], + color: [null, [Validators.required]] + }); + this.advancedRangeLevelFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.advancedRangeLevelFormGroup.disable({emitEvent: false}); + } else { + this.advancedRangeLevelFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: AdvancedColorRange): void { + this.modelValue = value; + this.advancedRangeLevelFormGroup.patchValue(value, {emitEvent: false}); + } + + private updateModel() { + this.modelValue = this.advancedRangeLevelFormGroup.value; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.ts index 168284ec13..f834066a13 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.ts @@ -36,10 +36,7 @@ import { } from '@angular/forms'; import { TimeSeriesChartThreshold, - TimeSeriesChartThresholdType, - TimeSeriesChartYAxisId, - timeSeriesThresholdTypes, - timeSeriesThresholdTypeTranslations + TimeSeriesChartYAxisId } from '@home/components/widget/lib/chart/time-series-chart.models'; import { TimeSeriesChartThresholdsPanelComponent @@ -50,6 +47,11 @@ import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.comp import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { deepClone } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { + ValueSourceTypes, + ValueSourceType, + ValueSourceTypeTranslation +} from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-time-series-chart-threshold-row', @@ -70,11 +72,11 @@ export class TimeSeriesChartThresholdRowComponent implements ControlValueAccesso DatasourceType = DatasourceType; - TimeSeriesChartThresholdType = TimeSeriesChartThresholdType; + TimeSeriesChartThresholdType = ValueSourceType; - timeSeriesThresholdTypes = timeSeriesThresholdTypes; + timeSeriesThresholdTypes = ValueSourceTypes; - timeSeriesThresholdTypeTranslations = timeSeriesThresholdTypeTranslations; + timeSeriesThresholdTypeTranslations = ValueSourceTypeTranslation; get aliasController(): IAliasController { return this.thresholdsPanel.aliasController; @@ -209,12 +211,12 @@ export class TimeSeriesChartThresholdRowComponent implements ControlValueAccesso decimals: value.decimals, }, {emitEvent: false} ); - if (value.type === TimeSeriesChartThresholdType.latestKey) { + if (value.type === ValueSourceType.latestKey) { this.latestKeyFormControl.patchValue({ type: value.latestKeyType, name: value.latestKey }, {emitEvent: false}); - } else if (value.type === TimeSeriesChartThresholdType.entity) { + } else if (value.type === ValueSourceType.entity) { this.entityKeyFormControl.patchValue({ type: value.entityKeyType, name: value.entityKey @@ -227,18 +229,18 @@ export class TimeSeriesChartThresholdRowComponent implements ControlValueAccesso } private updateValidators() { - const type: TimeSeriesChartThresholdType = this.thresholdFormGroup.get('type').value; - if (type === TimeSeriesChartThresholdType.constant) { + const type: ValueSourceType = this.thresholdFormGroup.get('type').value; + if (type === ValueSourceType.constant) { this.thresholdFormGroup.get('value').enable({emitEvent: false}); this.thresholdFormGroup.get('entityAlias').disable({emitEvent: false}); this.latestKeyFormControl.disable({emitEvent: false}); this.entityKeyFormControl.disable({emitEvent: false}); - } else if (type === TimeSeriesChartThresholdType.latestKey) { + } else if (type === ValueSourceType.latestKey) { this.thresholdFormGroup.get('value').disable({emitEvent: false}); this.thresholdFormGroup.get('entityAlias').disable({emitEvent: false}); this.latestKeyFormControl.enable({emitEvent: false}); this.entityKeyFormControl.disable({emitEvent: false}); - } else if (type === TimeSeriesChartThresholdType.entity) { + } else if (type === ValueSourceType.entity) { this.thresholdFormGroup.get('value').disable({emitEvent: false}); this.thresholdFormGroup.get('entityAlias').enable({emitEvent: false}); this.latestKeyFormControl.disable({emitEvent: false}); @@ -255,11 +257,11 @@ export class TimeSeriesChartThresholdRowComponent implements ControlValueAccesso this.modelValue.lineColor = value.lineColor; this.modelValue.units = value.units; this.modelValue.decimals = value.decimals; - if (value.type === TimeSeriesChartThresholdType.latestKey) { + if (value.type === ValueSourceType.latestKey) { const latestKey: DataKey = this.latestKeyFormControl.value; this.modelValue.latestKey = latestKey?.name; this.modelValue.latestKeyType = (latestKey?.type as any); - } else if (value.type === TimeSeriesChartThresholdType.entity) { + } else if (value.type === ValueSourceType.entity) { const entityKey: DataKey = this.entityKeyFormControl.value; this.modelValue.entityKey = entityKey?.name; this.modelValue.entityKeyType = (entityKey?.type as any); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.ts index 95e3b242c3..62d2de8402 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.ts @@ -29,9 +29,9 @@ import { import { TimeSeriesChartThreshold, timeSeriesChartThresholdDefaultSettings, - TimeSeriesChartThresholdType, timeSeriesChartThresholdValid, - timeSeriesChartThresholdValidator, TimeSeriesChartYAxisId + timeSeriesChartThresholdValidator, + TimeSeriesChartYAxisId } from '@home/components/widget/lib/chart/time-series-chart.models'; import { mergeDeep } from '@core/utils'; import { IAliasController } from '@core/api/widget-api.models'; @@ -39,6 +39,7 @@ import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.comp import { DataKey, Datasource, WidgetConfig } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-time-series-chart-thresholds-panel', @@ -169,7 +170,7 @@ export class TimeSeriesChartThresholdsPanelComponent implements ControlValueAcce const result: TimeSeriesChartThreshold[] = []; const latestKeys = this.datasource?.latestDataKeys || []; for (const threshold of thresholds) { - if (threshold.type === TimeSeriesChartThresholdType.latestKey) { + if (threshold.type === ValueSourceType.latestKey) { const found = latestKeys.find(k => this.isThresholdKey(k, threshold)); if (found) { result.push(threshold); @@ -191,7 +192,7 @@ export class TimeSeriesChartThresholdsPanelComponent implements ControlValueAcce const existingThresholdKeys = latestKeys.filter(k => k.settings?.__thresholdKey === true); const foundThresholdKeys: DataKey[] = []; for (const threshold of thresholds) { - if (threshold.type === TimeSeriesChartThresholdType.latestKey) { + if (threshold.type === ValueSourceType.latestKey) { const found = existingThresholdKeys.find(k => this.isThresholdKey(k, threshold)); if (!found) { const newKey = this.dataKeyCallbacks.generateDataKey(threshold.latestKey, threshold.latestKeyType, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.html index 68030f89bf..2c0787c778 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.html @@ -15,36 +15,97 @@ limitations under the License. --> -
{{ panelTitle }}
-
-
-
-
-
widgets.color.from
- - - -
widgets.color.to
- - - - - -
- + +
+
widgets.color.settings-type
+ + + {{ 'widgets.color.basic-mode' | translate }} + + + {{ 'widgets.color.advanced-mode' | translate}} + + +
+ + +
+
{{ panelTitle }}
+
+ +
+ +
+
+
+
+
widgets.color.from
+ + + +
widgets.color.to
+ + + + + +
+ + +
+
+
+
+ + +
+
+ + + +
+
+ +
+
-
- + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.scss similarity index 61% rename from ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.scss index cafbf3b17b..63265f6dbc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.scss @@ -13,28 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + :host { - display: block; - .mat-expansion-panel { - box-shadow: none; - &.tick-value { - border: 1px groove rgba(0, 0, 0, .25); - .mat-expansion-panel-header { - padding: 0 24px 0 8px; - &.mat-expanded { - height: 48px; - } - } - } - } -} + .range { + display: flex; + flex: 1; + align-items: center; + flex-direction: row; -:host ::ng-deep { - .mat-expansion-panel { - &.tick-value { - .mat-expansion-panel-body { - padding: 0 8px 8px; - } + &-container { + display: flex; + flex: 1; + flex-direction: row; + align-items: center; + gap: 12px; + padding: 8px; + margin-right: 12px; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 6px; } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts index 287ab13ce1..cbc258caa7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts @@ -18,21 +18,44 @@ import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; import { AbstractControl, ControlValueAccessor, + FormControl, FormGroup, NG_VALUE_ACCESSOR, UntypedFormArray, UntypedFormBuilder, - UntypedFormGroup + UntypedFormGroup, + ValidationErrors } from '@angular/forms'; -import { ColorRange } from '@shared/models/widget-settings.models'; +import { + AdvancedColorRange, + ColorRange, + ColorRangeSettings, + ValueSourceType +} from '@shared/models/widget-settings.models'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { deepClone, isDefinedAndNotNull, isUndefined } from '@core/utils'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { IAliasController } from '@core/api/widget-api.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { Datasource } from '@shared/models/widget.models'; + +export function advancedRangeValidator(control: AbstractControl): ValidationErrors | null { + const range: AdvancedColorRange = control.value; + if (!range || !range.color) { + return { + advancedRange: true + }; + } + return null; +} @Component({ selector: 'tb-color-range-list', templateUrl: './color-range-list.component.html', - styleUrls: ['color-settings-panel.component.scss'], + styleUrls: ['color-settings-panel.component.scss', 'color-range-list.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, @@ -52,19 +75,34 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On @Input() panelTitle: string; + @Input() + aliasController: IAliasController; + + @Input() + dataKeyCallbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; + + @Input() + @coerceBoolean() + advancedMode = false; + modelValue: any; colorRangeListFormGroup: UntypedFormGroup; private destroy$ = new Subject(); - private propagateChange = null; + private propagateChange = (v: any) => { }; constructor(private fb: UntypedFormBuilder) {} ngOnInit(): void { this.colorRangeListFormGroup = this.fb.group({ - rangeList: this.fb.array([]) + advancedMode: [false], + range: this.fb.array([]), + rangeAdvanced: this.fb.array([]) }); this.colorRangeListFormGroup.valueChanges.pipe( @@ -89,8 +127,21 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On } writeValue(value: any): void { - if (value && value?.length) { - value.forEach((r) => this.rangeListFormArray.push(this.colorRangeControl(r), {emitEvent: false})); + if (value) { + let rangeList: ColorRangeSettings = {}; + if (isUndefined(value?.advancedMode) && value?.length) { + rangeList.advancedMode = false; + rangeList.range = value; + } else { + rangeList = deepClone(value); + } + this.colorRangeListFormGroup.get('advancedMode').patchValue(rangeList.advancedMode, {emitEvent: false}); + if (isDefinedAndNotNull(rangeList?.range)) { + rangeList.range.forEach((r) => this.rangeListFormArray.push(this.colorRangeControl(r), {emitEvent: false})); + } + if (isDefinedAndNotNull(rangeList?.rangeAdvanced)) { + rangeList.rangeAdvanced.forEach((r) => this.advancedRangeFormArray.push(this.fb.control(r), {emitEvent: false})); + } } } @@ -103,7 +154,7 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On } get rangeListFormArray(): UntypedFormArray { - return this.colorRangeListFormGroup.get('rangeList') as UntypedFormArray; + return this.colorRangeListFormGroup.get('range') as UntypedFormArray; } get rangeListFormGroups(): FormGroup[] { @@ -114,23 +165,65 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On return rangeControl; } + public trackByAdvancedRange(index: number, advancedRangeControl: AbstractControl): any { + return advancedRangeControl; + } + + public removeAdvancedRange(index: number) { + (this.colorRangeListFormGroup.get('rangeAdvanced') as UntypedFormArray).removeAt(index); + } + + get advancedRangeFormArray(): UntypedFormArray { + return this.colorRangeListFormGroup.get('rangeAdvanced') as UntypedFormArray; + } + + get advancedRangeControls(): FormControl[] { + return this.advancedRangeFormArray.controls as FormControl[]; + } + removeRange(index: number) { this.rangeListFormArray.removeAt(index); this.colorRangeListFormGroup.markAsDirty(); setTimeout(() => {this.popover?.updatePosition();}, 0); } - addRange() { - const newRange: ColorRange = { - color: 'rgba(0,0,0,0.87)' + rangeDrop(event: CdkDragDrop, range: string) { + const rangeColorsArray = this.colorRangeListFormGroup.get(range) as UntypedFormArray; + const rangeColor = rangeColorsArray.at(event.previousIndex); + rangeColorsArray.removeAt(event.previousIndex); + rangeColorsArray.insert(event.currentIndex, rangeColor); + } + + public addAdvancedRange() { + const advancedRange: AdvancedColorRange = { + from: { + type: ValueSourceType.constant + }, + to: { + type: ValueSourceType.constant + }, + color: null }; - this.rangeListFormArray.push(this.colorRangeControl(newRange)); - this.colorRangeListFormGroup.markAsDirty(); - setTimeout(() => {this.popover?.updatePosition();}, 0); + const advancedRangeColorsArray = this.colorRangeListFormGroup.get('rangeAdvanced') as UntypedFormArray; + const advancedRangeColorControl = this.fb.control(advancedRange, [advancedRangeValidator]); + advancedRangeColorsArray.push(advancedRangeColorControl); + } + + addRange() { + if (this.colorRangeListFormGroup.get('advancedMode').value) { + this.addAdvancedRange(); + } else { + const newRange: ColorRange = { + color: 'rgba(0,0,0,0.87)' + }; + this.rangeListFormArray.push(this.colorRangeControl(newRange)); + this.colorRangeListFormGroup.markAsDirty(); + setTimeout(() => {this.popover?.updatePosition();}, 0); + } } updateModel() { - this.propagateChange(this.colorRangeListFormGroup.get('rangeList').value); + this.propagateChange(this.colorRangeListFormGroup.value); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-panel.component.html index 1b9a44286b..347db57baa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-panel.component.html @@ -18,7 +18,7 @@
widgets.color.color-settings
- diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html index b85a96325e..f7df367657 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html @@ -33,10 +33,26 @@
+
+ + +
-
@@ -50,7 +66,8 @@ color="primary" type="button" [matMenuTriggerFor]="settingsSourcesMenu" [matMenuTriggerData]="{menuWidth: copySettingsButton._elementRef.nativeElement.clientWidth}"> - {{ 'widgets.color.copy-color-settings-from' | translate }} + {{ 'widgets.color.copy-color-settings-from' | translate }} + {{ 'widgets.color.copy-from' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.scss index 610832007f..1b0fda35b3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.scss @@ -16,8 +16,10 @@ @import '../../../../../../../../scss/constants'; .tb-color-settings-panel { - width: 500px; - height: 470px; + width: 700px; + max-width: 90vw; + min-height: 300px; + max-height: 90vh; display: flex; flex-direction: column; gap: 16px; @@ -31,6 +33,12 @@ letter-spacing: 0.25px; color: rgba(0, 0, 0, 0.87); } + .tb-gradient-panel { + flex: 1; + gap: 16px; + display: flex; + flex-direction: column; + } .tb-color-ranges-panel { flex: 1; min-height: 0; @@ -82,3 +90,12 @@ align-items: flex-end; } } + +.tb-advanced-range-drag { + display: flex; + flex-direction: row; + align-items: center; + tb-advanced-range { + flex: 1; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts index e96d906b45..94cb084512 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts @@ -16,7 +16,12 @@ import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; -import { ColorSettings, ColorType, colorTypeTranslations } from '@shared/models/widget-settings.models'; +import { + ColorSettings, + ColorType, + colorTypeTranslations, + defaultGradient +} from '@shared/models/widget-settings.models'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; @@ -24,6 +29,10 @@ import { AppState } from '@core/core.state'; import { deepClone } from '@core/utils'; import { WidgetService } from '@core/http/widget.service'; import { ColorSettingsComponent } from '@home/components/widget/lib/settings/common/color-settings.component'; +import { IAliasController } from '@core/api/widget-api.models'; +import { coerceBoolean, coerceNumber } from '@shared/decorators/coercion'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { Datasource } from '@shared/models/widget.models'; @Component({ selector: 'tb-color-settings-panel', @@ -46,6 +55,31 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit @Output() colorSettingsApplied = new EventEmitter(); + @Input() + aliasController: IAliasController; + + @Input() + dataKeyCallbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; + + @Input() + @coerceBoolean() + rangeAdvancedMode = false; + + @Input() + @coerceBoolean() + gradientAdvancedMode = false; + + @Input() + @coerceNumber() + minValue: number; + + @Input() + @coerceNumber() + maxValue: number; + colorType = ColorType; colorTypes = Object.keys(ColorType) as ColorType[]; @@ -67,6 +101,7 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit { type: [this.colorSettings?.type || ColorType.constant, []], color: [this.colorSettings?.color, []], + gradient: [this.colorSettings?.gradient || defaultGradient(this.minValue, this.maxValue), []], rangeList: [this.colorSettings?.rangeList, []], colorFunction: [this.colorSettings?.colorFunction, []] } @@ -77,13 +112,13 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit } copyColorSettings(comp: ColorSettingsComponent) { - const sourceSettings = deepClone(comp.modelValue); - this.colorSettings = sourceSettings; + this.colorSettings = deepClone(comp.modelValue); this.colorSettingsFormGroup.patchValue({ type: this.colorSettings.type, color: this.colorSettings.color, + gradient: this.colorSettings.gradient || null, colorFunction: this.colorSettings.colorFunction, - rangeList: this.colorSettings.rangeList || [] + rangeList: this.colorSettings.rangeList || null }, {emitEvent: false}); this.colorSettingsFormGroup.markAsDirty(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts index 912c1e30ac..11837804df 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts @@ -25,12 +25,17 @@ import { ViewContainerRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { ColorSettings, ColorType, ComponentStyle } from '@shared/models/widget-settings.models'; +import { ColorRange, ColorSettings, ColorType, ComponentStyle } from '@shared/models/widget-settings.models'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { ColorSettingsPanelComponent } from '@home/components/widget/lib/settings/common/color-settings-panel.component'; +import { IAliasController } from '@core/api/widget-api.models'; +import { deepClone, isDefinedAndNotNull } from '@core/utils'; +import { coerceBoolean, coerceNumber } from '@shared/decorators/coercion'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { Datasource } from '@shared/models/widget.models'; @Injectable() export class ColorSettingsComponentService { @@ -79,13 +84,38 @@ export class ColorSettingsComponent implements OnInit, ControlValueAccessor, OnD @Input() settingsKey: string; + @Input() + aliasController: IAliasController; + + @Input() + dataKeyCallbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; + + @Input() + @coerceBoolean() + rangeAdvancedMode = false; + + @Input() + @coerceBoolean() + gradientAdvancedMode = false; + + @Input() + @coerceNumber() + minValue: number; + + @Input() + @coerceNumber() + maxValue: number; + colorType = ColorType; modelValue: ColorSettings; colorStyle: ComponentStyle = {}; - private propagateChange = null; + private propagateChange: (v: any) => void = () => { }; constructor(private popoverService: TbPopoverService, private renderer: Renderer2, @@ -113,8 +143,14 @@ export class ColorSettingsComponent implements OnInit, ControlValueAccessor, OnD } writeValue(value: ColorSettings): void { - this.modelValue = value; - this.updateColorStyle(); + if (value) { + this.modelValue = value; + if (isDefinedAndNotNull(this.modelValue.rangeList) && !isDefinedAndNotNull(this.modelValue.rangeList?.advancedMode)) { + const range = deepClone(this.modelValue.rangeList) as ColorRange[]; + this.modelValue.rangeList = deepClone({advancedMode: false, range}); + } + this.updateColorStyle(); + } } openColorSettingsPopup($event: Event, matButton: MatButton) { @@ -127,7 +163,14 @@ export class ColorSettingsComponent implements OnInit, ControlValueAccessor, OnD } else { const ctx: any = { colorSettings: this.modelValue, - settingsComponents: this.colorSettingsComponentService.getOtherColorSettingsComponents(this) + settingsComponents: this.colorSettingsComponentService.getOtherColorSettingsComponents(this), + aliasController: this.aliasController, + dataKeyCallbacks: this.dataKeyCallbacks, + datasource: this.datasource, + rangeAdvancedMode: this.rangeAdvancedMode, + gradientAdvancedMode: this.gradientAdvancedMode, + minValue: this.minValue, + maxValue: this.maxValue }; const colorSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, this.viewContainerRef, ColorSettingsPanelComponent, 'left', true, null, @@ -147,18 +190,32 @@ export class ColorSettingsComponent implements OnInit, ControlValueAccessor, OnD private updateColorStyle() { if (!this.disabled && this.modelValue) { let colors: string[] = [this.modelValue.color]; - if (this.modelValue.type === ColorType.range && this.modelValue.rangeList?.length) { - const rangeColors = this.modelValue.rangeList.slice(0, Math.min(2, this.modelValue.rangeList.length)).map(r => r.color); + const rangeList = this.modelValue.rangeList; + if (this.modelValue.type === ColorType.range && (rangeList?.range?.length || rangeList?.rangeAdvanced?.length)) { + let rangeColors: Array; + if (rangeList?.advancedMode) { + rangeColors = rangeList.rangeAdvanced.slice(0, Math.min(2, rangeList.rangeAdvanced.length)).map(r => r.color); + } else { + rangeColors = rangeList.range.slice(0, Math.min(2, rangeList.range.length)).map(r => r.color); + } colors = colors.concat(rangeColors); + } else if (this.modelValue.type === ColorType.gradient) { + colors = this.modelValue.gradient?.advancedMode ? + this.modelValue.gradient.gradientAdvanced.map(color => color.color) : + this.modelValue.gradient.gradient; } if (colors.length === 1) { this.colorStyle = {backgroundColor: colors[0]}; } else { const gradientValues: string[] = []; - const step = 100 / colors.length; - for (let i = 0; i < colors.length; i++) { - gradientValues.push(`${colors[i]} ${step*i}%`); - gradientValues.push(`${colors[i]} ${step*(i+1)}%`); + if (this.modelValue.type === ColorType.gradient) { + gradientValues.push(...colors); + } else { + const step = 100 / colors.length; + for (let i = 0; i < colors.length; i++) { + gradientValues.push(`${colors[i]} ${step*i}%`); + gradientValues.push(`${colors[i]} ${step*(i+1)}%`); + } } this.colorStyle = {background: `linear-gradient(90deg, ${gradientValues.join(', ')})`}; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/entity-alias-input.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/entity-alias-input.component.ts index dd6d19f14e..d36a4843a1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/entity-alias-input.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/entity-alias-input.component.ts @@ -15,7 +15,6 @@ /// import { - ChangeDetectorRef, Component, ElementRef, forwardRef, @@ -36,7 +35,6 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { IAliasController } from '@core/api/widget-api.models'; import { map, mergeMap } from 'rxjs/operators'; import { Observable, of } from 'rxjs'; -import { TimeSeriesChartThresholdType } from '@home/components/widget/lib/chart/time-series-chart.models'; @Component({ selector: 'tb-entity-alias-input', @@ -78,8 +76,7 @@ export class EntityAliasInputComponent implements ControlValueAccessor, OnInit { private propagateChange = (_val: any) => {}; - constructor(private fb: UntypedFormBuilder, - private cd: ChangeDetectorRef) { + constructor(private fb: UntypedFormBuilder) { } ngOnInit() { @@ -151,6 +148,4 @@ export class EntityAliasInputComponent implements ControlValueAccessor, OnInit { const value = this.entityAliasFormControl.value; this.propagateChange(value); } - - protected readonly TimeSeriesChartThresholdType = TimeSeriesChartThresholdType; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.html new file mode 100644 index 0000000000..2296aaf4e4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.html @@ -0,0 +1,208 @@ + + + +
+
widgets.color.gradient-type
+ + + {{ 'widgets.color.basic-mode' | translate }} + + + {{ 'widgets.color.advanced-mode' | translate}} + + +
+ +
+
+
+
+ +
+ +
+
+
widgets.color.gradient-start
+
+
+
widgets.color.start-value
+ + + +
+ + +
+
+
+ +
+
+
+
+
+
widgets.color.gradient-color
+ + +
+ + +
+
+
+
+ + +
+
+
widgets.color.gradient-end
+
+
+
widgets.color.end-value
+ + + +
+ + +
+
+
+
+ +
+
+
+ widgets.color.gradient-start + widgets.color.gradient-start-min +
+
+ + + + +
+
+
+ +
+
+
+
+
+
+ widgets.color.gradient-color + widgets.color.gradient-color-min +
+
+ + + + +
+
+ + +
+
+
+
+ + +
+
+
+ widgets.color.gradient-end + widgets.color.gradient-end-min +
+
+ + + + +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.scss new file mode 100644 index 0000000000..38f320b940 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.scss @@ -0,0 +1,145 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../../../../../scss/constants"; + + +:host { + overflow: auto; + height: 100%; + max-height: 420px; + .gradient { + display: flex; + flex: 1; + align-items: center; + flex-direction: row; + + &-container { + display: flex; + flex: 1; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 12px; + padding: 8px; + margin-right: 12px; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 6px; + &-start, &-end { + margin-right: 92px; + @media #{$mat-lt-lg} { + margin-right: 52px; + } + } + + .tb-gradient-text { + width: 140px; + @media #{$mat-xs} { + width: 50px; + } + } + } + .start-gradient-container, .end-gradient-container, .list-gradient-container { + display: flex; + gap: 8px; + .gradient-text { + font-size: 14px; + color: rgba(0, 0, 0, 0.38); + } + &-advanced { + flex: 1; + @media #{$mat-xs} { + align-items: center; + } + } + } + } + + .gradient-preview { + width: 100%; + padding: 40px 12px 0; + .gradient-background { + position: relative; + height: 56px; + border-radius: 8px; + } + } + + .gradient-settings { + flex: 1; + gap: 16px; + display: flex; + flex-direction: column; + } + + .tb-add-gradient { + margin-right: 92px; + @media #{$mat-lt-lg} { + margin-right: 52px; + } + } +} +::ng-deep { + .gradient-background { + .pointer { + position: absolute; + width: 8px; + height: 60px; + border-radius: 6px; + top: -2px; + border: 2px solid white; + -webkit-box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.75); + -moz-box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.75); + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.75); + + &.start { + left: 1px; + } + + &.end { + right: 1px; + } + + &-value { + position: absolute; + top: -40px; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + justify-content: center; + width: 33px; + height: 24px; + border-radius: 4px; + background-color: rgba(0, 0, 0, 0.06); + &-text { + font-size: 14px; + font-weight: 500; + } + &:after { + content: ""; + bottom: -8px; + position: absolute; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-top: 8px solid rgba(0, 0, 0, 0.06); + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts new file mode 100644 index 0000000000..e326cff0d4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts @@ -0,0 +1,266 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormGroup, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormGroup +} from '@angular/forms'; +import { AdvancedGradient, ColorGradientSettings, ValueSourceType } from '@shared/models/widget-settings.models'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { IAliasController } from '@core/api/widget-api.models'; +import { DomSanitizer } from '@angular/platform-browser'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { isDefinedAndNotNull } from '@core/utils'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { Datasource } from '@shared/models/widget.models'; + +@Component({ + selector: 'tb-gradient', + templateUrl: './gradient.component.html', + styleUrls: ['color-settings-panel.component.scss', 'gradient.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GradientComponent), + multi: true + } + ] +}) +export class GradientComponent implements OnInit, ControlValueAccessor, OnDestroy { + + @ViewChild('gradient') gradient: ElementRef; + + @Input() + disabled: boolean; + + @Input() + popover: TbPopoverComponent; + + @Input() + panelTitle: string; + + @Input() + aliasController: IAliasController; + + @Input() + dataKeyCallbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; + + @Input() + minValue: string; + + @Input() + maxValue: string; + + @Input() + @coerceBoolean() + advancedMode = true; + + modelValue: any; + + gradientFormGroup: UntypedFormGroup; + + private destroy$ = new Subject(); + + private propagateChange = (v: any) => { }; + + constructor(private fb: UntypedFormBuilder, + private sanitizer: DomSanitizer) {} + + ngOnInit(): void { + this.gradientFormGroup = this.fb.group({ + advancedMode: [false], + gradient: this.fb.group({ + start: ['rgba(0, 255, 0, 1)'], + gradientList: this.fb.array([]), + end: ['rgba(255, 0, 0, 1)'] + }), + gradientAdvanced: this.fb.group({ + start: this.fb.group({ + source: [{type: ValueSourceType.constant}], + color: ['rgba(0, 255, 0, 1)'] + }), + gradientList: this.fb.array([]), + end: this.fb.group({ + source: [{type: ValueSourceType.constant}], + color: ['rgba(255, 0, 0, 1)'] + }) + }) + }); + + this.gradientFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(() => this.updateModel()); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: ColorGradientSettings): void { + if (isDefinedAndNotNull(value)) { + this.gradientFormGroup.get('advancedMode').patchValue(value.advancedMode, {emitEvent: false}); + if (value?.gradient?.length) { + this.gradientFormGroup.get('gradient').get('start').patchValue(value.gradient[0], {emitEvent: false}); + this.gradientFormGroup.get('gradient').get('end').patchValue(value.gradient[value.gradient.length - 1], {emitEvent: false}); + value.gradient.slice(1, -1).forEach(r => this.gradientListFormArray.push(this.colorGradientControl(r), {emitEvent: false})); + } + if (value?.gradientAdvanced?.length) { + this.gradientFormGroup.get('gradientAdvanced').get('start').patchValue(value.gradientAdvanced[0], {emitEvent: false}); + this.gradientFormGroup.get('gradientAdvanced').get('end').patchValue( + value.gradientAdvanced[value.gradientAdvanced.length - 1], {emitEvent: false} + ); + value.gradientAdvanced.slice(1, -1).forEach( + r => this.advancedGradientListFormArray.push(this.advancedGradientControl(r), {emitEvent: false}) + ); + } + } + } + + get generatePointers() { + if (this.gradientFormGroup.get('advancedMode').value) { + const shift = 100 / (this.advancedGradientListFormArray.value.length + 1); + return `
` + + this.advancedGradientListFormArray.value.map((v, i) => this.pointer(shift * (i + 1), i+1, null, true)).join('') + + `
`; + } else { + const point = (+this.maxValue - +this.minValue) / (this.gradientListFormArray.value.length + 1); + const shift = 100 / (this.gradientListFormArray.value.length + 1); + const min = isDefinedAndNotNull(this.minValue) ? this.minValue : 0; + const max = isDefinedAndNotNull(this.maxValue) ? this.maxValue : 100; + return `
${min}
` + + this.gradientListFormArray.value.map((v, i) => this.pointer(shift * (i + 1), i+1, point)).join('') + + `
${max}
`; + } + } + + pointer(shift: number, index?: number, value?: number, advanced = false) { + if (advanced) { + return `
`; + } else { + return `
` + + `
${Math.floor(+this.minValue + (value * index))}
`; + } + } + + get linearGradient() { + const gradient = this.gradientFormGroup.get('advancedMode').value ? + [this.gradientFormGroup.value.gradientAdvanced.start.color, + ...this.gradientFormGroup.value.gradientAdvanced.gradientList.map(item => item.color), + this.gradientFormGroup.value.gradientAdvanced.end.color].join(', ') : + [this.gradientFormGroup.value.gradient.start, + ...this.gradientFormGroup.value.gradient.gradientList.map(item => item.color), + this.gradientFormGroup.value.gradient.end].join(', '); + return this.sanitizer.bypassSecurityTrustStyle(`background-image: linear-gradient(90deg, ${gradient})`); + } + + private colorGradientControl(gradient: string): UntypedFormGroup { + return this.fb.group({ + color: [gradient, []] + }); + } + + get gradientListFormArray(): UntypedFormArray { + return this.gradientFormGroup.get('gradient.gradientList') as UntypedFormArray; + } + get gradientListFormGroups(): FormGroup[] { + return this.gradientListFormArray.controls as FormGroup[]; + } + + private advancedGradientControl(gradient: AdvancedGradient): UntypedFormGroup { + return this.fb.group({ + source: [gradient.source, []], + color: [gradient.color, []] + }); + } + + get advancedGradientListFormArray(): UntypedFormArray { + return this.gradientFormGroup.get('gradientAdvanced.gradientList') as UntypedFormArray; + } + get advancedGradientListFormGroups(): FormGroup[] { + return this.advancedGradientListFormArray.controls as FormGroup[]; + } + + trackByGradient(index: number, gradientControl: AbstractControl): any { + return gradientControl; + } + + removeGradient(index: number, advanced = false) { + if (advanced) { + this.advancedGradientListFormArray.removeAt(index); + } else { + this.gradientListFormArray.removeAt(index); + } + this.gradientFormGroup.markAsDirty(); + setTimeout(() => {this.popover?.updatePosition();}, 0); + } + + gradientDrop(event: CdkDragDrop, advanced = false) { + const gradientColorsArray = advanced ? this.advancedGradientListFormArray : this.gradientListFormArray; + const gradientColor = gradientColorsArray.at(event.previousIndex); + gradientColorsArray.removeAt(event.previousIndex); + gradientColorsArray.insert(event.currentIndex, gradientColor); + } + + addGradient(advanced = false) { + if (advanced) { + this.advancedGradientListFormArray.push( + this.advancedGradientControl({source: {type: ValueSourceType.constant}, color: 'rgba(0,0,0,0.87)'}) + ); + } else { + this.gradientListFormArray.push(this.colorGradientControl('rgba(0,0,0,0.87)')); + } + this.gradientFormGroup.markAsDirty(); + setTimeout(() => {this.popover?.updatePosition();}, 0); + } + + updateModel() { + this.propagateChange( + { + advancedMode: this.gradientFormGroup.value.advancedMode, + gradient: [this.gradientFormGroup.value.gradient.start, + ...this.gradientFormGroup.value.gradient.gradientList.map(item => item.color), + this.gradientFormGroup.value.gradient.end], + gradientAdvanced: [this.gradientFormGroup.value.gradientAdvanced.start, + ...this.gradientFormGroup.value.gradientAdvanced.gradientList, + this.gradientFormGroup.value.gradientAdvanced.end] + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.html new file mode 100644 index 0000000000..be6faaefaf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.html @@ -0,0 +1,77 @@ + +
+
+ + + + {{ valueSourceDataKeyTypeTranslation.get(type) | translate }} + + + + + +
+
+ + + + warning + + + + + + +
+
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgeStyle.java b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.scss similarity index 63% rename from common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgeStyle.java rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.scss index 51606a2340..5d769d867a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/BadgeStyle.java +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.scss @@ -13,12 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.mobile; - - -public enum BadgeStyle { - - ORIGINAL, - WHITE; +:host { + .tb-value-source { + display: flex; + gap: 12px; + } + .tb-source-field { + display: flex; + flex: 1; + gap: 12px; + .tb-type-field, .tb-entity-alias-field { + flex: 1; + } + } + .tb-key-value-field, .tb-constant-field { + display: flex; + flex: 1; + } + ::ng-deep { + .tb-data-key-input { + flex: 1; + } + } } + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.ts new file mode 100644 index 0000000000..eb714597b2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.ts @@ -0,0 +1,192 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { IAliasController } from '@core/api/widget-api.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { DataKey, Datasource, DatasourceType } from '@app/shared/models/widget.models'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { + ValueSourceConfig, + ValueSourceType, + ValueSourceTypes, + ValueSourceTypeTranslation +} from '@shared/models/widget-settings.models'; + +@Component({ + selector: 'tb-value-source-data-key', + templateUrl: './value-source-data-key.component.html', + styleUrls: ['value-source-data-key.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ValueSourceDataKeyComponent), + multi: true + } + ] +}) +export class ValueSourceDataKeyComponent extends PageComponent implements OnInit, ControlValueAccessor { + + dataKeyType = DataKeyType; + datasourceType = DatasourceType; + + valueSourceDataKeyType = ValueSourceType; + valueSourceDataKeyTypes = ValueSourceTypes; + valueSourceDataKeyTypeTranslation = ValueSourceTypeTranslation; + + @Input() + disabled: boolean; + + @Input() + aliasController: IAliasController; + + @Input() + dataKeyCallbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; + + valueSourceFormGroup: UntypedFormGroup; + + latestKeyFormControl: UntypedFormControl; + entityKeyFormControl: UntypedFormControl; + + private modelValue: ValueSourceConfig; + + private propagateChange = (_val: any) => {}; + + constructor(protected store: Store, + private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef) { + super(store); + } + + ngOnInit(): void { + this.valueSourceFormGroup = this.fb.group({ + type: [ValueSourceType.constant, []], + value: [null, [Validators.required]], + entityAlias: [null, [Validators.required]] + }); + this.latestKeyFormControl = this.fb.control(null, [Validators.required]); + this.entityKeyFormControl = this.fb.control(null, [Validators.required]); + this.valueSourceFormGroup.valueChanges.subscribe( + () => this.updateModel() + ); + this.latestKeyFormControl.valueChanges.subscribe( + () => this.updateModel() + ); + this.entityKeyFormControl.valueChanges.subscribe( + () => this.updateModel() + ); + this.valueSourceFormGroup.get('type').valueChanges.subscribe(() => { + this.updateValidators(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.valueSourceFormGroup.disable({emitEvent: false}); + this.latestKeyFormControl.disable({emitEvent: false}); + this.entityKeyFormControl.disable({emitEvent: false}); + } else { + this.valueSourceFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: ValueSourceConfig): void { + this.modelValue = value; + this.valueSourceFormGroup.patchValue( + { + type: value.type, + value: value.value, + entityAlias: value.entityAlias + }, {emitEvent: false} + ); + if (value.type === ValueSourceType.latestKey) { + this.latestKeyFormControl.patchValue({ + type: value.latestKeyType, + name: value.latestKey + }, {emitEvent: false}); + } else if (value.type === ValueSourceType.entity) { + this.entityKeyFormControl.patchValue({ + type: value.entityKeyType, + name: value.entityKey + }, {emitEvent: false}); + } + + this.updateValidators(); + this.cd.markForCheck(); + } + + private updateModel() { + const value: ValueSourceConfig = this.valueSourceFormGroup.value; + this.modelValue.type = value.type; + this.modelValue.value = value.value; + this.modelValue.entityAlias = value.entityAlias; + + if (value.type === ValueSourceType.latestKey) { + const latestKey: DataKey = this.latestKeyFormControl.value; + this.modelValue.latestKey = latestKey?.name; + this.modelValue.latestKeyType = (latestKey?.type as any); + } else if (value.type === ValueSourceType.entity) { + const entityKey: DataKey = this.entityKeyFormControl.value; + this.modelValue.entityKey = entityKey?.name; + this.modelValue.entityKeyType = (entityKey?.type as any); + } + this.propagateChange(this.modelValue); + } + + private updateValidators(): void { + const type: ValueSourceType = this.valueSourceFormGroup.get('type').value; + if (type === ValueSourceType.constant) { + this.valueSourceFormGroup.get('value').enable({emitEvent: false}); + this.valueSourceFormGroup.get('entityAlias').disable({emitEvent: false}); + this.latestKeyFormControl.disable({emitEvent: false}); + this.entityKeyFormControl.disable({emitEvent: false}); + } else if (type === ValueSourceType.latestKey) { + this.valueSourceFormGroup.get('value').disable({emitEvent: false}); + this.valueSourceFormGroup.get('entityAlias').disable({emitEvent: false}); + this.latestKeyFormControl.enable({emitEvent: false}); + this.entityKeyFormControl.disable({emitEvent: false}); + } else if (type === ValueSourceType.entity) { + this.valueSourceFormGroup.get('value').disable({emitEvent: false}); + this.valueSourceFormGroup.get('entityAlias').enable({emitEvent: false}); + this.latestKeyFormControl.disable({emitEvent: false}); + this.entityKeyFormControl.enable({emitEvent: false}); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html index cb5d62040d..24c8564ffb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html @@ -15,65 +15,59 @@ limitations under the License. --> - -
- - + +
+ + + {{ 'widgets.value-source.predefined-value' | translate }} - - + + {{ 'widgets.value-source.entity-attribute' | translate }} - - -
-
-
widgets.value-source.value
- - - -
-
-
widgets.value-source.source-entity-alias
- - - - - - - - - -
-
-
widgets.value-source.source-entity-attribute
- - - - - - - - - -
- + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts index 9df42f0eb8..f290a05083 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts @@ -19,7 +19,6 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFor import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { TranslateService } from '@ngx-translate/core'; import { IAliasController } from '@core/api/widget-api.models'; import { Observable, of } from 'rxjs'; import { catchError, map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators'; @@ -60,9 +59,15 @@ export class ValueSourceComponent extends PageComponent implements OnInit, Contr @Input() aliasController: IAliasController; + @Input() + entityAliasPlaceholder = 'widgets.value-source.source-entity-alias'; + + @Input() + entityAttributePlaceholder = 'widgets.value-source.source-entity-attribute'; + private modelValue: ValueSourceProperty; - private propagateChange = null; + private propagateChange = (v: any) => { }; public valueSourceFormGroup: UntypedFormGroup; @@ -78,7 +83,6 @@ export class ValueSourceComponent extends PageComponent implements OnInit, Contr private entityAliasList: Array = []; constructor(protected store: Store, - private translate: TranslateService, private entityService: EntityService, private fb: UntypedFormBuilder) { super(store); @@ -247,5 +251,4 @@ export class ValueSourceComponent extends PageComponent implements OnInit, Contr this.valueSourceFormGroup.get('attribute').updateValueAndValidity({emitEvent: false}); this.valueSourceFormGroup.get('value').updateValueAndValidity({emitEvent: false}); } - } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index bbe3dfb20c..781042a4cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -49,7 +49,8 @@ import { import { ColorRangeListComponent } from '@home/components/widget/lib/settings/common/color-range-list.component'; import { ColorRangePanelComponent } from '@home/components/widget/lib/settings/common/color-range-panel.component'; import { - ColorRangeSettingsComponent, ColorRangeSettingsComponentService + ColorRangeSettingsComponent, + ColorRangeSettingsComponentService } from '@home/components/widget/lib/settings/common/color-range-settings.component'; import { GetValueActionSettingsComponent @@ -149,6 +150,11 @@ import { StatusWidgetStateSettingsComponent } from '@home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component'; import { ChartBarSettingsComponent } from '@home/components/widget/lib/settings/common/chart/chart-bar-settings.component'; +import { AdvancedRangeComponent } from '@home/components/widget/lib/settings/common/advanced-range.component'; +import { GradientComponent } from '@home/components/widget/lib/settings/common/gradient.component'; +import { + ValueSourceDataKeyComponent +} from '@home/components/widget/lib/settings/common/value-source-data-key.component'; @NgModule({ declarations: [ @@ -167,6 +173,7 @@ import { ChartBarSettingsComponent } from '@home/components/widget/lib/settings/ BackgroundSettingsComponent, BackgroundSettingsPanelComponent, ValueSourceComponent, + ValueSourceDataKeyComponent, LegendConfigComponent, WidgetFontComponent, CountWidgetSettingsComponent, @@ -205,7 +212,9 @@ import { ChartBarSettingsComponent } from '@home/components/widget/lib/settings/ TimeSeriesChartGridSettingsComponent, StatusWidgetStateSettingsComponent, DataKeyInputComponent, - EntityAliasInputComponent + EntityAliasInputComponent, + AdvancedRangeComponent, + GradientComponent ], imports: [ CommonModule, @@ -228,6 +237,7 @@ import { ChartBarSettingsComponent } from '@home/components/widget/lib/settings/ BackgroundSettingsComponent, BackgroundSettingsPanelComponent, ValueSourceComponent, + ValueSourceDataKeyComponent, LegendConfigComponent, WidgetFontComponent, CountWidgetSettingsComponent, @@ -266,7 +276,9 @@ import { ChartBarSettingsComponent } from '@home/components/widget/lib/settings/ TimeSeriesChartGridSettingsComponent, StatusWidgetStateSettingsComponent, DataKeyInputComponent, - EntityAliasInputComponent + EntityAliasInputComponent, + AdvancedRangeComponent, + GradientComponent ], providers: [ ColorSettingsComponentService, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html index e3592e73ad..86cd0ff825 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html @@ -15,368 +15,298 @@ limitations under the License. --> -
-
- widgets.gauge.common-settings -
- - widgets.gauge.min-value - + +
+
widgets.gauge.gauge-appearance
+ + + {{ digitalGaugeLayoutTranslationMap.get(layout) | translate }} + + + +
+
{{ 'widgets.gauge.donut-start-angle' | translate }}
+ + - - widgets.gauge.max-value - +
+ +
+ + {{ 'widgets.gauge.min-and-max-value' | translate }} + +
+
widgets.gauge.min-value-short
+ + + +
widgets.gauge.max-value-short
+ + + + + + + +
+
+ +
+ + {{ 'widgets.gauge.value' | translate }} + +
+ + + + +
+
+ +
+ + {{ 'widgets.gauge.label' | translate }} + +
+ + + + + + + +
+
+ +
+
widgets.gauge.unit-title-and-timestamp-settings
+
+ + {{ 'widgets.gauge.show-unit-title' | translate }} + + + + +
+ +
+ + {{ 'widgets.gauge.show-timestamp' | translate }} + + +
+ +
+
{{ 'widgets.gauge.font' | translate }}
+
+ + + + +
+
+
+
+ +
+
widgets.gauge.bar-settings
+
+
{{ 'widgets.gauge.relative-bar-width' | translate }}
+ + -
- - widgets.gauge.gauge-type - - - {{ 'widgets.gauge.gauge-type-arc' | translate }} - - - {{ 'widgets.gauge.gauge-type-donut' | translate }} - - - {{ 'widgets.gauge.gauge-type-horizontal-bar' | translate }} - - - {{ 'widgets.gauge.gauge-type-vertical-bar' | translate }} - - - - - widgets.gauge.donut-start-angle - - - - -
-
- widgets.gauge.bar-settings - - widgets.gauge.relative-bar-width - - - - widgets.gauge.neon-glow-brightness - - -
- - widgets.gauge.stripes-thickness - +
+ +
+
{{ 'widgets.gauge.neon-glow-brightness' | translate }}
+ + + +
+ +
+
{{ 'widgets.gauge.stripes-thickness' | translate }}
+ + - +
+ +
+ {{ 'widgets.gauge.rounded-line-cap' | translate }} - -
-
- widgets.gauge.bar-color-settings - - - - {{ 'widgets.gauge.use-precise-level-color-values' | translate }} -
- widgets.gauge.bar-colors -
-
-
-
- drag_handle -
- - - -
-
-
- widgets.gauge.no-bar-colors -
-
- +
+ +
+
{{ 'widgets.gauge.default-color' | translate }}
+ + +
+ +
+
{{ 'widgets.gauge.gauge-bar-background' | translate }}
+ + +
+ +
+
{{ 'widgets.gauge.bar-color' | translate }}
+ + +
+ +
+ +
+ + + + + {{ 'widgets.gauge.ticks' | translate }} + + + + widget-config.advanced-settings + + + +
+
{{ 'widgets.gauge.tick-width-and-color' | translate }}
+
+ + + + +
-
-
- widgets.gauge.fixed-level-colors -
+
+
widgets.gauge.tick-values
-
- - + (cdkDropListDropped)="tickValueDrop($event)"> +
+ +
-
- widgets.gauge.no-bar-colors +
+ widgets.gauge.no-tick-values
-
-
-
-
-
- widgets.gauge.gauge-title-settings - - - - - {{ 'widgets.gauge.show-gauge-title' | translate }} - - - - widget-config.advanced-settings - - - -
- - widgets.gauge.gauge-title - - -
- widgets.gauge.gauge-title-font - -
-
-
-
- widgets.gauge.unit-title-and-timestamp-settings -
- - {{ 'widgets.gauge.show-unit-title' | translate }} - - - widgets.gauge.unit-title - - -
-
- - {{ 'widgets.gauge.show-timestamp' | translate }} - - - widgets.gauge.timestamp-format - - -
- - - - widget-config.advanced-settings - - - -
- widgets.gauge.label-font - -
-
-
-
-
- widgets.gauge.value-settings - - - - - {{ 'widgets.gauge.show-value' | translate }} - - - - widget-config.advanced-settings - - - -
- widgets.gauge.value-font - -
-
-
-
-
- widgets.gauge.min-max-settings - - - - - {{ 'widgets.gauge.show-min-max' | translate }} - - - - widget-config.advanced-settings - - - -
- widgets.gauge.min-max-font - -
-
-
-
-
- widgets.gauge.ticks-settings - - - - - {{ 'widgets.gauge.show-ticks' | translate }} - - - - widget-config.advanced-settings - - - -
- - widgets.gauge.tick-width - - - - -
- widgets.gauge.tick-values -
-
-
+
- - -
-
-
- widgets.gauge.no-tick-values -
-
- -
-
- - - - - -
- widgets.gauge.animation-settings +
- {{ 'widgets.gauge.enable-animation' | translate }} - + widget-config.advanced-settings -
- - widgets.gauge.animation-duration - - - - widgets.gauge.animation-rule - - - {{ 'widgets.gauge.animation-linear' | translate }} - - - {{ 'widgets.gauge.animation-quad' | translate }} - - - {{ 'widgets.gauge.animation-quint' | translate }} - - - {{ 'widgets.gauge.animation-cycle' | translate }} - - - {{ 'widgets.gauge.animation-bounce' | translate }} - - - {{ 'widgets.gauge.animation-elastic' | translate }} - - - {{ 'widgets.gauge.animation-dequad' | translate }} - - - {{ 'widgets.gauge.animation-dequint' | translate }} - - - {{ 'widgets.gauge.animation-decycle' | translate }} - - - {{ 'widgets.gauge.animation-debounce' | translate }} - - - {{ 'widgets.gauge.animation-delastic' | translate }} - - - -
+
+
widgets.gauge.animation-duration-rule
+
+ + + + + + + {{ 'widgets.gauge.animation-linear' | translate }} + + + {{ 'widgets.gauge.animation-quad' | translate }} + + + {{ 'widgets.gauge.animation-quint' | translate }} + + + {{ 'widgets.gauge.animation-cycle' | translate }} + + + {{ 'widgets.gauge.animation-bounce' | translate }} + + + {{ 'widgets.gauge.animation-elastic' | translate }} + + + {{ 'widgets.gauge.animation-dequad' | translate }} + + + {{ 'widgets.gauge.animation-dequint' | translate }} + + + {{ 'widgets.gauge.animation-decycle' | translate }} + + + {{ 'widgets.gauge.animation-debounce' | translate }} + + + {{ 'widgets.gauge.animation-delastic' | translate }} + + + +
+
-
- + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts index 77cdf367bb..9f55ad90a1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { Datasource, WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; import { Component } from '@angular/core'; import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; @@ -22,10 +22,22 @@ import { AppState } from '@core/core.state'; import { GaugeType } from '@home/components/widget/lib/canvas-digital-gauge'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { - FixedColorLevel, - fixedColorLevelValidator -} from '@home/components/widget/lib/settings/gauge/fixed-color-level.component'; -import { ValueSourceProperty } from '@home/components/widget/lib/settings/common/value-source.component'; + backwardCompatibilityFixedLevelColors, + backwardCompatibilityTicks, + digitalGaugeLayoutImages, + digitalGaugeLayouts, + digitalGaugeLayoutTranslations, + DigitalGaugeType +} from '@home/components/widget/lib/digital-gauge.models'; +import { formatValue } from '@core/utils'; +import { + ColorSettings, + ColorType, + constantColor, + simpleDateFormat, + ValueSourceConfig, + ValueSourceType +} from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-digital-gauge-widget-settings', @@ -34,8 +46,26 @@ import { ValueSourceProperty } from '@home/components/widget/lib/settings/common }) export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent { + digitalGaugeType = DigitalGaugeType; + digitalGaugeLayouts = digitalGaugeLayouts; + + digitalGaugeLayoutTranslationMap = digitalGaugeLayoutTranslations; + digitalGaugeLayoutImageMap = digitalGaugeLayoutImages; + digitalGaugeWidgetSettingsForm: UntypedFormGroup; + valuePreviewFn = this._valuePreviewFn.bind(this, true); + previewFn = this._valuePreviewFn.bind(this, false); + + public get datasource(): Datasource { + const datasources: Datasource[] = this.widgetConfig.config.datasources; + if (datasources && datasources.length) { + return datasources[0]; + } else { + return null; + } + } + constructor(protected store: Store, protected fb: UntypedFormBuilder) { super(store); @@ -107,63 +137,101 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent } protected onSettingsSet(settings: WidgetSettings) { + if (!settings.barColor) { + settings.barColor = constantColor(settings.gaugeColor); + + if (settings.fixedLevelColors.length) { + settings.barColor.rangeList = { + advancedMode: settings.useFixedLevelColor, + range: null, + rangeAdvanced: backwardCompatibilityFixedLevelColors(settings.fixedLevelColors) + }; + } + if (settings.levelColors.length) { + settings.barColor.gradient = { + advancedMode: false, + gradient: settings.levelColors, + gradientAdvanced: null + }; + } + if (settings.useFixedLevelColor) { + settings.barColor.type = ColorType.range; + } else if (settings.levelColors.length) { + settings.barColor.type = ColorType.gradient; + } + } + this.digitalGaugeWidgetSettingsForm = this.fb.group({ - // Common gauge settings - minValue: [settings.minValue, []], - maxValue: [settings.maxValue, []], gaugeType: [settings.gaugeType, []], donutStartAngle: [settings.donutStartAngle, []], - defaultColor: [settings.defaultColor, []], - - // Gauge bar settings - gaugeWidthScale: [settings.gaugeWidthScale, [Validators.min(0)]], - neonGlowBrightness: [settings.neonGlowBrightness, [Validators.min(0), Validators.max(100)]], - dashThickness: [settings.dashThickness, [Validators.min(0)]], - roundedLineCap: [settings.roundedLineCap, []], + showMinMax: [settings.showMinMax, []], + minValue: [settings.minValue, []], + maxValue: [settings.maxValue, []], + minMaxFont: [settings.minMaxFont, []], + minMaxColor: [settings.minMaxFont.color, []], - // Gauge bar colors settings - gaugeColor: [settings.gaugeColor, []], - useFixedLevelColor: [settings.useFixedLevelColor, []], - levelColors: this.prepareLevelColorFormArray(settings.levelColors), - fixedLevelColors: this.prepareFixedLevelColorFormArray(settings.fixedLevelColors), + showValue: [settings.showValue, []], + valueFont: [settings.valueFont, []], + valueColor: [settings.valueFont.color, []], - // Title settings showTitle: [settings.showTitle, []], title: [settings.title, []], titleFont: [settings.titleFont, []], + titleColor: [settings.titleFont.color, []], - // Unit title/timestamp settings showUnitTitle: [settings.showUnitTitle, []], unitTitle: [settings.unitTitle, []], showTimestamp: [settings.showTimestamp, []], - timestampFormat: [settings.timestampFormat, []], + timestampFormat: [simpleDateFormat(settings.timestampFormat), []], labelFont: [settings.labelFont, []], + labelColor: [settings.labelFont.color, []], - // Value settings - showValue: [settings.showValue, []], - valueFont: [settings.valueFont, []], + gaugeWidthScale: [settings.gaugeWidthScale, [Validators.min(0)]], + neonGlowBrightness: [settings.neonGlowBrightness, [Validators.min(0), Validators.max(100)]], + dashThickness: [settings.dashThickness, [Validators.min(0)]], + roundedLineCap: [settings.roundedLineCap, []], - // Min/max labels settings - showMinMax: [settings.showMinMax, []], - minMaxFont: [settings.minMaxFont, []], + defaultColor: [settings.defaultColor, []], + gaugeColor: [settings.gaugeColor, []], + barColor: [settings.barColor], - // Ticks settings showTicks: [settings.showTicks, []], tickWidth: [settings.tickWidth, [Validators.min(0)]], colorTicks: [settings.colorTicks, []], - ticksValue: this.prepareTicksValueFormArray(settings.ticksValue), + ticksValue: this.prepareTicksValueFormArray(backwardCompatibilityTicks(settings.ticksValue)), - // Animation settings animation: [settings.animation, []], animationDuration: [settings.animationDuration, [Validators.min(0)]], animationRule: [settings.animationRule, []] - }); } + protected prepareOutputSettings(settings) { + + const barColor: ColorSettings = this.digitalGaugeWidgetSettingsForm.get('barColor').value; + + if (barColor.type === ColorType.range) { + settings.useFixedLevelColor = true; + settings.fixedLevelColors = barColor.rangeList.advancedMode ? barColor.rangeList.rangeAdvanced : barColor.rangeList.range; + } else { + settings.useFixedLevelColor = false; + } + if (barColor.gradient?.gradient?.length) { + settings.levelColors = barColor.gradient.gradient; + } + settings.barColor = this.digitalGaugeWidgetSettingsForm.get('barColor').value; + settings.timestampFormat = this.digitalGaugeWidgetSettingsForm.get('timestampFormat').value.format; + settings.minMaxFont.color = this.digitalGaugeWidgetSettingsForm.get('minMaxColor').value; + settings.valueFont.color = this.digitalGaugeWidgetSettingsForm.get('valueColor').value; + settings.titleFont.color = this.digitalGaugeWidgetSettingsForm.get('titleColor').value; + settings.labelFont.color = this.digitalGaugeWidgetSettingsForm.get('labelColor').value; + + return settings; + } + protected validatorTriggers(): string[] { - return ['gaugeType', 'showTitle', 'showUnitTitle', 'showValue', 'showMinMax', 'showTimestamp', 'useFixedLevelColor', 'showTicks', 'animation']; + return ['gaugeType', 'showTitle', 'showUnitTitle', 'showValue', 'showMinMax', 'showTimestamp', 'showTicks', 'animation']; } protected updateValidators(emitEvent: boolean) { @@ -173,21 +241,43 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent const showValue: boolean = this.digitalGaugeWidgetSettingsForm.get('showValue').value; const showMinMax: boolean = this.digitalGaugeWidgetSettingsForm.get('showMinMax').value; const showTimestamp: boolean = this.digitalGaugeWidgetSettingsForm.get('showTimestamp').value; - const useFixedLevelColor: boolean = this.digitalGaugeWidgetSettingsForm.get('useFixedLevelColor').value; const showTicks: boolean = this.digitalGaugeWidgetSettingsForm.get('showTicks').value; const animation: boolean = this.digitalGaugeWidgetSettingsForm.get('animation').value; + if (gaugeType === 'donut') { this.digitalGaugeWidgetSettingsForm.get('donutStartAngle').enable(); + + this.digitalGaugeWidgetSettingsForm.get('showMinMax').disable({emitEvent: false}); + this.digitalGaugeWidgetSettingsForm.get('minValue').enable({emitEvent: false}); + this.digitalGaugeWidgetSettingsForm.get('maxValue').enable({emitEvent: false}); + this.digitalGaugeWidgetSettingsForm.get('minMaxFont').disable({emitEvent: false}); + this.digitalGaugeWidgetSettingsForm.get('minMaxColor').disable({emitEvent: false}); } else { this.digitalGaugeWidgetSettingsForm.get('donutStartAngle').disable(); + + this.digitalGaugeWidgetSettingsForm.get('showMinMax').enable({emitEvent: false}); + if (showMinMax) { + this.digitalGaugeWidgetSettingsForm.get('minValue').enable(); + this.digitalGaugeWidgetSettingsForm.get('maxValue').enable(); + this.digitalGaugeWidgetSettingsForm.get('minMaxFont').enable(); + this.digitalGaugeWidgetSettingsForm.get('minMaxColor').enable(); + } else { + this.digitalGaugeWidgetSettingsForm.get('minValue').disable(); + this.digitalGaugeWidgetSettingsForm.get('maxValue').disable(); + this.digitalGaugeWidgetSettingsForm.get('minMaxFont').disable(); + this.digitalGaugeWidgetSettingsForm.get('minMaxColor').disable(); + } } + if (showTitle) { this.digitalGaugeWidgetSettingsForm.get('title').enable(); this.digitalGaugeWidgetSettingsForm.get('titleFont').enable(); + this.digitalGaugeWidgetSettingsForm.get('titleColor').enable(); } else { this.digitalGaugeWidgetSettingsForm.get('title').disable(); this.digitalGaugeWidgetSettingsForm.get('titleFont').disable(); + this.digitalGaugeWidgetSettingsForm.get('titleColor').disable(); } if (showUnitTitle) { this.digitalGaugeWidgetSettingsForm.get('unitTitle').enable(); @@ -201,25 +291,17 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent } if (showUnitTitle || showTimestamp) { this.digitalGaugeWidgetSettingsForm.get('labelFont').enable(); + this.digitalGaugeWidgetSettingsForm.get('labelColor').enable(); } else { this.digitalGaugeWidgetSettingsForm.get('labelFont').disable(); + this.digitalGaugeWidgetSettingsForm.get('labelColor').disable(); } if (showValue) { this.digitalGaugeWidgetSettingsForm.get('valueFont').enable(); + this.digitalGaugeWidgetSettingsForm.get('valueColor').enable(); } else { this.digitalGaugeWidgetSettingsForm.get('valueFont').disable(); - } - if (showMinMax) { - this.digitalGaugeWidgetSettingsForm.get('minMaxFont').enable(); - } else { - this.digitalGaugeWidgetSettingsForm.get('minMaxFont').disable(); - } - if (useFixedLevelColor) { - this.digitalGaugeWidgetSettingsForm.get('fixedLevelColors').enable(); - this.digitalGaugeWidgetSettingsForm.get('levelColors').disable(); - } else { - this.digitalGaugeWidgetSettingsForm.get('fixedLevelColors').disable(); - this.digitalGaugeWidgetSettingsForm.get('levelColors').enable(); + this.digitalGaugeWidgetSettingsForm.get('valueColor').disable(); } if (showTicks) { this.digitalGaugeWidgetSettingsForm.get('tickWidth').enable(); @@ -240,13 +322,21 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent this.digitalGaugeWidgetSettingsForm.get('donutStartAngle').updateValueAndValidity({emitEvent}); this.digitalGaugeWidgetSettingsForm.get('title').updateValueAndValidity({emitEvent}); this.digitalGaugeWidgetSettingsForm.get('titleFont').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('titleColor').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('unitTitle').updateValueAndValidity({emitEvent}); this.digitalGaugeWidgetSettingsForm.get('timestampFormat').updateValueAndValidity({emitEvent}); this.digitalGaugeWidgetSettingsForm.get('labelFont').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('labelColor').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('valueFont').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('valueColor').updateValueAndValidity({emitEvent}); + + this.digitalGaugeWidgetSettingsForm.get('minValue').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('maxValue').updateValueAndValidity({emitEvent}); this.digitalGaugeWidgetSettingsForm.get('minMaxFont').updateValueAndValidity({emitEvent}); - this.digitalGaugeWidgetSettingsForm.get('fixedLevelColors').updateValueAndValidity({emitEvent}); - this.digitalGaugeWidgetSettingsForm.get('levelColors').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('minMaxColor').updateValueAndValidity({emitEvent}); + this.digitalGaugeWidgetSettingsForm.get('tickWidth').updateValueAndValidity({emitEvent}); this.digitalGaugeWidgetSettingsForm.get('colorTicks').updateValueAndValidity({emitEvent}); this.digitalGaugeWidgetSettingsForm.get('ticksValue').updateValueAndValidity({emitEvent}); @@ -255,32 +345,10 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent } protected doUpdateSettings(settingsForm: UntypedFormGroup, settings: WidgetSettings) { - settingsForm.setControl('levelColors', this.prepareLevelColorFormArray(settings.levelColors), {emitEvent: false}); - settingsForm.setControl('fixedLevelColors', this.prepareFixedLevelColorFormArray(settings.fixedLevelColors), {emitEvent: false}); settingsForm.setControl('ticksValue', this.prepareTicksValueFormArray(settings.ticksValue), {emitEvent: false}); } - private prepareLevelColorFormArray(levelColors: string[] | undefined): UntypedFormArray { - const levelColorsControls: Array = []; - if (levelColors) { - levelColors.forEach((levelColor) => { - levelColorsControls.push(this.fb.control(levelColor, [Validators.required])); - }); - } - return this.fb.array(levelColorsControls); - } - - private prepareFixedLevelColorFormArray(fixedLevelColors: FixedColorLevel[] | undefined): UntypedFormArray { - const fixedLevelColorsControls: Array = []; - if (fixedLevelColors) { - fixedLevelColors.forEach((fixedLevelColor) => { - fixedLevelColorsControls.push(this.fb.control(fixedLevelColor, [fixedColorLevelValidator])); - }); - } - return this.fb.array(fixedLevelColorsControls); - } - - private prepareTicksValueFormArray(ticksValue: ValueSourceProperty[] | undefined): UntypedFormArray { + private prepareTicksValueFormArray(ticksValue: ValueSourceConfig[] | undefined): UntypedFormArray { const ticksValueControls: Array = []; if (ticksValue) { ticksValue.forEach((tickValue) => { @@ -290,71 +358,6 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent return this.fb.array(ticksValueControls); } - levelColorsFormArray(): UntypedFormArray { - return this.digitalGaugeWidgetSettingsForm.get('levelColors') as UntypedFormArray; - } - - public trackByLevelColor(index: number, levelColorControl: AbstractControl): any { - return levelColorControl; - } - - public removeLevelColor(index: number) { - (this.digitalGaugeWidgetSettingsForm.get('levelColors') as UntypedFormArray).removeAt(index); - } - - public addLevelColor() { - const levelColorsArray = this.digitalGaugeWidgetSettingsForm.get('levelColors') as UntypedFormArray; - const levelColorControl = this.fb.control(null, []); - levelColorsArray.push(levelColorControl); - this.digitalGaugeWidgetSettingsForm.updateValueAndValidity(); - } - - levelColorDrop(event: CdkDragDrop) { - const levelColorsArray = this.digitalGaugeWidgetSettingsForm.get('levelColors') as UntypedFormArray; - const levelColor = levelColorsArray.at(event.previousIndex); - levelColorsArray.removeAt(event.previousIndex); - levelColorsArray.insert(event.currentIndex, levelColor); - } - - fixedLevelColorFormArray(): UntypedFormArray { - return this.digitalGaugeWidgetSettingsForm.get('fixedLevelColors') as UntypedFormArray; - } - - public trackByFixedLevelColor(index: number, fixedLevelColorControl: AbstractControl): any { - return fixedLevelColorControl; - } - - public removeFixedLevelColor(index: number) { - (this.digitalGaugeWidgetSettingsForm.get('fixedLevelColors') as UntypedFormArray).removeAt(index); - } - - public addFixedLevelColor() { - const fixedLevelColor: FixedColorLevel = { - from: { - valueSource: 'predefinedValue' - }, - to: { - valueSource: 'predefinedValue' - }, - color: null - }; - const fixedLevelColorsArray = this.digitalGaugeWidgetSettingsForm.get('fixedLevelColors') as UntypedFormArray; - const fixedLevelColorControl = this.fb.control(fixedLevelColor, [fixedColorLevelValidator]); - (fixedLevelColorControl as any).new = true; - fixedLevelColorsArray.push(fixedLevelColorControl); - this.digitalGaugeWidgetSettingsForm.updateValueAndValidity(); - if (!this.digitalGaugeWidgetSettingsForm.valid) { - this.onSettingsChanged(this.digitalGaugeWidgetSettingsForm.value); - } - } - - fixedLevelColorDrop(event: CdkDragDrop) { - const fixedLevelColorsArray = this.digitalGaugeWidgetSettingsForm.get('fixedLevelColors') as UntypedFormArray; - const fixedLevelColor = fixedLevelColorsArray.at(event.previousIndex); - fixedLevelColorsArray.removeAt(event.previousIndex); - fixedLevelColorsArray.insert(event.currentIndex, fixedLevelColor); - } - tickValuesFormArray(): UntypedFormArray { return this.digitalGaugeWidgetSettingsForm.get('ticksValue') as UntypedFormArray; } @@ -368,8 +371,8 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent } public addTickValue() { - const tickValue: ValueSourceProperty = { - valueSource: 'predefinedValue' + const tickValue: ValueSourceConfig = { + type: ValueSourceType.constant }; const tickValuesArray = this.digitalGaugeWidgetSettingsForm.get('ticksValue') as UntypedFormArray; const tickValueControl = this.fb.control(tickValue, []); @@ -385,4 +388,7 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent tickValuesArray.insert(event.currentIndex, tickValue); } + private _valuePreviewFn(units: boolean): string { + return formatValue(22, 0, units ? this.widget.config.units : null, true); + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.html deleted file mode 100644 index 30008112ba..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.html +++ /dev/null @@ -1,60 +0,0 @@ - - - -
- -
-
{{ fixedColorLevelRangeText() }}
-
-
-
-
-
- - -
-
- -
- -
-
- widgets.gauge.from - -
-
- widgets.gauge.to - -
- - -
-
-
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.ts deleted file mode 100644 index b1464e7e8a..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.ts +++ /dev/null @@ -1,147 +0,0 @@ -/// -/// Copyright © 2016-2024 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { ValueSourceProperty } from '@home/components/widget/lib/settings/common/value-source.component'; -import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; -import { - AbstractControl, - ControlValueAccessor, - UntypedFormBuilder, - UntypedFormGroup, - NG_VALUE_ACCESSOR, ValidationErrors, - Validators -} from '@angular/forms'; -import { PageComponent } from '@shared/components/page.component'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { TranslateService } from '@ngx-translate/core'; -import { isNumber } from '@core/utils'; -import { IAliasController } from '@core/api/widget-api.models'; - -export interface FixedColorLevel { - from?: ValueSourceProperty; - to?: ValueSourceProperty; - color: string; -} - -export function fixedColorLevelValidator(control: AbstractControl): ValidationErrors | null { - const fixedColorLevel: FixedColorLevel = control.value; - if (!fixedColorLevel || !fixedColorLevel.color) { - return { - fixedColorLevel: true - }; - } - return null; -} - -@Component({ - selector: 'tb-fixed-color-level', - templateUrl: './fixed-color-level.component.html', - styleUrls: ['./fixed-color-level.component.scss'], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => FixedColorLevelComponent), - multi: true - } - ] -}) -export class FixedColorLevelComponent extends PageComponent implements OnInit, ControlValueAccessor { - - @Input() - disabled: boolean; - - @Input() - expanded = false; - - @Input() - aliasController: IAliasController; - - @Output() - removeFixedColorLevel = new EventEmitter(); - - private modelValue: FixedColorLevel; - - private propagateChange = null; - - public fixedColorLevelFormGroup: UntypedFormGroup; - - constructor(protected store: Store, - private translate: TranslateService, - private fb: UntypedFormBuilder) { - super(store); - } - - ngOnInit(): void { - this.fixedColorLevelFormGroup = this.fb.group({ - from: [null, []], - to: [null, []], - color: [null, [Validators.required]] - }); - this.fixedColorLevelFormGroup.valueChanges.subscribe(() => { - this.updateModel(); - }); - } - - registerOnChange(fn: any): void { - this.propagateChange = fn; - } - - registerOnTouched(fn: any): void { - } - - setDisabledState(isDisabled: boolean): void { - this.disabled = isDisabled; - if (isDisabled) { - this.fixedColorLevelFormGroup.disable({emitEvent: false}); - } else { - this.fixedColorLevelFormGroup.enable({emitEvent: false}); - } - } - - writeValue(value: FixedColorLevel): void { - this.modelValue = value; - this.fixedColorLevelFormGroup.patchValue( - value, {emitEvent: false} - ); - } - - fixedColorLevelRangeText(): string { - const value: FixedColorLevel = this.fixedColorLevelFormGroup.value; - const from = this.valueSourcePropertyText(value?.from); - const to = this.valueSourcePropertyText(value?.to); - return `${from} - ${to}`; - } - - private valueSourcePropertyText(source?: ValueSourceProperty): string { - if (source) { - if (source.valueSource === 'predefinedValue') { - return `${isNumber(source.value) ? source.value : 0}`; - } else if (source.valueSource === 'entityAttribute') { - const alias = source.entityAlias || 'Undefined'; - const key = source.attribute || 'Undefined'; - return `${alias}.${key}`; - } - } - return 'Undefined'; - } - - private updateModel() { - const value: FixedColorLevel = this.fixedColorLevelFormGroup.value; - this.modelValue = value; - this.propagateChange(this.modelValue); - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.html index c91880c432..7155ad8d5e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.html @@ -15,30 +15,20 @@ limitations under the License. --> - - -
- -
-
{{ tickValueText() }}
-
-
- - -
-
- -
- -
- -
-
-
-
+ +
+ + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts index 9a7bb98c1c..fcbeded87d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts @@ -16,18 +16,19 @@ import { ValueSourceProperty } from '@home/components/widget/lib/settings/common/value-source.component'; import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; -import { isNumber } from '@core/utils'; import { IAliasController } from '@core/api/widget-api.models'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { Datasource } from '@shared/models/widget.models'; @Component({ selector: 'tb-tick-value', templateUrl: './tick-value.component.html', - styleUrls: ['./tick-value.component.scss'], + styleUrls: [], providers: [ { provide: NG_VALUE_ACCESSOR, @@ -42,10 +43,13 @@ export class TickValueComponent extends PageComponent implements OnInit, Control disabled: boolean; @Input() - expanded = false; + aliasController: IAliasController; @Input() - aliasController: IAliasController; + dataKeyCallbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; @Output() removeTickValue = new EventEmitter(); @@ -94,24 +98,6 @@ export class TickValueComponent extends PageComponent implements OnInit, Control ); } - tickValueText(): string { - const value: ValueSourceProperty = this.tickValueFormGroup.get('tickValue').value; - return this.valueSourcePropertyText(value); - } - - private valueSourcePropertyText(source?: ValueSourceProperty): string { - if (source) { - if (source.valueSource === 'predefinedValue') { - return `${isNumber(source.value) ? source.value : 0}`; - } else if (source.valueSource === 'entityAttribute') { - const alias = source.entityAlias || 'Undefined'; - const key = source.attribute || 'Undefined'; - return `${alias}.${key}`; - } - } - return 'Undefined'; - } - private updateModel() { const value: ValueSourceProperty = this.tickValueFormGroup.get('tickValue').value; this.modelValue = value; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.html index ed79ec1820..2903cbd456 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.html @@ -48,18 +48,40 @@ [autoScale]="batteryLevelWidgetSettingsForm.get('autoScaleValueSize').value" [previewText]="valuePreviewFn"> - +
{{ 'widgets.battery-level.battery-level-color' | translate }}
- +
{{ 'widgets.battery-level.battery-shape-color' | translate }}
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.ts index fb0e2b540c..2b27a40eff 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.ts @@ -15,7 +15,7 @@ /// import { Component, Injector } from '@angular/core'; -import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { Datasource, WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -48,6 +48,15 @@ export class BatteryLevelWidgetSettingsComponent extends WidgetSettingsComponent return [BatteryLevelLayout.vertical_divided, BatteryLevelLayout.horizontal_divided].includes(layout); } + public get datasource(): Datasource { + const datasources: Datasource[] = this.widgetConfig.config.datasources; + if (datasources && datasources.length) { + return datasources[0]; + } else { + return null; + } + } + constructor(protected store: Store, private $injector: Injector, private fb: UntypedFormBuilder) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index f093c4c568..461c92bfea 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -74,7 +74,6 @@ import { import { DigitalGaugeWidgetSettingsComponent } from '@home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component'; -import { FixedColorLevelComponent } from '@home/components/widget/lib/settings/gauge/fixed-color-level.component'; import { TickValueComponent } from '@home/components/widget/lib/settings/gauge/tick-value.component'; import { FlotWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/flot-widget-settings.component'; import { @@ -380,7 +379,6 @@ import { AnalogueLinearGaugeWidgetSettingsComponent, AnalogueCompassWidgetSettingsComponent, DigitalGaugeWidgetSettingsComponent, - FixedColorLevelComponent, TickValueComponent, FlotWidgetSettingsComponent, LabelDataKeyComponent, @@ -513,7 +511,6 @@ import { AnalogueLinearGaugeWidgetSettingsComponent, AnalogueCompassWidgetSettingsComponent, DigitalGaugeWidgetSettingsComponent, - FixedColorLevelComponent, TickValueComponent, FlotWidgetSettingsComponent, LabelDataKeyComponent, diff --git a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts index b7ccbd76bf..d74373f734 100644 --- a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts @@ -65,7 +65,7 @@ export interface WidgetContextMenuItem extends ContextMenuItem { export interface DashboardCallbacks { onEditWidget?: ($event: Event, widget: Widget) => void; - onExportWidget?: ($event: Event, widget: Widget) => void; + onExportWidget?: ($event: Event, widget: Widget, widgeTitle: string) => void; onRemoveWidget?: ($event: Event, widget: Widget) => void; onWidgetMouseDown?: ($event: Event, widget: Widget) => void; onWidgetClicked?: ($event: Event, widget: Widget) => void; diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html index 0d5de08de0..208520bbf0 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html @@ -27,7 +27,7 @@
-
+
admin.mobile-app.applications
{{ 'admin.mobile-app.default' | translate }} @@ -104,7 +104,7 @@
-
+
admin.mobile-app.appearance-on-home-page
{{ 'admin.mobile-app.enabled' | translate }} @@ -117,13 +117,6 @@ (click)="$event.stopPropagation()"> {{ 'admin.mobile-app.badges' | translate }} - - - - {{ badgeStyle.value | translate }} - - - @@ -139,13 +132,15 @@ {{ 'admin.mobile-app.label' | translate }} - + warning diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts index 0423ce989f..edba183919 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts @@ -25,10 +25,9 @@ import { MobileAppService } from '@core/http/mobile-app.service'; import { BadgePosition, badgePositionTranslationsMap, - BadgeStyle, - badgeStyleTranslationsMap, MobileAppSettings } from '@shared/models/mobile-app.models'; +import { ActionUpdateMobileQrCodeEnabled } from '@core/auth/auth.actions'; @Component({ selector: 'tb-mobile-app-settings', @@ -44,7 +43,6 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf private readonly destroy$ = new Subject(); badgePositionTranslationsMap = badgePositionTranslationsMap; - badgeStyleTranslationsMap = badgeStyleTranslationsMap; constructor(protected store: Store, private mobileAppService: MobileAppService, @@ -80,23 +78,37 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf ).subscribe(value => { this.iosEnableChanged(value); }); + this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (value) { + this.mobileAppSettingsForm.get('qrCodeConfig').enable({emitEvent: false}); + } else { + this.mobileAppSettingsForm.get('qrCodeConfig').disable({emitEvent: false}); + this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').enable({emitEvent: false}); + } + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').updateValueAndValidity({onlySelf: true}); + this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').updateValueAndValidity({onlySelf: true}); + }); this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(value => { if (value) { if (this.mobileAppSettingsForm.get('androidConfig.enabled').value || this.mobileAppSettingsForm.get('iosConfig.enabled').value) { - this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').enable({emitEvent: false}); + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false}); this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable({emitEvent: false}); + } else { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); } } else { - this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable({emitEvent: false}); this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); } }); this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(value => { - if (value) { + if (value && this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').enable({emitEvent: false}); } else { this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').disable({emitEvent: false}); @@ -125,10 +137,9 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf qrCodeConfig: this.fb.group({ showOnHomePage: [true], badgeEnabled: [true], - badgeStyle: [{value: BadgeStyle.ORIGINAL, disabled: true}], - badgePosition: [{value: BadgePosition.RIGHT, disabled: true}], + badgePosition: [BadgePosition.RIGHT], qrCodeLabelEnabled: [true], - qrCodeLabel: ['', [Validators.required]] + qrCodeLabel: ['', [Validators.required, Validators.maxLength(50)]] }) }); } @@ -148,7 +159,7 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf this.mobileAppSettingsForm.get('androidConfig.appPackage').disable({emitEvent: false}); this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').disable({emitEvent: false}); } - this.updateBadgeControls(value); + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').updateValueAndValidity({onlySelf: true}); } private iosEnableChanged(value: boolean): void { @@ -159,31 +170,20 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf } else { this.mobileAppSettingsForm.get('iosConfig.appId').disable({emitEvent: false}); } - this.updateBadgeControls(value); - } - - private updateBadgeControls(value: boolean) { - if (this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { - if (value) { - this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false}); - if (this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').value) { - this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').enable({emitEvent: false}); - this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable({emitEvent: false}); - } - } else { - if (!this.mobileAppSettingsForm.get('iosConfig.enabled').value && !this.mobileAppSettingsForm.get('androidConfig.enabled').value) { - this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); - this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable({emitEvent: false}); - this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); - } - } - } + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').updateValueAndValidity({onlySelf: true}); } save(): void { + const showOnHomePagePreviousValue = this.mobileAppSettings.qrCodeConfig.showOnHomePage; this.mobileAppSettings = {...this.mobileAppSettings, ...this.mobileAppSettingsForm.getRawValue()}; this.mobileAppService.saveMobileAppSettings(this.mobileAppSettings) - .subscribe((settings) => this.processMobileAppSettings(settings)); + .subscribe((settings) => { + const showOnHomePageValue = settings.qrCodeConfig.showOnHomePage; + if (showOnHomePagePreviousValue !== showOnHomePageValue) { + this.store.dispatch(new ActionUpdateMobileQrCodeEnabled({mobileQrEnabled: showOnHomePageValue})); + } + this.processMobileAppSettings(settings); + }); } confirmForm(): FormGroup { diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html index 2e85f8b54b..2ea96f6387 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html @@ -156,7 +156,7 @@
- + admin.oauth2.mobile-package admin.oauth2.mobile-package-hint @@ -166,9 +166,9 @@
- + admin.oauth2.mobile-app-secret - + - - {{ 'admin.oauth2.invalid-mobile-app-secret' | translate }} + admin.oauth2.mobile-app-secret-hint + + {{ 'admin.oauth2.mobile-app-secret-required' | translate }} + + + {{ 'admin.oauth2.mobile-app-secret-min-length' | translate }} + + + {{ 'admin.oauth2.mobile-app-secret-base64' | translate }}
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts index f6f8340550..f1eabf2d0c 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts @@ -17,6 +17,7 @@ import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { AbstractControl, + FormControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, @@ -215,7 +216,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha this.oauth2SettingsForm.get('edgeEnabled').patchValue(false); this.oauth2SettingsForm.get('edgeEnabled').disable(); } - })) + })); } private initOAuth2Settings(oauth2Info: OAuth2Info): void { @@ -302,11 +303,25 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha private buildMobileInfoForm(mobileInfo?: OAuth2MobileInfo): UntypedFormGroup { return this.fb.group({ pkgName: [mobileInfo?.pkgName, [Validators.required]], - appSecret: [mobileInfo?.appSecret, [Validators.required, Validators.minLength(16), Validators.maxLength(2048), - Validators.pattern(/^[A-Za-z0-9]+$/)]], + appSecret: [mobileInfo?.appSecret, [Validators.required, this.base64Format]], }, {validators: this.uniquePkgNameValidator}); } + private base64Format(control: FormControl): { [key: string]: boolean } | null { + if (control.value === '') { + return null; + } + try { + const value = atob(control.value); + if (value.length < 64) { + return {minLength: true}; + } + return null; + } catch (e) { + return {base64: true}; + } + } + private buildRegistrationForm(registration?: OAuth2RegistrationInfo): UntypedFormGroup { let additionalInfo = null; if (isDefinedAndNotNull(registration?.additionalInfo)) { @@ -556,7 +571,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha addMobileInfo(control: AbstractControl): void { this.mobileInfos(control).push(this.buildMobileInfoForm({ pkgName: '', - appSecret: randomAlphanumeric(24) + appSecret: btoa(randomAlphanumeric(64)) })); } diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts index 17dda4a0c9..c7ac489753 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts @@ -201,7 +201,7 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi } try { const value = atob(control.value); - if (value.length < 32) { + if (value.length < 64) { return {minLength: true}; } return null; diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts index a455bac64d..303a1bce4e 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts @@ -33,6 +33,7 @@ import { } from '@core/auth/auth.selectors'; import { EntityKeyType } from '@shared/models/query/query.models'; import { ResourcesService } from '@core/services/resources.service'; +import { isDefinedAndNotNull } from '@core/utils'; const sysAdminHomePageJson = '/assets/dashboard/sys_admin_home_page.json'; const tenantAdminHomePageJson = '/assets/dashboard/tenant_admin_home_page.json'; @@ -83,12 +84,12 @@ const applySystemParametersToHomeDashboard = (store: Store, } } } - if (params.mobileQrEnabled) { + if (isDefinedAndNotNull(params.mobileQrEnabled)) { for (const widgetId of Object.keys(dashboard.configuration.widgets)) { if (dashboard.configuration.widgets[widgetId].config.title === 'Select show mobile QR code') { dashboard.configuration.widgets[widgetId].config.settings.markdownTextFunction = (dashboard.configuration.widgets[widgetId].config.settings.markdownTextFunction as string) - .replace('\'${mobileQrEnabled}\'', String(true)); + .replace(/\${mobileQrEnabled:([^}]+)}/, `\${mobileQrEnabled:${String(params.mobileQrEnabled)}}`); break; } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 77e3602215..46c84087ee 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -77,7 +77,7 @@ import { } from '@shared/models/rule-node.models'; import { FcRuleNodeModel, FcRuleNodeTypeModel, RuleChainMenuContextInfo } from './rulechain-page.models'; import { RuleChainService } from '@core/http/rule-chain.service'; -import { NEVER, Observable, of, ReplaySubject, startWith, skip, Subject } from 'rxjs'; +import { NEVER, Observable, of, ReplaySubject, skip, startWith, Subject } from 'rxjs'; import { debounceTime, distinctUntilChanged, mergeMap, takeUntil, tap } from 'rxjs/operators'; import { ISearchableComponent } from '../../models/searchable-component.models'; import { deepClone, isDefinedAndNotNull } from '@core/utils'; @@ -373,7 +373,7 @@ export class RuleChainPageComponent extends PageComponent return false; } return true; - }, ['INPUT', 'SELECT', 'TEXTAREA'], + }, [], this.translate.instant('rulenode.select-all-objects')) ); this.hotKeys.push( @@ -384,7 +384,7 @@ export class RuleChainPageComponent extends PageComponent return false; } return true; - }, ['INPUT', 'SELECT', 'TEXTAREA'], + }, [], this.translate.instant('rulenode.copy-selected')) ); this.hotKeys.push( @@ -397,7 +397,7 @@ export class RuleChainPageComponent extends PageComponent return false; } return true; - }, ['INPUT', 'SELECT', 'TEXTAREA'], + }, [], this.translate.instant('action.paste')) ); this.hotKeys.push( @@ -442,7 +442,7 @@ export class RuleChainPageComponent extends PageComponent return false; } return true; - }, ['INPUT', 'SELECT', 'TEXTAREA'], + }, [], this.translate.instant('rulenode.delete-selected-objects')) ); this.hotKeys.push( diff --git a/ui-ngx/src/app/shared/components/color-input.component.html b/ui-ngx/src/app/shared/components/color-input.component.html index 29df75d8d6..d57824b443 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.html +++ b/ui-ngx/src/app/shared/components/color-input.component.html @@ -38,6 +38,7 @@