diff --git a/application/src/main/data/json/demo/dashboards/firmware.json b/application/src/main/data/json/demo/dashboards/firmware.json index 779f2cb93c..7a21e86761 100644 --- a/application/src/main/data/json/demo/dashboards/firmware.json +++ b/application/src/main/data/json/demo/dashboards/firmware.json @@ -1,24 +1,34 @@ { "title": "Firmware", "image": null, + "mobileHide": false, + "mobileOrder": null, "configuration": { "description": "", "widgets": { "cd03188e-cd9d-9601-fd57-da4cb95fc016": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "entities_table", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 7.5, "sizeY": 6.5, "config": { "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, "interval": 1000, - "timewindowMs": 86400000 + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085363133, + "endTimeMs": 1694171763133 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "NONE", @@ -299,16 +309,11 @@ }, "row": 0, "col": 0, - "id": "cd03188e-cd9d-9601-fd57-da4cb95fc016" + "id": "cd03188e-cd9d-9601-fd57-da4cb95fc016", + "typeFullFqn": "system.cards.entities_table" }, "100b756c-0082-6505-3ae1-3603e6deea48": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "timeseries_table", "type": "timeseries", - "title": "New widget", - "image": null, - "description": null, "sizeX": 8, "sizeY": 6.5, "config": { @@ -461,14 +466,11 @@ }, "row": 0, "col": 0, - "id": "100b756c-0082-6505-3ae1-3603e6deea48" + "id": "100b756c-0082-6505-3ae1-3603e6deea48", + "typeFullFqn": "system.cards.timeseries_table" }, "17543c57-af4a-2c1e-bf12-53a7b46791e6": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "html_value_card", "type": "latest", - "title": "New widget", "sizeX": 8, "sizeY": 3, "config": { @@ -496,8 +498,27 @@ } ], "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { - "timewindowMs": 60000 + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085363133, + "endTimeMs": 1694171763133 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 } }, "showTitle": false, @@ -545,14 +566,11 @@ "enableDataExport": false, "displayTimewindow": true }, - "id": "17543c57-af4a-2c1e-bf12-53a7b46791e6" + "id": "17543c57-af4a-2c1e-bf12-53a7b46791e6", + "typeFullFqn": "system.cards.html_value_card" }, "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "html_value_card", "type": "latest", - "title": "New widget", "sizeX": 8, "sizeY": 3, "config": { @@ -580,8 +598,27 @@ } ], "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { - "timewindowMs": 60000 + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085363133, + "endTimeMs": 1694171763133 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 } }, "showTitle": false, @@ -629,14 +666,11 @@ "enableDataExport": false, "displayTimewindow": true }, - "id": "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6" + "id": "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6", + "typeFullFqn": "system.cards.html_value_card" }, "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "html_value_card", "type": "latest", - "title": "New widget", "sizeX": 8, "sizeY": 3, "config": { @@ -664,8 +698,27 @@ } ], "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { - "timewindowMs": 60000 + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085363133, + "endTimeMs": 1694171763133 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 } }, "showTitle": false, @@ -713,14 +766,11 @@ "enableDataExport": false, "displayTimewindow": true }, - "id": "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81" + "id": "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81", + "typeFullFqn": "system.cards.html_value_card" }, "77b10144-b904-edd5-8c7c-8fb75616c6d8": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "html_value_card", "type": "latest", - "title": "New widget", "sizeX": 8, "sizeY": 3, "config": { @@ -748,8 +798,27 @@ } ], "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { - "timewindowMs": 60000 + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085363133, + "endTimeMs": 1694171763133 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 } }, "showTitle": false, @@ -797,23 +866,32 @@ "enableDataExport": false, "displayTimewindow": true }, - "id": "77b10144-b904-edd5-8c7c-8fb75616c6d8" + "id": "77b10144-b904-edd5-8c7c-8fb75616c6d8", + "typeFullFqn": "system.cards.html_value_card" }, "21be08bb-ec90-f760-ad6f-e7678f12c401": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "entities_table", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 7.5, "sizeY": 6.5, "config": { "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { + "realtimeType": 1, "interval": 1000, - "timewindowMs": 86400000 + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085363133, + "endTimeMs": 1694171763133 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "NONE", @@ -1094,23 +1172,32 @@ }, "row": 0, "col": 0, - "id": "21be08bb-ec90-f760-ad6f-e7678f12c401" + "id": "21be08bb-ec90-f760-ad6f-e7678f12c401", + "typeFullFqn": "system.cards.entities_table" }, "e8280043-d3dc-7acb-c2ff-a4522972ff91": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "entities_table", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 7.5, "sizeY": 6.5, "config": { "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, "interval": 1000, - "timewindowMs": 86400000 + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085363133, + "endTimeMs": 1694171763133 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "NONE", @@ -1391,23 +1478,32 @@ }, "row": 0, "col": 0, - "id": "e8280043-d3dc-7acb-c2ff-a4522972ff91" + "id": "e8280043-d3dc-7acb-c2ff-a4522972ff91", + "typeFullFqn": "system.cards.entities_table" }, "3624013b-378c-f110-5eba-ae95c25a4dcc": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "entities_table", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 7.5, "sizeY": 6.5, "config": { "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, "interval": 1000, - "timewindowMs": 86400000 + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085363133, + "endTimeMs": 1694171763133 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "NONE", @@ -1688,23 +1784,32 @@ }, "row": 0, "col": 0, - "id": "3624013b-378c-f110-5eba-ae95c25a4dcc" + "id": "3624013b-378c-f110-5eba-ae95c25a4dcc", + "typeFullFqn": "system.cards.entities_table" }, "d2d13e0d-4e71-889f-9343-ad2f0af9f176": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "entities_table", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 7.5, "sizeY": 6.5, "config": { "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { + "realtimeType": 1, "interval": 1000, - "timewindowMs": 86400000 + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085363133, + "endTimeMs": 1694171763133 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "NONE", @@ -1985,7 +2090,8 @@ }, "row": 0, "col": 0, - "id": "d2d13e0d-4e71-889f-9343-ad2f0af9f176" + "id": "d2d13e0d-4e71-889f-9343-ad2f0af9f176", + "typeFullFqn": "system.cards.entities_table" } }, "states": { @@ -2035,7 +2141,8 @@ "autoFillHeight": true, "backgroundImageUrl": null, "mobileAutoFillHeight": true, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "outerMargin": true } } } @@ -2062,7 +2169,8 @@ "autoFillHeight": true, "backgroundImageUrl": null, "mobileAutoFillHeight": false, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "outerMargin": true } } } @@ -2089,7 +2197,8 @@ "autoFillHeight": true, "backgroundImageUrl": null, "mobileAutoFillHeight": false, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "outerMargin": true } } } @@ -2116,7 +2225,8 @@ "autoFillHeight": true, "backgroundImageUrl": null, "mobileAutoFillHeight": false, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "outerMargin": true } } } @@ -2143,7 +2253,8 @@ "autoFillHeight": true, "backgroundImageUrl": null, "mobileAutoFillHeight": false, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "outerMargin": true } } } @@ -2170,7 +2281,8 @@ "autoFillHeight": true, "backgroundImageUrl": null, "mobileAutoFillHeight": false, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "outerMargin": true } } } @@ -2488,5 +2600,6 @@ "showUpdateDashboardImage": false } }, + "externalId": null, "name": "Firmware" } \ No newline at end of file diff --git a/application/src/main/data/json/demo/dashboards/gateways.json b/application/src/main/data/json/demo/dashboards/gateways.json index 6051fbd514..f4eb275c3c 100644 --- a/application/src/main/data/json/demo/dashboards/gateways.json +++ b/application/src/main/data/json/demo/dashboards/gateways.json @@ -1,20 +1,33 @@ { "title": "Gateways", + "image": null, + "mobileHide": false, + "mobileOrder": null, "configuration": { "widgets": { "94715984-ae74-76e4-20b7-2f956b01ed80": { - "isSystemType": true, - "bundleAlias": "entity_admin_widgets", - "typeAlias": "device_admin_table", "type": "latest", - "title": "New widget", "sizeX": 24, "sizeY": 12, "config": { "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { + "realtimeType": 1, "interval": 1000, - "timewindowMs": 86400000 + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085270425, + "endTimeMs": 1694171670425 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "NONE", @@ -187,14 +200,11 @@ ] } }, - "id": "94715984-ae74-76e4-20b7-2f956b01ed80" + "id": "94715984-ae74-76e4-20b7-2f956b01ed80", + "typeFullFqn": "system.entity_admin_widgets.device_admin_table" }, "eadabbc7-519e-76fc-ba10-b3fe8c18da10": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "timeseries_table", "type": "timeseries", - "title": "New widget", "sizeX": 14, "sizeY": 13, "config": { @@ -255,14 +265,11 @@ "titleTooltip": "", "displayTimewindow": true }, - "id": "eadabbc7-519e-76fc-ba10-b3fe8c18da10" + "id": "eadabbc7-519e-76fc-ba10-b3fe8c18da10", + "typeFullFqn": "system.cards.timeseries_table" }, "f928afc4-30d1-8d0c-e3cf-777f9f9d1155": { - "isSystemType": true, - "bundleAlias": "charts", - "typeAlias": "basic_timeseries", "type": "timeseries", - "title": "New widget", "sizeX": 17, "sizeY": 4, "config": { @@ -375,6 +382,15 @@ "xaxisSecond": { "axisPosition": "top", "showLabels": true + }, + "showLegend": true, + "legendConfig": { + "direction": "column", + "position": "right", + "showMin": true, + "showMax": true, + "showAvg": true, + "showTotal": true } }, "title": "Real time information", @@ -393,25 +409,13 @@ "widgetStyle": {}, "useDashboardTimewindow": false, "displayTimewindow": true, - "showLegend": true, - "legendConfig": { - "direction": "column", - "position": "right", - "showMin": true, - "showMax": true, - "showAvg": true, - "showTotal": true - }, "actions": {} }, - "id": "f928afc4-30d1-8d0c-e3cf-777f9f9d1155" + "id": "f928afc4-30d1-8d0c-e3cf-777f9f9d1155", + "typeFullFqn": "system.charts.basic_timeseries" }, "2a95b473-042d-59d0-2da2-40d0cccb6c8a": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "timeseries_table", "type": "timeseries", - "title": "New widget", "sizeX": 7, "sizeY": 7, "config": { @@ -491,14 +495,11 @@ "showTotal": false } }, - "id": "2a95b473-042d-59d0-2da2-40d0cccb6c8a" + "id": "2a95b473-042d-59d0-2da2-40d0cccb6c8a", + "typeFullFqn": "system.cards.timeseries_table" }, "aaa69366-aacc-9028-65aa-645c0f8533ec": { - "isSystemType": true, - "bundleAlias": "charts", - "typeAlias": "basic_timeseries", "type": "timeseries", - "title": "New widget", "sizeX": 17, "sizeY": 4, "config": { @@ -604,6 +605,15 @@ "xaxisSecond": { "axisPosition": "top", "showLabels": true + }, + "showLegend": true, + "legendConfig": { + "direction": "column", + "position": "right", + "showMin": true, + "showMax": true, + "showAvg": true, + "showTotal": true } }, "title": "History information", @@ -622,25 +632,13 @@ "widgetStyle": {}, "useDashboardTimewindow": true, "displayTimewindow": true, - "showLegend": true, - "legendConfig": { - "direction": "column", - "position": "right", - "showMin": true, - "showMax": true, - "showAvg": true, - "showTotal": true - }, "actions": {} }, - "id": "aaa69366-aacc-9028-65aa-645c0f8533ec" + "id": "aaa69366-aacc-9028-65aa-645c0f8533ec", + "typeFullFqn": "system.charts.basic_timeseries" }, "ce5c7d01-a3ef-5cf0-4578-8505135c23a0": { - "isSystemType": true, - "bundleAlias": "charts", - "typeAlias": "basic_timeseries", "type": "timeseries", - "title": "New widget", "sizeX": 17, "sizeY": 4, "config": { @@ -751,6 +749,15 @@ "xaxisSecond": { "axisPosition": "top", "showLabels": true + }, + "showLegend": true, + "legendConfig": { + "direction": "column", + "position": "right", + "showMin": true, + "showMax": true, + "showAvg": true, + "showTotal": true } }, "title": "Real time information", @@ -769,25 +776,13 @@ "widgetStyle": {}, "useDashboardTimewindow": false, "displayTimewindow": true, - "showLegend": true, - "legendConfig": { - "direction": "column", - "position": "right", - "showMin": true, - "showMax": true, - "showAvg": true, - "showTotal": true - }, "actions": {} }, - "id": "ce5c7d01-a3ef-5cf0-4578-8505135c23a0" + "id": "ce5c7d01-a3ef-5cf0-4578-8505135c23a0", + "typeFullFqn": "system.charts.basic_timeseries" }, "466f046d-6005-a168-b107-60fcb2469cd5": { - "isSystemType": true, - "bundleAlias": "gateway_widgets", - "typeAlias": "attributes_card", "type": "latest", - "title": "New widget", "sizeX": 7, "sizeY": 5, "config": { @@ -799,8 +794,27 @@ } ], "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { - "timewindowMs": 60000 + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085270425, + "endTimeMs": 1694171670425 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 } }, "showTitle": true, @@ -832,14 +846,11 @@ "showLegend": false, "actions": {} }, - "id": "466f046d-6005-a168-b107-60fcb2469cd5" + "id": "466f046d-6005-a168-b107-60fcb2469cd5", + "typeFullFqn": "system.gateway_widgets.attributes_card" }, "8fc32225-164f-3258-73f7-e6b6d959cf0b": { - "isSystemType": true, - "bundleAlias": "gateway_widgets", - "typeAlias": "config_form_latest", "type": "latest", - "title": "New widget", "sizeX": 10, "sizeY": 9, "config": { @@ -851,8 +862,27 @@ } ], "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { - "timewindowMs": 60000 + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085270425, + "endTimeMs": 1694171670425 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 } }, "showTitle": true, @@ -881,14 +911,11 @@ "showLegend": false, "actions": {} }, - "id": "8fc32225-164f-3258-73f7-e6b6d959cf0b" + "id": "8fc32225-164f-3258-73f7-e6b6d959cf0b", + "typeFullFqn": "system.gateway_widgets.config_form_latest" }, "063fc179-c9fd-f952-e714-f24e9c43c05c": { - "isSystemType": true, - "bundleAlias": "control_widgets", - "typeAlias": "rpcbutton", "type": "rpc", - "title": "New widget", "sizeX": 4, "sizeY": 2, "config": { @@ -930,14 +957,11 @@ "b2487e75-2fa4-f211-142c-434dfd50c70c" ] }, - "id": "063fc179-c9fd-f952-e714-f24e9c43c05c" + "id": "063fc179-c9fd-f952-e714-f24e9c43c05c", + "typeFullFqn": "system.control_widgets.rpcbutton" }, "3c2134cc-27a0-93e1-dbe1-2fa7c1ce16b7": { - "isSystemType": true, - "bundleAlias": "control_widgets", - "typeAlias": "rpcbutton", "type": "rpc", - "title": "New widget", "sizeX": 4, "sizeY": 2, "config": { @@ -979,14 +1003,11 @@ "b2487e75-2fa4-f211-142c-434dfd50c70c" ] }, - "id": "3c2134cc-27a0-93e1-dbe1-2fa7c1ce16b7" + "id": "3c2134cc-27a0-93e1-dbe1-2fa7c1ce16b7", + "typeFullFqn": "system.control_widgets.rpcbutton" }, "6770b6ba-eff8-df05-75f8-c1f9326d4842": { - "isSystemType": true, - "bundleAlias": "input_widgets", - "typeAlias": "markers_placement_openstreetmap", "type": "latest", - "title": "New widget", "sizeX": 6, "sizeY": 4, "config": { @@ -1015,8 +1036,27 @@ } ], "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { - "timewindowMs": 60000 + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085270425, + "endTimeMs": 1694171670425 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 } }, "showTitle": false, @@ -1083,7 +1123,8 @@ "titleTooltip": "", "displayTimewindow": true }, - "id": "6770b6ba-eff8-df05-75f8-c1f9326d4842" + "id": "6770b6ba-eff8-df05-75f8-c1f9326d4842", + "typeFullFqn": "system.input_widgets.markers_placement_openstreetmap" } }, "states": { @@ -1108,7 +1149,8 @@ "autoFillHeight": true, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "margin": 10 + "margin": 10, + "outerMargin": true } } } @@ -1158,7 +1200,8 @@ "autoFillHeight": true, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "margin": 10 + "margin": 10, + "outerMargin": true } } } @@ -1211,7 +1254,8 @@ "autoFillHeight": true, "mobileAutoFillHeight": true, "mobileRowHeight": 70, - "margin": 10 + "margin": 10, + "outerMargin": true } } } @@ -1224,8 +1268,10 @@ "filter": { "type": "deviceType", "resolveMultiple": true, - "deviceType": "gateway", - "deviceNameFilter": "" + "deviceNameFilter": "", + "deviceTypes": [ + "gateway" + ] } }, "b2487e75-2fa4-f211-142c-434dfd50c70c": { @@ -1261,7 +1307,9 @@ "showDashboardExport": true, "toolbarAlwaysOpen": true, "titleColor": "rgba(0,0,0,0.870588)" - } + }, + "filters": {} }, + "externalId": null, "name": "Gateways" -} +} \ No newline at end of file diff --git a/application/src/main/data/json/demo/dashboards/rule_engine_statistics.json b/application/src/main/data/json/demo/dashboards/rule_engine_statistics.json index b8231ab880..c84faa359e 100644 --- a/application/src/main/data/json/demo/dashboards/rule_engine_statistics.json +++ b/application/src/main/data/json/demo/dashboards/rule_engine_statistics.json @@ -1,13 +1,12 @@ { "title": "Rule Engine Statistics", + "image": null, + "mobileHide": false, + "mobileOrder": null, "configuration": { "widgets": { "81987f19-3eac-e4ce-b790-d96e9b54d9a0": { - "isSystemType": true, - "bundleAlias": "charts", - "typeAlias": "basic_timeseries", "type": "timeseries", - "title": "New widget", "sizeX": 12, "sizeY": 7, "config": { @@ -151,6 +150,15 @@ "xaxisSecond": { "axisPosition": "top", "showLabels": true + }, + "showLegend": true, + "legendConfig": { + "direction": "column", + "position": "bottom", + "showMin": true, + "showMax": true, + "showAvg": false, + "showTotal": true } }, "title": "Queue Stats", @@ -169,25 +177,13 @@ "widgetStyle": {}, "useDashboardTimewindow": false, "displayTimewindow": true, - "showLegend": true, - "actions": {}, - "legendConfig": { - "direction": "column", - "position": "bottom", - "showMin": true, - "showMax": true, - "showAvg": false, - "showTotal": true - } + "actions": {} }, - "id": "81987f19-3eac-e4ce-b790-d96e9b54d9a0" + "id": "81987f19-3eac-e4ce-b790-d96e9b54d9a0", + "typeFullFqn": "system.charts.basic_timeseries" }, "5eb79712-5c24-3060-7e4f-6af36b8f842d": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "timeseries_table", "type": "timeseries", - "title": "New widget", "sizeX": 24, "sizeY": 5, "config": { @@ -272,14 +268,11 @@ "titleTooltip": "", "displayTimewindow": true }, - "id": "5eb79712-5c24-3060-7e4f-6af36b8f842d" + "id": "5eb79712-5c24-3060-7e4f-6af36b8f842d", + "typeFullFqn": "system.cards.timeseries_table" }, "ad3f1417-87a8-750e-fc67-49a2de1466d4": { - "isSystemType": true, - "bundleAlias": "charts", - "typeAlias": "basic_timeseries", "type": "timeseries", - "title": "New widget", "sizeX": 12, "sizeY": 7, "config": { @@ -393,6 +386,15 @@ "xaxisSecond": { "axisPosition": "top", "showLabels": true + }, + "showLegend": true, + "legendConfig": { + "direction": "column", + "position": "bottom", + "showMin": true, + "showMax": true, + "showAvg": false, + "showTotal": true } }, "title": "Processing Failures and Timeouts", @@ -411,18 +413,10 @@ "widgetStyle": {}, "useDashboardTimewindow": false, "displayTimewindow": true, - "showLegend": true, - "actions": {}, - "legendConfig": { - "direction": "column", - "position": "bottom", - "showMin": true, - "showMax": true, - "showAvg": false, - "showTotal": true - } + "actions": {} }, - "id": "ad3f1417-87a8-750e-fc67-49a2de1466d4" + "id": "ad3f1417-87a8-750e-fc67-49a2de1466d4", + "typeFullFqn": "system.charts.basic_timeseries" } }, "states": { @@ -457,14 +451,12 @@ "backgroundColor": "#eeeeee", "color": "rgba(0,0,0,0.870588)", "columns": 24, - "margins": [ - 10, - 10 - ], "backgroundSizeMode": "100%", "autoFillHeight": true, "mobileAutoFillHeight": false, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "margin": 10, + "outerMargin": true } } } @@ -477,8 +469,10 @@ "filter": { "type": "assetType", "resolveMultiple": true, - "assetType": "TbServiceQueue", - "assetNameFilter": "" + "assetNameFilter": "", + "assetTypes": [ + "TbServiceQueue" + ] } } }, @@ -514,7 +508,9 @@ "showDashboardTimewindow": true, "showDashboardExport": true, "toolbarAlwaysOpen": true - } + }, + "filters": {} }, + "externalId": null, "name": "Rule Engine Statistics" } \ No newline at end of file diff --git a/application/src/main/data/json/demo/dashboards/software.json b/application/src/main/data/json/demo/dashboards/software.json index e9d7e9bbbd..eea81e5368 100644 --- a/application/src/main/data/json/demo/dashboards/software.json +++ b/application/src/main/data/json/demo/dashboards/software.json @@ -1,24 +1,34 @@ { "title": "Software", "image": null, + "mobileHide": false, + "mobileOrder": null, "configuration": { "description": "", "widgets": { "cd03188e-cd9d-9601-fd57-da4cb95fc016": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "entities_table", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 7.5, "sizeY": 6.5, "config": { "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, "interval": 1000, - "timewindowMs": 86400000 + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085343279, + "endTimeMs": 1694171743279 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "NONE", @@ -299,16 +309,11 @@ }, "row": 0, "col": 0, - "id": "cd03188e-cd9d-9601-fd57-da4cb95fc016" + "id": "cd03188e-cd9d-9601-fd57-da4cb95fc016", + "typeFullFqn": "system.cards.entities_table" }, "100b756c-0082-6505-3ae1-3603e6deea48": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "timeseries_table", "type": "timeseries", - "title": "New widget", - "image": null, - "description": null, "sizeX": 8, "sizeY": 6.5, "config": { @@ -461,14 +466,11 @@ }, "row": 0, "col": 0, - "id": "100b756c-0082-6505-3ae1-3603e6deea48" + "id": "100b756c-0082-6505-3ae1-3603e6deea48", + "typeFullFqn": "system.cards.timeseries_table" }, "17543c57-af4a-2c1e-bf12-53a7b46791e6": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "html_value_card", "type": "latest", - "title": "New widget", "sizeX": 8, "sizeY": 3, "config": { @@ -496,8 +498,27 @@ } ], "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { - "timewindowMs": 60000 + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085343280, + "endTimeMs": 1694171743280 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 } }, "showTitle": false, @@ -545,14 +566,11 @@ "enableDataExport": false, "displayTimewindow": true }, - "id": "17543c57-af4a-2c1e-bf12-53a7b46791e6" + "id": "17543c57-af4a-2c1e-bf12-53a7b46791e6", + "typeFullFqn": "system.cards.html_value_card" }, "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "html_value_card", "type": "latest", - "title": "New widget", "sizeX": 8, "sizeY": 3, "config": { @@ -580,8 +598,27 @@ } ], "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { - "timewindowMs": 60000 + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085343280, + "endTimeMs": 1694171743280 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 } }, "showTitle": false, @@ -629,14 +666,11 @@ "enableDataExport": false, "displayTimewindow": true }, - "id": "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6" + "id": "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6", + "typeFullFqn": "system.cards.html_value_card" }, "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "html_value_card", "type": "latest", - "title": "New widget", "sizeX": 8, "sizeY": 3, "config": { @@ -664,8 +698,27 @@ } ], "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { - "timewindowMs": 60000 + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085343280, + "endTimeMs": 1694171743280 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 } }, "showTitle": false, @@ -713,14 +766,11 @@ "enableDataExport": false, "displayTimewindow": true }, - "id": "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81" + "id": "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81", + "typeFullFqn": "system.cards.html_value_card" }, "77b10144-b904-edd5-8c7c-8fb75616c6d8": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "html_value_card", "type": "latest", - "title": "New widget", "sizeX": 8, "sizeY": 3, "config": { @@ -748,8 +798,27 @@ } ], "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { - "timewindowMs": 60000 + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085343280, + "endTimeMs": 1694171743280 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 } }, "showTitle": false, @@ -797,23 +866,32 @@ "enableDataExport": false, "displayTimewindow": true }, - "id": "77b10144-b904-edd5-8c7c-8fb75616c6d8" + "id": "77b10144-b904-edd5-8c7c-8fb75616c6d8", + "typeFullFqn": "system.cards.html_value_card" }, "21be08bb-ec90-f760-ad6f-e7678f12c401": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "entities_table", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 7.5, "sizeY": 6.5, "config": { "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { + "realtimeType": 1, "interval": 1000, - "timewindowMs": 86400000 + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085343280, + "endTimeMs": 1694171743280 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "NONE", @@ -1094,23 +1172,32 @@ }, "row": 0, "col": 0, - "id": "21be08bb-ec90-f760-ad6f-e7678f12c401" + "id": "21be08bb-ec90-f760-ad6f-e7678f12c401", + "typeFullFqn": "system.cards.entities_table" }, "e8280043-d3dc-7acb-c2ff-a4522972ff91": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "entities_table", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 7.5, "sizeY": 6.5, "config": { "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, "interval": 1000, - "timewindowMs": 86400000 + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085343280, + "endTimeMs": 1694171743280 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "NONE", @@ -1391,23 +1478,32 @@ }, "row": 0, "col": 0, - "id": "e8280043-d3dc-7acb-c2ff-a4522972ff91" + "id": "e8280043-d3dc-7acb-c2ff-a4522972ff91", + "typeFullFqn": "system.cards.entities_table" }, "3624013b-378c-f110-5eba-ae95c25a4dcc": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "entities_table", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 7.5, "sizeY": 6.5, "config": { "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, "interval": 1000, - "timewindowMs": 86400000 + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085343280, + "endTimeMs": 1694171743280 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "NONE", @@ -1688,23 +1784,32 @@ }, "row": 0, "col": 0, - "id": "3624013b-378c-f110-5eba-ae95c25a4dcc" + "id": "3624013b-378c-f110-5eba-ae95c25a4dcc", + "typeFullFqn": "system.cards.entities_table" }, "d2d13e0d-4e71-889f-9343-ad2f0af9f176": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "entities_table", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 7.5, "sizeY": 6.5, "config": { "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { + "realtimeType": 1, "interval": 1000, - "timewindowMs": 86400000 + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085343280, + "endTimeMs": 1694171743280 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "NONE", @@ -1985,7 +2090,8 @@ }, "row": 0, "col": 0, - "id": "d2d13e0d-4e71-889f-9343-ad2f0af9f176" + "id": "d2d13e0d-4e71-889f-9343-ad2f0af9f176", + "typeFullFqn": "system.cards.entities_table" } }, "states": { @@ -2035,7 +2141,8 @@ "autoFillHeight": true, "backgroundImageUrl": null, "mobileAutoFillHeight": true, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "outerMargin": true } } } @@ -2062,7 +2169,8 @@ "autoFillHeight": true, "backgroundImageUrl": null, "mobileAutoFillHeight": false, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "outerMargin": true } } } @@ -2089,7 +2197,8 @@ "autoFillHeight": true, "backgroundImageUrl": null, "mobileAutoFillHeight": false, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "outerMargin": true } } } @@ -2116,7 +2225,8 @@ "autoFillHeight": true, "backgroundImageUrl": null, "mobileAutoFillHeight": false, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "outerMargin": true } } } @@ -2143,7 +2253,8 @@ "autoFillHeight": true, "backgroundImageUrl": null, "mobileAutoFillHeight": false, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "outerMargin": true } } } @@ -2170,7 +2281,8 @@ "autoFillHeight": true, "backgroundImageUrl": null, "mobileAutoFillHeight": false, - "mobileRowHeight": 70 + "mobileRowHeight": 70, + "outerMargin": true } } } @@ -2488,5 +2600,6 @@ "showUpdateDashboardImage": false } }, + "externalId": null, "name": "Software" } \ No newline at end of file diff --git a/application/src/main/data/json/demo/dashboards/thermostats.json b/application/src/main/data/json/demo/dashboards/thermostats.json index c014d8875a..967c1cb253 100644 --- a/application/src/main/data/json/demo/dashboards/thermostats.json +++ b/application/src/main/data/json/demo/dashboards/thermostats.json @@ -1,20 +1,33 @@ { "title": "Thermostats", + "image": null, + "mobileHide": false, + "mobileOrder": null, "configuration": { "widgets": { "f33c746c-0dfc-c212-395b-b448c8a17209": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "entities_table", "type": "latest", - "title": "New widget", "sizeX": 11, "sizeY": 11, "config": { "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { + "realtimeType": 1, "interval": 1000, - "timewindowMs": 86400000 + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085177686, + "endTimeMs": 1694171577686 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "NONE", @@ -184,14 +197,11 @@ "rowClick": [] } }, - "id": "f33c746c-0dfc-c212-395b-b448c8a17209" + "id": "f33c746c-0dfc-c212-395b-b448c8a17209", + "typeFullFqn": "system.cards.entities_table" }, "7943196b-eedb-d422-f9c3-b32d379ad172": { - "isSystemType": true, - "bundleAlias": "alarm_widgets", - "typeAlias": "alarms_table", "type": "alarm", - "title": "New widget", "sizeX": 13, "sizeY": 5, "config": { @@ -230,7 +240,7 @@ "titleStyle": { "fontSize": "16px", "fontWeight": 400, - "padding": "5px 10px 5px 10px" + "padding": "5px 10px 5px 2px" }, "useDashboardTimewindow": false, "showLegend": false, @@ -290,7 +300,6 @@ } ] }, - "alarmSearchStatus": "ANY", "alarmsPollingInterval": 5, "showTitleIcon": false, "titleIcon": null, @@ -302,16 +311,22 @@ "actions": {}, "datasources": [], "alarmsMaxCountLoad": 0, - "alarmsFetchSize": 100 + "alarmsFetchSize": 100, + "alarmFilterConfig": { + "statusList": [], + "severityList": [], + "typeList": [], + "searchPropagatedAlarms": true + }, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" }, - "id": "7943196b-eedb-d422-f9c3-b32d379ad172" + "id": "7943196b-eedb-d422-f9c3-b32d379ad172", + "typeFullFqn": "system.alarm_widgets.alarms_table" }, "14a19183-f0b2-d6be-0f62-9863f0a51111": { - "isSystemType": true, - "bundleAlias": "charts", - "typeAlias": "basic_timeseries", "type": "timeseries", - "title": "New widget", "sizeX": 18, "sizeY": 6, "config": { @@ -395,7 +410,16 @@ "axisPosition": "top", "showLabels": true }, - "smoothLines": true + "smoothLines": true, + "showLegend": true, + "legendConfig": { + "direction": "column", + "position": "bottom", + "showMin": true, + "showMax": true, + "showAvg": true, + "showTotal": false + } }, "title": "Temperature", "dropShadow": true, @@ -413,25 +437,13 @@ "widgetStyle": {}, "useDashboardTimewindow": false, "displayTimewindow": true, - "showLegend": true, - "legendConfig": { - "direction": "column", - "position": "bottom", - "showMin": true, - "showMax": true, - "showAvg": true, - "showTotal": false - }, "actions": {} }, - "id": "14a19183-f0b2-d6be-0f62-9863f0a51111" + "id": "14a19183-f0b2-d6be-0f62-9863f0a51111", + "typeFullFqn": "system.charts.basic_timeseries" }, "07f49fd5-a73b-d74c-c220-362c20af81f4": { - "isSystemType": true, - "bundleAlias": "charts", - "typeAlias": "basic_timeseries", "type": "timeseries", - "title": "New widget", "sizeX": 18, "sizeY": 6, "config": { @@ -515,7 +527,16 @@ "axisPosition": "top", "showLabels": true }, - "smoothLines": true + "smoothLines": true, + "showLegend": true, + "legendConfig": { + "direction": "column", + "position": "bottom", + "showMin": true, + "showMax": true, + "showAvg": true, + "showTotal": false + } }, "title": "Humidity", "dropShadow": true, @@ -533,25 +554,13 @@ "widgetStyle": {}, "useDashboardTimewindow": false, "displayTimewindow": true, - "showLegend": true, - "legendConfig": { - "direction": "column", - "position": "bottom", - "showMin": true, - "showMax": true, - "showAvg": true, - "showTotal": false - }, "actions": {} }, - "id": "07f49fd5-a73b-d74c-c220-362c20af81f4" + "id": "07f49fd5-a73b-d74c-c220-362c20af81f4", + "typeFullFqn": "system.charts.basic_timeseries" }, "c4631f94-2db3-523b-4d09-2a1a0a75d93f": { - "isSystemType": true, - "bundleAlias": "input_widgets", - "typeAlias": "update_multiple_attributes", "type": "latest", - "title": "New widget", "sizeX": 6, "sizeY": 6, "config": { @@ -626,8 +635,27 @@ } ], "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { - "timewindowMs": 60000 + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085177686, + "endTimeMs": 1694171577686 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 } }, "showTitle": true, @@ -661,14 +689,11 @@ "titleTooltip": "", "displayTimewindow": true }, - "id": "c4631f94-2db3-523b-4d09-2a1a0a75d93f" + "id": "c4631f94-2db3-523b-4d09-2a1a0a75d93f", + "typeFullFqn": "system.input_widgets.update_multiple_attributes" }, "3da9a9a1-0b9a-2e1f-0dcb-0ff34a695abb": { - "isSystemType": true, - "bundleAlias": "maps_v2", - "typeAlias": "openstreetmap", "type": "latest", - "title": "New widget", "sizeX": 13, "sizeY": 6, "config": { @@ -725,8 +750,27 @@ } ], "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { - "timewindowMs": 60000 + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085177686, + "endTimeMs": 1694171577686 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 } }, "showTitle": false, @@ -802,14 +846,11 @@ "titleTooltip": "", "displayTimewindow": true }, - "id": "3da9a9a1-0b9a-2e1f-0dcb-0ff34a695abb" + "id": "3da9a9a1-0b9a-2e1f-0dcb-0ff34a695abb", + "typeFullFqn": "system.maps_v2.openstreetmap" }, "00fb2742-ba1f-7e43-673f-d6c08b72ed06": { - "isSystemType": true, - "bundleAlias": "input_widgets", - "typeAlias": "markers_placement_openstreetmap", "type": "latest", - "title": "New widget", "sizeX": 24, "sizeY": 12, "config": { @@ -864,8 +905,27 @@ } ], "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { - "timewindowMs": 60000 + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085177686, + "endTimeMs": 1694171577686 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 } }, "showTitle": false, @@ -937,14 +997,11 @@ "titleTooltip": "", "displayTimewindow": true }, - "id": "00fb2742-ba1f-7e43-673f-d6c08b72ed06" + "id": "00fb2742-ba1f-7e43-673f-d6c08b72ed06", + "typeFullFqn": "system.input_widgets.markers_placement_openstreetmap" }, "0a430429-9078-9ae6-2b67-e4a15a2bf8bf": { - "isSystemType": true, - "bundleAlias": "input_widgets", - "typeAlias": "markers_placement_openstreetmap", "type": "latest", - "title": "New widget", "sizeX": 6, "sizeY": 6, "config": { @@ -999,8 +1056,27 @@ } ], "timewindow": { + "displayValue": "", + "selectedTab": 0, "realtime": { - "timewindowMs": 60000 + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1694085177686, + "endTimeMs": 1694171577686 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 } }, "showTitle": false, @@ -1073,7 +1149,8 @@ "titleTooltip": "", "displayTimewindow": true }, - "id": "0a430429-9078-9ae6-2b67-e4a15a2bf8bf" + "id": "0a430429-9078-9ae6-2b67-e4a15a2bf8bf", + "typeFullFqn": "system.input_widgets.markers_placement_openstreetmap" } }, "states": { @@ -1110,7 +1187,8 @@ "autoFillHeight": true, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "margin": 10 + "margin": 10, + "outerMargin": true } } } @@ -1136,7 +1214,8 @@ "autoFillHeight": true, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "margin": 10 + "margin": 10, + "outerMargin": true } } } @@ -1182,7 +1261,8 @@ "autoFillHeight": true, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "margin": 10 + "margin": 10, + "outerMargin": true } } } @@ -1195,8 +1275,10 @@ "filter": { "type": "deviceType", "resolveMultiple": true, - "deviceType": "thermostat", - "deviceNameFilter": "" + "deviceNameFilter": "", + "deviceTypes": [ + "thermostat" + ] } }, "12ae98c7-1ea2-52cf-64d5-763e9d993547": { @@ -1245,5 +1327,6 @@ }, "filters": {} }, + "externalId": null, "name": "Thermostats" } \ No newline at end of file diff --git a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json index de71ced3a5..790c9d36c7 100644 --- a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json +++ b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json @@ -48,8 +48,8 @@ "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", "debugMode": false, + "configurationVersion": 1, "configuration": { - "version": 1, "scope": "CLIENT_SCOPE", "notifyDevice": "false", "sendAttributesUpdatedNotification": "false", diff --git a/application/src/main/data/json/system/widget_types/compass.json b/application/src/main/data/json/system/widget_types/compass.json index 03f8d6a0aa..79e92d7d57 100644 --- a/application/src/main/data/json/system/widget_types/compass.json +++ b/application/src/main/data/json/system/widget_types/compass.json @@ -15,7 +15,7 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-analogue-compass-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"minorTicks\":22,\"needleCircleSize\":15,\"showBorder\":true,\"borderOuterWidth\":10,\"colorPlate\":\"#222\",\"colorMajorTicks\":\"#f5f5f5\",\"colorMinorTicks\":\"#ddd\",\"colorNeedle\":\"#f08080\",\"colorNeedleCircle\":\"#e8e8e8\",\"colorBorder\":\"#ccc\",\"majorTickFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ccc\"},\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"animationTarget\":\"needle\",\"majorTicks\":[\"N\",\"NE\",\"E\",\"SE\",\"S\",\"SW\",\"W\",\"NW\"]},\"title\":\"Compass\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"minorTicks\":22,\"needleCircleSize\":15,\"showBorder\":true,\"borderOuterWidth\":10,\"colorPlate\":\"#222\",\"colorMajorTicks\":\"#f5f5f5\",\"colorMinorTicks\":\"#ddd\",\"colorNeedle\":\"#f08080\",\"colorNeedleCircle\":\"#e8e8e8\",\"colorBorder\":\"#ccc\",\"majorTickFont\":{\"family\":\"Roboto\",\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#ccc\"},\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"animationTarget\":\"needle\",\"majorTicks\":[\"N\",\"NE\",\"E\",\"SE\",\"S\",\"SW\",\"W\",\"NW\"]},\"title\":\"Compass\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\"}" }, "externalId": null } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/radial_gauge.json b/application/src/main/data/json/system/widget_types/radial_gauge.json index d0111e2866..afd8697cee 100644 --- a/application/src/main/data/json/system/widget_types/radial_gauge.json +++ b/application/src/main/data/json/system/widget_types/radial_gauge.json @@ -15,7 +15,7 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-analogue-radial-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() * 50 - 25;\\nif (value < -100) {\\n\\tvalue = -100;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":10,\"highlights\":[],\"showUnitTitle\":true,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":10,\"valueInt\":3,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":36,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"minValue\":-100,\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "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() * 50 - 25;\\nif (value < -100) {\\n\\tvalue = -100;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":10,\"highlights\":[],\"showUnitTitle\":true,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":10,\"valueInt\":3,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":500,\"animationRule\":\"cycle\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"numbersFont\":{\"family\":\"Roboto\",\"size\":18,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":36,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"minValue\":-100,\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\"}" }, "externalId": null } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/speed_gauge.json b/application/src/main/data/json/system/widget_types/speed_gauge.json index 3e534c217a..752211a3cf 100644 --- a/application/src/main/data/json/system/widget_types/speed_gauge.json +++ b/application/src/main/data/json/system/widget_types/speed_gauge.json @@ -15,7 +15,7 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-analogue-radial-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() * 50 - 25;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 220) {\\n\\tvalue = 220;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":180,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":false,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":80,\"to\":120,\"color\":\"#fdd835\"},{\"color\":\"#e57373\",\"from\":120,\"to\":180}],\"showUnitTitle\":false,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"minValue\":0,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"MPH\",\"majorTicksCount\":9,\"numbersFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"size\":32,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\",\"family\":\"Segment7Standard\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Speed gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "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() * 50 - 25;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 220) {\\n\\tvalue = 220;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":180,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":false,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":80,\"to\":120,\"color\":\"#fdd835\"},{\"color\":\"#e57373\",\"from\":120,\"to\":180}],\"showUnitTitle\":false,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"minValue\":0,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"MPH\",\"majorTicksCount\":9,\"numbersFont\":{\"family\":\"Roboto\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"size\":32,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\",\"family\":\"Segment7Standard\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Speed gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\"}" }, "externalId": null } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/temperature_radial_gauge.json b/application/src/main/data/json/system/widget_types/temperature_radial_gauge.json index 5c3227e110..1f3598f702 100644 --- a/application/src/main/data/json/system/widget_types/temperature_radial_gauge.json +++ b/application/src/main/data/json/system/widget_types/temperature_radial_gauge.json @@ -15,7 +15,7 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-analogue-radial-gauge-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":60,\"startAngle\":67.5,\"ticksAngle\":225,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":-60,\"to\":-50,\"color\":\"#42a5f5\"},{\"from\":-50,\"to\":-40,\"color\":\"rgba(66, 165, 245, 0.83)\"},{\"from\":-40,\"to\":-30,\"color\":\"rgba(66, 165, 245, 0.66)\"},{\"from\":-30,\"to\":-20,\"color\":\"rgba(66, 165, 245, 0.5)\"},{\"from\":-20,\"to\":-10,\"color\":\"rgba(66, 165, 245, 0.33)\"},{\"from\":-10,\"to\":0,\"color\":\"rgba(66, 165, 245, 0.16)\"},{\"from\":0,\"to\":10,\"color\":\"rgba(229, 115, 115, 0.16)\"},{\"from\":10,\"to\":20,\"color\":\"rgba(229, 115, 115, 0.33)\"},{\"from\":20,\"to\":30,\"color\":\"rgba(229, 115, 115, 0.5)\"},{\"from\":30,\"to\":40,\"color\":\"rgba(229, 115, 115, 0.66)\"},{\"from\":40,\"to\":50,\"color\":\"rgba(229, 115, 115, 0.83)\"},{\"from\":50,\"to\":60,\"color\":\"#e57373\"}],\"showUnitTitle\":true,\"colorPlate\":\"#cfd8dc\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"valueDec\":1,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1000,\"animationRule\":\"bounce\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"°C\",\"majorTicksCount\":12,\"numbersFont\":{\"family\":\"Roboto\",\"size\":20,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":30,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"unitTitle\":\"Temperature\",\"minValue\":-60},\"title\":\"Temperature radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"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\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":60,\"startAngle\":67.5,\"ticksAngle\":225,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":-60,\"to\":-50,\"color\":\"#42a5f5\"},{\"from\":-50,\"to\":-40,\"color\":\"rgba(66, 165, 245, 0.83)\"},{\"from\":-40,\"to\":-30,\"color\":\"rgba(66, 165, 245, 0.66)\"},{\"from\":-30,\"to\":-20,\"color\":\"rgba(66, 165, 245, 0.5)\"},{\"from\":-20,\"to\":-10,\"color\":\"rgba(66, 165, 245, 0.33)\"},{\"from\":-10,\"to\":0,\"color\":\"rgba(66, 165, 245, 0.16)\"},{\"from\":0,\"to\":10,\"color\":\"rgba(229, 115, 115, 0.16)\"},{\"from\":10,\"to\":20,\"color\":\"rgba(229, 115, 115, 0.33)\"},{\"from\":20,\"to\":30,\"color\":\"rgba(229, 115, 115, 0.5)\"},{\"from\":30,\"to\":40,\"color\":\"rgba(229, 115, 115, 0.66)\"},{\"from\":40,\"to\":50,\"color\":\"rgba(229, 115, 115, 0.83)\"},{\"from\":50,\"to\":60,\"color\":\"#e57373\"}],\"showUnitTitle\":true,\"colorPlate\":\"#cfd8dc\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"valueDec\":1,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1000,\"animationRule\":\"bounce\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"°C\",\"majorTicksCount\":12,\"numbersFont\":{\"family\":\"Roboto\",\"size\":20,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":30,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"unitTitle\":\"Temperature\",\"minValue\":-60},\"title\":\"Temperature radial gauge\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\"}" }, "externalId": null } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/thermometer_scale.json b/application/src/main/data/json/system/widget_types/thermometer_scale.json index f2e5c3fedf..68c7e22d2e 100644 --- a/application/src/main/data/json/system/widget_types/thermometer_scale.json +++ b/application/src/main/data/json/system/widget_types/thermometer_scale.json @@ -15,7 +15,7 @@ "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-analogue-linear-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() * 30 - 15;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"defaultColor\":\"#e64a19\",\"barStrokeWidth\":2.5,\"colorBar\":\"rgba(255, 255, 255, 0.4)\",\"colorBarEnd\":\"rgba(221, 221, 221, 0.38)\",\"showUnitTitle\":true,\"minorTicks\":2,\"valueBox\":true,\"valueInt\":3,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"colorNeedleShadowUp\":\"rgba(2,255,255,0.2)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"highlightsWidth\":10,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"showBorder\":false,\"majorTicksCount\":8,\"numbersFont\":{\"family\":\"Arial\",\"size\":18,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#78909c\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":26,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#37474f\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":40,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#444\",\"shadowColor\":\"rgba(0,0,0,0.3)\"},\"minValue\":-60,\"highlights\":[{\"from\":-60,\"to\":-40,\"color\":\"#90caf9\"},{\"from\":-40,\"to\":-20,\"color\":\"rgba(144, 202, 249, 0.66)\"},{\"from\":-20,\"to\":0,\"color\":\"rgba(144, 202, 249, 0.33)\"},{\"from\":0,\"to\":20,\"color\":\"rgba(244, 67, 54, 0.2)\"},{\"from\":20,\"to\":40,\"color\":\"rgba(244, 67, 54, 0.4)\"},{\"from\":40,\"to\":60,\"color\":\"rgba(244, 67, 54, 0.6)\"},{\"from\":60,\"to\":80,\"color\":\"rgba(244, 67, 54, 0.8)\"},{\"from\":80,\"to\":100,\"color\":\"#f44336\"}],\"unitTitle\":\"Temperature\",\"units\":\"°C\",\"colorBarProgress\":\"#90caf9\",\"colorBarProgressEnd\":\"#f44336\",\"colorBarStroke\":\"#b0bec5\",\"valueDec\":1},\"title\":\"Thermometer scale\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "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() * 30 - 15;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":100,\"defaultColor\":\"#e64a19\",\"barStrokeWidth\":2.5,\"colorBar\":\"rgba(255, 255, 255, 0.4)\",\"colorBarEnd\":\"rgba(221, 221, 221, 0.38)\",\"showUnitTitle\":true,\"minorTicks\":2,\"valueBox\":true,\"valueInt\":3,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"colorNeedleShadowUp\":\"rgba(2,255,255,0.2)\",\"colorNeedleShadowDown\":\"rgba(188,143,143,0.45)\",\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"highlightsWidth\":10,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"showBorder\":false,\"majorTicksCount\":8,\"numbersFont\":{\"family\":\"Arial\",\"size\":18,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"Roboto\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#78909c\"},\"unitsFont\":{\"family\":\"Roboto\",\"size\":26,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#37474f\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":40,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#444\",\"shadowColor\":\"rgba(0,0,0,0.3)\"},\"minValue\":-60,\"highlights\":[{\"from\":-60,\"to\":-40,\"color\":\"#90caf9\"},{\"from\":-40,\"to\":-20,\"color\":\"rgba(144, 202, 249, 0.66)\"},{\"from\":-20,\"to\":0,\"color\":\"rgba(144, 202, 249, 0.33)\"},{\"from\":0,\"to\":20,\"color\":\"rgba(244, 67, 54, 0.2)\"},{\"from\":20,\"to\":40,\"color\":\"rgba(244, 67, 54, 0.4)\"},{\"from\":40,\"to\":60,\"color\":\"rgba(244, 67, 54, 0.6)\"},{\"from\":60,\"to\":80,\"color\":\"rgba(244, 67, 54, 0.8)\"},{\"from\":80,\"to\":100,\"color\":\"#f44336\"}],\"unitTitle\":\"Temperature\",\"units\":\"°C\",\"colorBarProgress\":\"#90caf9\",\"colorBarProgressEnd\":\"#f44336\",\"colorBarStroke\":\"#b0bec5\",\"valueDec\":1},\"title\":\"Thermometer scale\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"decimals\":0,\"noDataDisplayMessage\":\"\"}" }, "externalId": null } \ No newline at end of file diff --git a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json index 7d1deb7e41..d03fcd0b7b 100644 --- a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json +++ b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json @@ -32,8 +32,8 @@ "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", "debugMode": false, + "configurationVersion": 1, "configuration": { - "version": 1, "scope": "CLIENT_SCOPE", "notifyDevice": "false", "sendAttributesUpdatedNotification": "false", diff --git a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json index 57c3a86dfb..419d30ed4c 100644 --- a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json +++ b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json @@ -31,8 +31,8 @@ "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", "debugMode": false, + "configurationVersion": 1, "configuration": { - "version": 1, "scope": "CLIENT_SCOPE", "notifyDevice": "false", "sendAttributesUpdatedNotification": "false", diff --git a/application/src/main/data/upgrade/3.5.1/schema_update.sql b/application/src/main/data/upgrade/3.5.1/schema_update.sql index c8efd4390e..5aab5d740b 100644 --- a/application/src/main/data/upgrade/3.5.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.5.1/schema_update.sql @@ -127,6 +127,8 @@ UPDATE resource ALTER TABLE notification_request ALTER COLUMN info SET DATA TYPE varchar(1000000); +DELETE FROM alarm WHERE tenant_id NOT IN (SELECT id FROM tenant); + CREATE TABLE IF NOT EXISTS alarm_types ( tenant_id uuid NOT NULL, type varchar(255) NOT NULL, diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java index 112c406d19..d5f150ce24 100644 --- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java +++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java @@ -30,13 +30,12 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.api.NodeDefinition; import org.thingsboard.rule.engine.api.RuleNode; -import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.common.data.msg.TbNodeConnectionType; -import org.thingsboard.rule.engine.api.TbVersionedNode; import org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode; import org.thingsboard.rule.engine.filter.TbOriginatorTypeSwitchNode; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -89,15 +88,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe try { var clazz = Class.forName(clazzName); RuleNode annotation = clazz.getAnnotation(RuleNode.class); - boolean versioned = false; - if (annotation.version() > 0) { // No need to process nodes that has version = 0; - if (TbVersionedNode.class.isAssignableFrom(clazz)) { - versioned = true; - } else { - log.error("RuleNode [{}] has version {} but does not implement TbVersionedNode interface! Any update procedures for this rule node will be skipped!", clazzName, annotation.version()); - } - } - ruleNodeClasses.put(clazzName, new RuleNodeClassInfo(clazz, annotation, versioned)); + ruleNodeClasses.put(clazzName, new RuleNodeClassInfo(clazz, annotation)); } catch (Exception e) { log.warn("Failed to create instance of rule node type: {} due to: ", clazzName, e); } @@ -194,7 +185,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe scannedComponent.setType(type); Class clazz = def.getClazz(); RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class); - scannedComponent.setConfigurationVersion(def.isVersioned() ? def.getCurrentVersion() : 0); + scannedComponent.setConfigurationVersion(def.getCurrentVersion()); scannedComponent.setName(ruleNodeAnnotation.name()); scannedComponent.setScope(ruleNodeAnnotation.scope()); scannedComponent.setClusteringMode(ruleNodeAnnotation.clusteringMode()); diff --git a/application/src/main/java/org/thingsboard/server/service/component/RuleNodeClassInfo.java b/application/src/main/java/org/thingsboard/server/service/component/RuleNodeClassInfo.java index 56fd367820..317ec4ab9d 100644 --- a/application/src/main/java/org/thingsboard/server/service/component/RuleNodeClassInfo.java +++ b/application/src/main/java/org/thingsboard/server/service/component/RuleNodeClassInfo.java @@ -23,7 +23,6 @@ public class RuleNodeClassInfo { private final Class clazz; private final RuleNode annotation; - private final boolean versioned; public String getClassName(){ return clazz.getName(); @@ -37,4 +36,8 @@ public class RuleNodeClassInfo { return annotation.version(); } + public boolean isVersioned() { + return annotation.version() > 0; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java index 509e0fdbef..a37e03de1b 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java @@ -33,7 +33,10 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredential; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode; +import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; +import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.device.data.PowerMode; +import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; @@ -45,6 +48,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumnType; +import org.thingsboard.server.common.data.transport.snmp.SnmpProtocolVersion; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; @@ -76,18 +80,18 @@ public class DeviceBulkImportService extends AbstractBulkImportService { private final Lock findOrCreateDeviceProfileLock = new ReentrantLock(); @Override - protected void setEntityFields(Device entity, Map fields) { - ObjectNode additionalInfo = getOrCreateAdditionalInfoObj(entity); + protected void setEntityFields(Device device, Map fields) { + ObjectNode additionalInfo = getOrCreateAdditionalInfoObj(device); fields.forEach((columnType, value) -> { switch (columnType) { case NAME: - entity.setName(value); + device.setName(value); break; case TYPE: - entity.setType(value); + device.setType(value); break; case LABEL: - entity.setLabel(value); + device.setLabel(value); break; case DESCRIPTION: additionalInfo.set("description", new TextNode(value)); @@ -96,16 +100,17 @@ public class DeviceBulkImportService extends AbstractBulkImportService { additionalInfo.set("gateway", BooleanNode.valueOf(Boolean.parseBoolean(value))); break; } - entity.setAdditionalInfo(additionalInfo); + device.setAdditionalInfo(additionalInfo); }); + setUpDeviceConfiguration(device, fields); } @Override @SneakyThrows - protected Device saveEntity(SecurityUser user, Device entity, Map fields) { + protected Device saveEntity(SecurityUser user, Device device, Map fields) { DeviceCredentials deviceCredentials; try { - deviceCredentials = createDeviceCredentials(entity.getTenantId(), entity.getId(), fields); + deviceCredentials = createDeviceCredentials(device.getTenantId(), device.getId(), fields); deviceCredentialsService.formatCredentials(deviceCredentials); } catch (Exception e) { throw new DeviceCredentialsValidationException("Invalid device credentials: " + e.getMessage()); @@ -113,15 +118,15 @@ public class DeviceBulkImportService extends AbstractBulkImportService { DeviceProfile deviceProfile; if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.LWM2M_CREDENTIALS) { - deviceProfile = setUpLwM2mDeviceProfile(entity.getTenantId(), entity); - } else if (StringUtils.isNotEmpty(entity.getType())) { - deviceProfile = deviceProfileService.findOrCreateDeviceProfile(entity.getTenantId(), entity.getType()); + deviceProfile = setUpLwM2mDeviceProfile(device.getTenantId(), device); + } else if (StringUtils.isNotEmpty(device.getType())) { + deviceProfile = deviceProfileService.findOrCreateDeviceProfile(device.getTenantId(), device.getType()); } else { - deviceProfile = deviceProfileService.findDefaultDeviceProfile(entity.getTenantId()); + deviceProfile = deviceProfileService.findDefaultDeviceProfile(device.getTenantId()); } - entity.setDeviceProfileId(deviceProfile.getId()); + device.setDeviceProfileId(deviceProfile.getId()); - return tbDeviceService.saveDeviceWithCredentials(entity, deviceCredentials, user); + return tbDeviceService.saveDeviceWithCredentials(device, deviceCredentials, user); } @Override @@ -136,6 +141,22 @@ public class DeviceBulkImportService extends AbstractBulkImportService { entity.setCustomerId(user.getCustomerId()); } + private void setUpDeviceConfiguration(Device device, Map fields) { + if (fields.containsKey(BulkImportColumnType.SNMP_HOST)) { + SnmpDeviceTransportConfiguration transportConfiguration = new SnmpDeviceTransportConfiguration(); + transportConfiguration.setHost(fields.get(BulkImportColumnType.SNMP_HOST)); + transportConfiguration.setPort(Optional.ofNullable(fields.get(BulkImportColumnType.SNMP_PORT)) + .map(Integer::parseInt).orElse(161)); + transportConfiguration.setProtocolVersion(Optional.ofNullable(fields.get(BulkImportColumnType.SNMP_VERSION)) + .map(version -> SnmpProtocolVersion.valueOf(version.toUpperCase())).orElse(SnmpProtocolVersion.V2C)); + transportConfiguration.setCommunity(fields.getOrDefault(BulkImportColumnType.SNMP_COMMUNITY_STRING, "public")); + + DeviceData deviceData = new DeviceData(); + deviceData.setTransportConfiguration(transportConfiguration); + device.setDeviceData(deviceData); + } + } + @SneakyThrows private DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceId deviceId, Map fields) { DeviceCredentials credentials = new DeviceCredentials(); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index fd341365cd..cfb92d0bfd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -614,13 +614,13 @@ public final class EdgeGrpcSession implements Closeable { case EDGE: return ctx.getEdgeProcessor().convertEdgeEventToDownlink(edgeEvent); case DEVICE: - return ctx.getDeviceProcessor().convertDeviceEventToDownlink(edgeEvent); + return ctx.getDeviceProcessor().convertDeviceEventToDownlink(edgeEvent, this.edge.getId(), this.edgeVersion); case DEVICE_PROFILE: - return ctx.getDeviceProfileProcessor().convertDeviceProfileEventToDownlink(edgeEvent); + return ctx.getDeviceProfileProcessor().convertDeviceProfileEventToDownlink(edgeEvent, this.edge.getId(), this.edgeVersion); case ASSET_PROFILE: - return ctx.getAssetProfileProcessor().convertAssetProfileEventToDownlink(edgeEvent); + return ctx.getAssetProfileProcessor().convertAssetProfileEventToDownlink(edgeEvent, this.edge.getId(), this.edgeVersion); case ASSET: - return ctx.getAssetProcessor().convertAssetEventToDownlink(edgeEvent); + return ctx.getAssetProcessor().convertAssetEventToDownlink(edgeEvent, this.edge.getId(), this.edgeVersion); case ENTITY_VIEW: return ctx.getEntityViewProcessor().convertEntityViewEventToDownlink(edgeEvent); case DASHBOARD: diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java index e3c8d54683..af574b4b75 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java @@ -35,6 +35,13 @@ public class WidgetTypeMsgConstructor { .setIdLSB(widgetTypeDetails.getId().getId().getLeastSignificantBits()); if (widgetTypeDetails.getFqn() != null) { builder.setFqn(widgetTypeDetails.getFqn()); + if (widgetTypeDetails.getFqn().contains(".")) { + String[] aliases = widgetTypeDetails.getFqn().split("\\.", 2); + if (aliases.length == 2) { + builder.setBundleAlias(aliases[0]); + builder.setAlias(aliases[1]); + } + } } if (widgetTypeDetails.getName() != null) { builder.setName(widgetTypeDetails.getName()); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 54aa99a5fd..328a64efe2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -79,6 +79,7 @@ import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.TbQueueCallback; @@ -596,4 +597,57 @@ public abstract class BaseEdgeProcessor { } }); } + + protected AssetProfile checkIfAssetProfileDefaultFieldsAssignedToEdge(TenantId tenantId, EdgeId edgeId, AssetProfile assetProfile, EdgeVersion edgeVersion) { + switch (edgeVersion) { + case V_3_3_3: + case V_3_3_0: + case V_3_4_0: + if (assetProfile.getDefaultDashboardId() != null + && isEntityNotAssignedToEdge(tenantId, assetProfile.getDefaultDashboardId(), edgeId)) { + assetProfile.setDefaultDashboardId(null); + } + if (assetProfile.getDefaultEdgeRuleChainId() != null + && isEntityNotAssignedToEdge(tenantId, assetProfile.getDefaultEdgeRuleChainId(), edgeId)) { + assetProfile.setDefaultEdgeRuleChainId(null); + } + break; + } + return assetProfile; + } + + protected DeviceProfile checkIfDeviceProfileDefaultFieldsAssignedToEdge(TenantId tenantId, EdgeId edgeId, DeviceProfile deviceProfile, EdgeVersion edgeVersion) { + switch (edgeVersion) { + case V_3_3_3: + case V_3_3_0: + case V_3_4_0: + if (deviceProfile.getDefaultDashboardId() != null + && isEntityNotAssignedToEdge(tenantId, deviceProfile.getDefaultDashboardId(), edgeId)) { + deviceProfile.setDefaultDashboardId(null); + } + if (deviceProfile.getDefaultEdgeRuleChainId() != null + && isEntityNotAssignedToEdge(tenantId, deviceProfile.getDefaultEdgeRuleChainId(), edgeId)) { + deviceProfile.setDefaultEdgeRuleChainId(null); + } + break; + } + return deviceProfile; + } + + private boolean isEntityNotAssignedToEdge(TenantId tenantId, EntityId entityId, EdgeId edgeId) { + PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); + PageData pageData; + do { + pageData = edgeService.findRelatedEdgeIdsByEntityId(tenantId, entityId, pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + if (pageData.getData().contains(edgeId)) { + return false; + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + return true; + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index aa0679a632..6e3c56d3b3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -37,6 +38,7 @@ import org.thingsboard.server.dao.asset.BaseAssetService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -106,7 +108,7 @@ public class AssetEdgeProcessor extends BaseAssetProcessor { } } - public DownlinkMsg convertAssetEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertAssetEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) { AssetId assetId = new AssetId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -125,6 +127,7 @@ public class AssetEdgeProcessor extends BaseAssetProcessor { .addAssetUpdateMsg(assetUpdateMsg); if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId()); + assetProfile = checkIfAssetProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, assetProfile, edgeVersion); builder.addAssetProfileUpdateMsg(assetProfileMsgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile)); } downlinkMsg = builder.build(); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java index 3fa69ed078..aceab6692c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; @@ -36,6 +37,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -94,7 +96,7 @@ public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor { } } - public DownlinkMsg convertAssetProfileEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertAssetProfileEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) { AssetProfileId assetProfileId = new AssetProfileId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -103,6 +105,7 @@ public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor { AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), assetProfileId); if (assetProfile != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + assetProfile = checkIfAssetProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, assetProfile, edgeVersion); AssetProfileUpdateMsg assetProfileUpdateMsg = assetProfileMsgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile); downlinkMsg = DownlinkMsg.newBuilder() diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index 15edd4215d..7b1da9a297 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.rpc.RpcError; @@ -49,6 +50,7 @@ import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; @@ -202,7 +204,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { return Futures.immediateFuture(null); } - public DownlinkMsg convertDeviceEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertDeviceEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) { DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -221,6 +223,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { .addDeviceUpdateMsg(deviceUpdateMsg); if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); + deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion); builder.addDeviceProfileUpdateMsg(deviceProfileMsgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); } downlinkMsg = builder.build(); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java index 8f1e711775..d482f42cf6 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; @@ -36,6 +37,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -94,7 +96,7 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor { } } - public DownlinkMsg convertDeviceProfileEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertDeviceProfileEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) { DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -103,6 +105,7 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor { DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), deviceProfileId); if (deviceProfile != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion); DeviceProfileUpdateMsg deviceProfileUpdateMsg = deviceProfileMsgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile); downlinkMsg = DownlinkMsg.newBuilder() diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index 40596e03e9..7f0b70cb27 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -28,7 +28,6 @@ import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; @@ -104,15 +103,15 @@ public class InstallScripts { @Autowired private ResourceService resourceService; - private Path getTenantRuleChainsDir() { + Path getTenantRuleChainsDir() { return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR); } - private Path getDeviceProfileDefaultRuleChainTemplateFilePath() { + Path getDeviceProfileDefaultRuleChainTemplateFilePath() { return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DEVICE_PROFILE_DIR, "rule_chain_template.json"); } - private Path getEdgeRuleChainsDir() { + Path getEdgeRuleChainsDir() { return Paths.get(getDataDir(), JSON_DIR, EDGE_DIR, RULE_CHAINS_DIR); } @@ -148,8 +147,8 @@ public class InstallScripts { } private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) throws IOException { - try (DirectoryStream dirStream = Files.newDirectoryStream(ruleChainsPath, path -> path.toString().endsWith(InstallScripts.JSON_EXT))) { - dirStream.forEach( + findRuleChainsFromPath(ruleChainsPath) + .forEach( path -> { try { createRuleChainFromFile(tenantId, path, null); @@ -157,9 +156,15 @@ public class InstallScripts { log.error("Unable to load rule chain from json: [{}]", path.toString()); throw new RuntimeException("Unable to load rule chain from json", e); } - } - ); + }); + } + + List findRuleChainsFromPath(Path ruleChainsPath) throws IOException { + List paths = new ArrayList<>(); + try (DirectoryStream dirStream = Files.newDirectoryStream(ruleChainsPath, path -> path.toString().endsWith(InstallScripts.JSON_EXT))) { + dirStream.forEach(paths::add); } + return paths; } public RuleChain createDefaultRuleChain(TenantId tenantId, String ruleChainName) throws IOException { diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 2e2b3e96be..9e942b481e 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -731,8 +731,8 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService break; case "3.5.1": try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { - log.info("Updating schema ..."); if (isOldSchema(conn, 3005001)) { + log.info("Updating schema ..."); schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.5.1", SCHEMA_UPDATE_SQL); loadSql(schemaUpdateFile, conn); @@ -756,14 +756,23 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);"); } catch (Exception e) { } + try { + conn.createStatement().execute("UPDATE rule_node SET " + + "configuration = (configuration::jsonb || '{\"updateAttributesOnlyOnValueChange\": \"false\"}'::jsonb)::varchar, " + + "configuration_version = 1 " + + "WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode' AND configuration_version < 1;"); + } catch (Exception e) { + } try { conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';"); } catch (Exception e) { } conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005002;"); + log.info("Schema updated to version 3.5.2."); + } else { + log.info("Skip schema re-update to version 3.5.2. Use env flag 'SKIP_SCHEMA_VERSION_CHECK' to force the re-update."); } - log.info("Schema updated."); } catch (Exception e) { log.error("Failed updating schema!!!", e); } 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 e9ee7512c7..8c3b78040b 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 @@ -27,7 +27,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.rule.engine.api.TbVersionedNode; +import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.flow.TbRuleChainInputNode; import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration; import org.thingsboard.rule.engine.profile.TbDeviceProfileNode; @@ -78,6 +78,7 @@ import org.thingsboard.server.dao.model.sql.DeviceProfileEntity; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.sql.JpaExecutorService; import org.thingsboard.server.dao.sql.device.DeviceProfileRepository; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; @@ -88,9 +89,7 @@ import org.thingsboard.server.service.install.SystemDataLoaderService; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; @@ -103,6 +102,8 @@ import static org.thingsboard.server.common.data.StringUtils.isBlank; @Slf4j public class DefaultDataUpdateService implements DataUpdateService { + private static final int MAX_PENDING_SAVE_RULE_NODE_FUTURES = 100; + @Autowired private TenantService tenantService; @@ -155,6 +156,9 @@ public class DefaultDataUpdateService implements DataUpdateService { @Autowired private EdgeEventDao edgeEventDao; + @Autowired + JpaExecutorService jpaExecutorService; + @Override public void updateData(String fromVersion) throws Exception { switch (fromVersion) { @@ -227,13 +231,15 @@ public class DefaultDataUpdateService implements DataUpdateService { @Override public void upgradeRuleNodes() { try { + var futures = new ArrayList>(100); + int totalRuleNodesUpgraded = 0; log.info("Starting rule nodes upgrade ..."); var nodeClassToVersionMap = componentDiscoveryService.getVersionedNodes(); log.debug("Found {} versioned nodes to check for upgrade!", nodeClassToVersionMap.size()); - nodeClassToVersionMap.forEach(clazz -> { - var ruleNodeType = clazz.getClassName(); - var ruleNodeTypeForLogs = clazz.getSimpleName(); - var toVersion = clazz.getCurrentVersion(); + for (var ruleNodeClassInfo : nodeClassToVersionMap) { + var ruleNodeType = ruleNodeClassInfo.getClassName(); + var ruleNodeTypeForLogs = ruleNodeClassInfo.getSimpleName(); + var toVersion = ruleNodeClassInfo.getCurrentVersion(); log.debug("Going to check for nodes with type: {} to upgrade to version: {}.", ruleNodeTypeForLogs, toVersion); var ruleNodesToUpdate = new PageDataIterable<>( pageLink -> ruleChainService.findAllRuleNodesByTypeAndVersionLessThan(ruleNodeType, toVersion, pageLink), 1024 @@ -248,28 +254,44 @@ public class DefaultDataUpdateService implements DataUpdateService { log.debug("Going to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}", ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion); try { - var tbVersionedNode = (TbVersionedNode) clazz.getClazz().getDeclaredConstructor().newInstance(); + var tbVersionedNode = (TbNode) ruleNodeClassInfo.getClazz().getDeclaredConstructor().newInstance(); TbPair upgradeRuleNodeConfigurationResult = tbVersionedNode.upgrade(fromVersion, oldConfiguration); if (upgradeRuleNodeConfigurationResult.getFirst()) { ruleNode.setConfiguration(upgradeRuleNodeConfigurationResult.getSecond()); } ruleNode.setConfigurationVersion(toVersion); - ruleChainService.saveRuleNode(TenantId.SYS_TENANT_ID, ruleNode); - log.debug("Successfully upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}", - ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion); + futures.add(jpaExecutorService.submit(() -> { + ruleChainService.saveRuleNode(TenantId.SYS_TENANT_ID, ruleNode); + log.debug("Successfully upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}", + ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion); + })); + if (futures.size() >= MAX_PENDING_SAVE_RULE_NODE_FUTURES) { + log.info("{} upgraded rule nodes so far ...", + totalRuleNodesUpgraded += awaitFuturesToCompleteAndGetCount(futures)); + futures.clear(); + } } catch (Exception e) { log.warn("Failed to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {} due to: ", ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion, e); } } } - }); - log.info("Finished rule nodes upgrade!"); + } + log.info("Finished rule nodes upgrade. Upgraded rule nodes count: {}", + totalRuleNodesUpgraded + awaitFuturesToCompleteAndGetCount(futures)); } catch (Exception e) { log.error("Unexpected error during rule nodes upgrade: ", e); } } + private int awaitFuturesToCompleteAndGetCount(List> futures) { + try { + return Futures.allAsList(futures).get().size(); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException("Failed to process save rule nodes requests due to: ", e); + } + } + private final PaginatedUpdater deviceProfileEntityDynamicConditionsUpdater = new PaginatedUpdater<>() { 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 fef22acf40..fd5a252cc5 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 @@ -24,10 +24,14 @@ import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.event.ErrorEvent; +import org.thingsboard.server.common.data.event.Event; +import org.thingsboard.server.common.data.event.LifecycleEvent; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.TenantId; @@ -45,7 +49,9 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.EdgeNotificationMsgProto; +import org.thingsboard.server.gen.transport.TransportProtos.ErrorEventProto; import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto; +import org.thingsboard.server.gen.transport.TransportProtos.LifecycleEventProto; import org.thingsboard.server.gen.transport.TransportProtos.LocalSubscriptionServiceMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionMgrMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmDeleteProto; @@ -274,6 +280,10 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService callback.onSuccess(), + callback::onFailure, + actorContext.getDbCallbackExecutor()); + } + private void throwNotHandled(Object msg, TbCallback callback) { log.warn("Message not handled: {}", msg); callback.onFailure(new RuntimeException("Message not handled!")); } + private TenantId toTenantId(long tenantIdMSB, long tenantIdLSB) { + return TenantId.fromUUID(new UUID(tenantIdMSB, tenantIdLSB)); + } + @Override protected void stopMainConsumers() { if (mainConsumer != null) { diff --git a/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java b/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java index 884c614231..56df2caa71 100644 --- a/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java +++ b/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java @@ -20,8 +20,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbVersionedNode; import org.thingsboard.rule.engine.flow.TbRuleChainInputNode; import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration; import org.thingsboard.rule.engine.flow.TbRuleChainOutputNode; @@ -402,7 +402,7 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement var ruleNodeClass = componentDiscoveryService.getRuleNodeInfo(ruleNodeType) .orElseThrow(() -> new RuntimeException("Rule node " + ruleNodeType + " is not supported!")); if (ruleNodeClass.isVersioned()) { - TbVersionedNode tbVersionedNode = (TbVersionedNode) ruleNodeClass.getClazz().getDeclaredConstructor().newInstance(); + TbNode tbVersionedNode = (TbNode) ruleNodeClass.getClazz().getDeclaredConstructor().newInstance(); int fromVersion = node.getConfigurationVersion(); int toVersion = ruleNodeClass.getCurrentVersion(); if (fromVersion < toVersion) { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java index abc90221be..3cf05ac503 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java @@ -48,8 +48,8 @@ public abstract class BaseEntityExportService getSupportedEntityTypes(); - protected void replaceUuidsRecursively(EntitiesExportCtx ctx, JsonNode node, Set skipFieldsSet, Pattern includedFieldsPattern) { - JacksonUtil.replaceUuidsRecursively(node, skipFieldsSet, includedFieldsPattern, uuid -> getExternalIdOrElseInternalByUuid(ctx, uuid)); + protected void replaceUuidsRecursively(EntitiesExportCtx ctx, JsonNode node, Set skippedRootFields, Pattern includedFieldsPattern) { + JacksonUtil.replaceUuidsRecursively(node, skippedRootFields, includedFieldsPattern, uuid -> getExternalIdOrElseInternalByUuid(ctx, uuid), true); } protected Stream toExternalIds(Collection internalIds, Function entityIdCreator, diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java index 0df1e83c7f..d1278bc585 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java @@ -26,8 +26,11 @@ import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; +import java.util.Collections; import java.util.Set; +import static org.thingsboard.server.service.sync.ie.importing.impl.DashboardImportService.WIDGET_CONFIG_PROCESSED_FIELDS_PATTERN; + @Service @TbCoreComponent public class DashboardExportService extends BaseEntityExportService> { @@ -43,7 +46,7 @@ public class DashboardExportService extends BaseEntityExportService castResult = TypeCastUtil.castValue(entry.getValue()); + Pair castResult = TypeCastUtil.castValue(entry.getValue()); entityData.getKvs().put(entry.getKey(), new ParsedValue(castResult.getValue(), castResult.getKey())); } }); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index 87438d39cb..ee82668334 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -388,11 +388,11 @@ public abstract class BaseEntityImportService skipFieldsSet, Pattern includedFieldsPattern, + Set skippedRootFields, Pattern includedFieldsPattern, LinkedHashSet hints) { - JacksonUtil.replaceUuidsRecursively(json, skipFieldsSet, includedFieldsPattern, + JacksonUtil.replaceUuidsRecursively(json, skippedRootFields, includedFieldsPattern, uuid -> idProvider.getInternalIdByUuid(uuid, ctx.isFinalImportAttempt(), hints) - .map(EntityId::getId).orElse(uuid)); + .map(EntityId::getId).orElse(uuid), true); } } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java index eac229f774..658ecf744a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java @@ -36,6 +36,7 @@ import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Optional; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; @Service @@ -44,6 +45,7 @@ import java.util.stream.Collectors; public class DashboardImportService extends BaseEntityImportService> { private static final LinkedHashSet HINTS = new LinkedHashSet<>(Arrays.asList(EntityType.DASHBOARD, EntityType.DEVICE, EntityType.ASSET)); + public static final Pattern WIDGET_CONFIG_PROCESSED_FIELDS_PATTERN = Pattern.compile(".*Id.*"); private final DashboardService dashboardService; @@ -68,7 +70,7 @@ public class DashboardImportService extends BaseEntityImportService + + diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 6e745f063f..306e9a798c 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -991,11 +991,15 @@ transport: # value: "${LWM2M_PROTOCOL_STAGE_THREAD_COUNT:4}" snmp: enabled: "${SNMP_ENABLED:true}" + bind_port: "${SNMP_BIND_PORT:1620}" response_processing: # parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:20}" # to configure SNMP to work over UDP or TCP underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}" + max_request_oids: "${SNMP_MAX_REQUEST_OIDS:100}" + response: + ignore_type_cast_errors: "${SNMP_RESPONSE_IGNORE_TYPE_CAST_ERRORS:false}" stats: enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" 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 525e18240d..237f900d47 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java @@ -66,6 +66,8 @@ import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.PEM_CERT_FI @TestPropertySource(properties = { "device.connectivity.https.enabled=true", + "device.connectivity.http.port=8080", + "device.connectivity.https.port=444", "device.connectivity.mqtts.enabled=true", "device.connectivity.mqtts.pem_cert_file=/tmp/" + PEM_CERT_FILE_NAME, "device.connectivity.coaps.enabled=true", @@ -191,7 +193,7 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://localhost:8080/api/v1/%s/telemetry " + "--header Content-Type:application/json --data \"{temperature:25}\"", credentials.getCredentialsId())); - assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost:443/api/v1/%s/telemetry " + + assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost:444/api/v1/%s/telemetry " + "--header Content-Type:application/json --data \"{temperature:25}\"", credentials.getCredentialsId())); diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerWithDefaultPortTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerWithDefaultPortTest.java new file mode 100644 index 0000000000..8ca0d07540 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerWithDefaultPortTest.java @@ -0,0 +1,144 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.AdditionalAnswers; +import org.mockito.Mockito; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; +import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.dao.device.DeviceDao; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAP; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAPS; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.DOCKER; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.PEM_CERT_FILE_NAME; + +@TestPropertySource(properties = { + "device.connectivity.https.enabled=true", + "device.connectivity.http.port=80", + "device.connectivity.mqtt.enabled=false", + "device.connectivity.mqtts.enabled=false", + "device.connectivity.coap.enabled=false", + "device.connectivity.coaps.enabled=false", +}) +@ContextConfiguration(classes = {DeviceConnectivityControllerWithDefaultPortTest.Config.class}) +@DaoSqlTest +public class DeviceConnectivityControllerWithDefaultPortTest extends AbstractControllerTest { + + ListeningExecutorService executor; + + private Tenant savedTenant; + + static class Config { + @Bean + @Primary + public DeviceDao deviceDao(DeviceDao deviceDao) { + return Mockito.mock(DeviceDao.class, AdditionalAnswers.delegatesTo(deviceDao)); + } + } + + @Before + public void beforeTest() throws Exception { + executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass())); + + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + + User tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + createUserAndLogin(tenantAdmin, "testPassword1"); + } + + @After + public void afterTest() throws Exception { + executor.shutdownNow(); + + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testFetchPublishTelemetryCommandsForDefaultDevice() throws Exception { + Device device = new Device(); + device.setName("My device"); + device.setType("default"); + Device savedDevice = doPost("/api/device", device, Device.class); + JsonNode commands = + doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { + }); + + DeviceCredentials credentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + + assertThat(commands).hasSize(1); + JsonNode httpCommands = commands.get(HTTP); + assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://localhost/api/v1/%s/telemetry " + + "--header Content-Type:application/json --data \"{temperature:25}\"", + credentials.getCredentialsId())); + assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost/api/v1/%s/telemetry " + + "--header Content-Type:application/json --data \"{temperature:25}\"", + credentials.getCredentialsId())); + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java index f7e5f6f6f3..7b7554c559 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java @@ -149,6 +149,19 @@ public class TelemetryControllerTest extends AbstractControllerTest { doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest()); } + @Test + public void testEmptyKeyIsProhibited() throws Exception { + loginTenantAdmin(); + Device device = createDevice(); + String invalidRequestBody = "{\"\": \"value\"}"; + doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", invalidRequestBody, String.class, status().isBadRequest()); + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest()); + + String invalidRequestBody2 = "{\" \": \"value\"}"; + doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", invalidRequestBody2, String.class, status().isBadRequest()); + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody2, String.class, status().isBadRequest()); + } + private Device createDevice() throws Exception { String testToken = "TEST_TOKEN"; diff --git a/application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java index 63b6114072..e958741cd2 100644 --- a/application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/WidgetEdgeTest.java @@ -58,6 +58,7 @@ public class WidgetEdgeTest extends AbstractEdgeTest { descriptor.put("key", "value"); widgetType.setDescriptor(descriptor); widgetType.setDeprecated(true); + widgetType.setFqn("bundle_alias.type_alias"); WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class); Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); @@ -69,6 +70,8 @@ public class WidgetEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedWidgetType.getFqn(), widgetTypeUpdateMsg.getFqn()); Assert.assertEquals(savedWidgetType.getName(), widgetTypeUpdateMsg.getName()); Assert.assertTrue(widgetTypeUpdateMsg.getDeprecated()); + Assert.assertEquals("bundle_alias", widgetTypeUpdateMsg.getBundleAlias()); + Assert.assertEquals("type_alias", widgetTypeUpdateMsg.getAlias()); Assert.assertEquals(JacksonUtil.toJsonNode(widgetTypeUpdateMsg.getDescriptorJson()), savedWidgetType.getDescriptor()); // update widget bundle diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java new file mode 100644 index 0000000000..1c4b481302 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java @@ -0,0 +1,306 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor; + +import org.junit.jupiter.params.provider.Arguments; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.context.annotation.Lazy; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.asset.AssetProfileService; +import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeEventService; +import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; +import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.queue.QueueService; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.tenant.TenantProfileService; +import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.dao.widget.WidgetsBundleService; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.provider.TbQueueProducerProvider; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; +import org.thingsboard.server.service.edge.rpc.constructor.AdminSettingsMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.AlarmMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.AssetMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.AssetProfileMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.CustomerMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.DashboardMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.DeviceMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.DeviceProfileMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.EdgeMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.EntityDataMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.EntityViewMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.OtaPackageMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.QueueMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.RelationMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.RuleChainMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.TenantMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.TenantProfileMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.UserMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.WidgetTypeMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.WidgetsBundleMsgConstructor; +import org.thingsboard.server.service.entitiy.TbNotificationEntityService; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.profile.TbAssetProfileCache; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; +import org.thingsboard.server.service.state.DeviceStateService; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; + +import java.util.UUID; +import java.util.stream.Stream; + +public abstract class BaseEdgeProcessorTest { + + @MockBean + protected TelemetrySubscriptionService tsSubService; + + @MockBean + protected TbNotificationEntityService notificationEntityService; + + @MockBean + protected RuleChainService ruleChainService; + + @MockBean + protected AlarmService alarmService; + + @MockBean + protected DeviceService deviceService; + + @MockBean + protected TbDeviceProfileCache deviceProfileCache; + + @MockBean + protected TbAssetProfileCache assetProfileCache; + + @MockBean + protected DashboardService dashboardService; + + @MockBean + protected AssetService assetService; + + @MockBean + protected EntityViewService entityViewService; + + @MockBean + protected TenantService tenantService; + + @MockBean + protected TenantProfileService tenantProfileService; + + @MockBean + protected EdgeService edgeService; + + @MockBean + protected CustomerService customerService; + + @MockBean + protected UserService userService; + + @MockBean + protected DeviceProfileService deviceProfileService; + + @MockBean + protected AssetProfileService assetProfileService; + + @MockBean + protected RelationService relationService; + + @MockBean + protected DeviceCredentialsService deviceCredentialsService; + + @MockBean + protected AttributesService attributesService; + + @MockBean + protected TbClusterService tbClusterService; + + @MockBean + protected DeviceStateService deviceStateService; + + @MockBean + protected EdgeEventService edgeEventService; + + @MockBean + protected WidgetsBundleService widgetsBundleService; + + @MockBean + protected WidgetTypeService widgetTypeService; + + @MockBean + protected OtaPackageService otaPackageService; + + @MockBean + protected QueueService queueService; + + @MockBean + protected PartitionService partitionService; + + @MockBean + @Lazy + protected TbQueueProducerProvider producerProvider; + + @MockBean + protected DataValidator deviceValidator; + + @MockBean + protected DataValidator deviceProfileValidator; + + @MockBean + protected DataValidator assetValidator; + + @MockBean + protected DataValidator assetProfileValidator; + + @MockBean + protected DataValidator dashboardValidator; + + @MockBean + protected DataValidator entityViewValidator; + + @MockBean + protected EdgeMsgConstructor edgeMsgConstructor; + + @MockBean + protected EntityDataMsgConstructor entityDataMsgConstructor; + + @MockBean + protected RuleChainMsgConstructor ruleChainMsgConstructor; + + @MockBean + protected AlarmMsgConstructor alarmMsgConstructor; + + @SpyBean + protected DeviceMsgConstructor deviceMsgConstructor; + + @SpyBean + protected AssetMsgConstructor assetMsgConstructor; + + @MockBean + protected EntityViewMsgConstructor entityViewMsgConstructor; + + @MockBean + protected DashboardMsgConstructor dashboardMsgConstructor; + + @MockBean + protected RelationMsgConstructor relationMsgConstructor; + + @MockBean + protected UserMsgConstructor userMsgConstructor; + + @MockBean + protected CustomerMsgConstructor customerMsgConstructor; + + @SpyBean + protected DeviceProfileMsgConstructor deviceProfileMsgConstructor; + + @SpyBean + protected AssetProfileMsgConstructor assetProfileMsgConstructor; + + @MockBean + protected TenantMsgConstructor tenantMsgConstructor; + + @MockBean + protected TenantProfileMsgConstructor tenantProfileMsgConstructor; + + @MockBean + protected WidgetsBundleMsgConstructor widgetsBundleMsgConstructor; + + @MockBean + protected WidgetTypeMsgConstructor widgetTypeMsgConstructor; + + @MockBean + protected AdminSettingsMsgConstructor adminSettingsMsgConstructor; + + @MockBean + protected OtaPackageMsgConstructor otaPackageMsgConstructor; + + @MockBean + protected QueueMsgConstructor queueMsgConstructor; + + @MockBean + protected EdgeSynchronizationManager edgeSynchronizationManager; + + @MockBean + protected DbCallbackExecutorService dbCallbackExecutorService; + + @MockBean + protected DataDecodingEncodingService dataDecodingEncodingService; + + protected EdgeId edgeId; + protected TenantId tenantId; + protected EdgeEvent edgeEvent; + + protected DashboardId getDashboardId(long expectedDashboardIdMSB, long expectedDashboardIdLSB) { + DashboardId dashboardId; + if (expectedDashboardIdMSB != 0 && expectedDashboardIdLSB != 0) { + dashboardId = new DashboardId(new UUID(expectedDashboardIdMSB, expectedDashboardIdLSB)); + } else { + dashboardId = new DashboardId(UUID.randomUUID()); + } + return dashboardId; + } + + protected RuleChainId getRuleChainId(long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { + RuleChainId ruleChainId; + if (expectedRuleChainIdMSB != 0 && expectedRuleChainIdLSB != 0) { + ruleChainId = new RuleChainId(new UUID(expectedRuleChainIdMSB, expectedRuleChainIdLSB)); + } else { + ruleChainId = new RuleChainId(UUID.randomUUID()); + } + return ruleChainId; + } + + protected static Stream provideParameters() { + UUID dashoboardUUID = UUID.randomUUID(); + UUID ruleChaindUUID = UUID.randomUUID(); + return Stream.of( + Arguments.of(EdgeVersion.V_3_3_0, 0, 0, 0, 0), + Arguments.of(EdgeVersion.V_3_3_3, 0, 0, 0, 0), + Arguments.of(EdgeVersion.V_3_4_0, 0, 0, 0, 0), + Arguments.of(EdgeVersion.V_3_6_0, + dashoboardUUID.getMostSignificantBits(), + dashoboardUUID.getLeastSignificantBits(), + ruleChaindUUID.getMostSignificantBits(), + ruleChaindUUID.getLeastSignificantBits()) + ); + } +} diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AbstractAssetProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AbstractAssetProcessorTest.java new file mode 100644 index 0000000000..6216c71440 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AbstractAssetProcessorTest.java @@ -0,0 +1,92 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.asset; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessorTest; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.BDDMockito.willReturn; + +public abstract class AbstractAssetProcessorTest extends BaseEdgeProcessorTest { + + + protected AssetId assetId; + protected AssetProfileId assetProfileId; + protected AssetProfile assetProfile; + + @BeforeEach + public void setUp() { + edgeId = new EdgeId(UUID.randomUUID()); + tenantId = new TenantId(UUID.randomUUID()); + assetId = new AssetId(UUID.randomUUID()); + assetProfileId = new AssetProfileId(UUID.randomUUID()); + + assetProfile = new AssetProfile(); + assetProfile.setId(assetProfileId); + assetProfile.setName("AssetProfile"); + assetProfile.setDefault(true); + + Asset asset = new Asset(); + asset.setAssetProfileId(assetProfileId); + asset.setId(assetId); + asset.setName("Asset"); + asset.setType(assetProfile.getName()); + + edgeEvent = new EdgeEvent(); + edgeEvent.setTenantId(tenantId); + edgeEvent.setAction(EdgeEventActionType.ADDED); + + + willReturn(asset).given(assetService).findAssetById(tenantId, assetId); + willReturn(assetProfile).given(assetProfileService).findAssetProfileById(tenantId, assetProfileId); + } + + protected void updateAssetProfileDefaultFields(long expectedDashboardIdMSB, long expectedDashboardIdLSB, + long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { + DashboardId dashboardId = getDashboardId(expectedDashboardIdMSB, expectedDashboardIdLSB); + RuleChainId ruleChainId = getRuleChainId(expectedRuleChainIdMSB, expectedRuleChainIdLSB); + + assetProfile.setDefaultDashboardId(dashboardId); + assetProfile.setDefaultEdgeRuleChainId(ruleChainId); + + } + + protected void verify(DownlinkMsg downlinkMsg, long expectedDashboardIdMSB, long expectedDashboardIdLSB, + long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { + AssetProfileUpdateMsg assetProfileUpdateMsg = downlinkMsg.getAssetProfileUpdateMsgList().get(0); + assertNotNull(assetProfileUpdateMsg); + Assertions.assertThat(assetProfileUpdateMsg.getDefaultDashboardIdMSB()).isEqualTo(expectedDashboardIdMSB); + Assertions.assertThat(assetProfileUpdateMsg.getDefaultDashboardIdLSB()).isEqualTo(expectedDashboardIdLSB); + Assertions.assertThat(assetProfileUpdateMsg.getDefaultRuleChainIdMSB()).isEqualTo(expectedRuleChainIdMSB); + Assertions.assertThat(assetProfileUpdateMsg.getDefaultRuleChainIdLSB()).isEqualTo(expectedRuleChainIdLSB); + } +} diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorTest.java new file mode 100644 index 0000000000..d8c8041694 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorTest.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.asset; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; + +@SpringBootTest(classes = {AssetEdgeProcessor.class}) +class AssetEdgeProcessorTest extends AbstractAssetProcessorTest { + + @SpyBean + AssetEdgeProcessor assetEdgeProcessor; + + @ParameterizedTest + @MethodSource("provideParameters") + public void testAssetProfileDefaultFields_notSendToEdgeOlder3_6_0IfNotAssigned(EdgeVersion edgeVersion, long expectedDashboardIdMSB, long expectedDashboardIdLSB, + long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { + updateAssetProfileDefaultFields(expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB); + + edgeEvent.setEntityId(assetId.getId()); + + DownlinkMsg downlinkMsg = assetEdgeProcessor.convertAssetEventToDownlink(edgeEvent, edgeId, edgeVersion); + + verify(downlinkMsg, expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB); + } + +} \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessorTest.java new file mode 100644 index 0000000000..e0a6155664 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessorTest.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.asset; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; + +@SpringBootTest(classes = {AssetProfileEdgeProcessor.class}) +class AssetProfileEdgeProcessorTest extends AbstractAssetProcessorTest{ + @SpyBean + AssetProfileEdgeProcessor assetProfileEdgeProcessor; + + @ParameterizedTest + @MethodSource("provideParameters") + public void testAssetProfileDefaultFields_notSendToEdgeOlder3_6_0IfNotAssigned(EdgeVersion edgeVersion, long expectedDashboardIdMSB, long expectedDashboardIdLSB, + long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { + + updateAssetProfileDefaultFields(expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB); + + edgeEvent.setEntityId(assetProfileId.getId()); + + DownlinkMsg downlinkMsg = assetProfileEdgeProcessor.convertAssetProfileEventToDownlink(edgeEvent, edgeId, edgeVersion); + + verify(downlinkMsg, expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB); + } +} \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java new file mode 100644 index 0000000000..6f2249b173 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java @@ -0,0 +1,100 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.device; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessorTest; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.BDDMockito.willReturn; + +public abstract class AbstractDeviceProcessorTest extends BaseEdgeProcessorTest { + + protected DeviceId deviceId; + protected DeviceProfileId deviceProfileId; + protected DeviceProfile deviceProfile; + + @BeforeEach + public void setUp() { + edgeId = new EdgeId(UUID.randomUUID()); + tenantId = new TenantId(UUID.randomUUID()); + deviceId = new DeviceId(UUID.randomUUID()); + deviceProfileId = new DeviceProfileId(UUID.randomUUID()); + + deviceProfile = new DeviceProfile(); + deviceProfile.setId(deviceProfileId); + deviceProfile.setName("DeviceProfile"); + deviceProfile.setDefault(true); + deviceProfile.setType(DeviceProfileType.DEFAULT); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + deviceProfile.setProfileData(deviceProfileData); + deviceProfile.setTransportType(DeviceTransportType.DEFAULT); + + Device device = new Device(); + device.setDeviceProfileId(deviceProfileId); + device.setId(deviceId); + device.setName("Device"); + device.setType(deviceProfile.getName()); + + edgeEvent = new EdgeEvent(); + edgeEvent.setTenantId(tenantId); + edgeEvent.setAction(EdgeEventActionType.ADDED); + + + willReturn(device).given(deviceService).findDeviceById(tenantId, deviceId); + willReturn(deviceProfile).given(deviceProfileService).findDeviceProfileById(tenantId, deviceProfileId); + willReturn(new byte[]{0x00}).given(dataDecodingEncodingService).encode(deviceProfileData); + + } + + protected void updateDeviceProfileDefaultFields(long expectedDashboardIdMSB, long expectedDashboardIdLSB, + long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { + DashboardId dashboardId = getDashboardId(expectedDashboardIdMSB, expectedDashboardIdLSB); + RuleChainId ruleChainId = getRuleChainId(expectedRuleChainIdMSB, expectedRuleChainIdLSB); + + deviceProfile.setDefaultDashboardId(dashboardId); + deviceProfile.setDefaultEdgeRuleChainId(ruleChainId); + + } + + protected void verify(DownlinkMsg downlinkMsg, long expectedDashboardIdMSB, long expectedDashboardIdLSB, + long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { + DeviceProfileUpdateMsg deviceProfileUpdateMsg = downlinkMsg.getDeviceProfileUpdateMsgList().get(0); + assertNotNull(deviceProfileUpdateMsg); + Assertions.assertThat(deviceProfileUpdateMsg.getDefaultDashboardIdMSB()).isEqualTo(expectedDashboardIdMSB); + Assertions.assertThat(deviceProfileUpdateMsg.getDefaultDashboardIdLSB()).isEqualTo(expectedDashboardIdLSB); + Assertions.assertThat(deviceProfileUpdateMsg.getDefaultRuleChainIdMSB()).isEqualTo(expectedRuleChainIdMSB); + Assertions.assertThat(deviceProfileUpdateMsg.getDefaultRuleChainIdLSB()).isEqualTo(expectedRuleChainIdLSB); + } +} diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessorTest.java new file mode 100644 index 0000000000..f64bd898d1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessorTest.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.device; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; + +@SpringBootTest(classes = {DeviceEdgeProcessor.class}) +class DeviceEdgeProcessorTest extends AbstractDeviceProcessorTest { + + @SpyBean + DeviceEdgeProcessor deviceEdgeProcessor; + + @ParameterizedTest + @MethodSource("provideParameters") + public void testDeviceProfileDefaultFields_notSendToEdgeOlder3_6_0IfNotAssigned(EdgeVersion edgeVersion, long expectedDashboardIdMSB, long expectedDashboardIdLSB, + long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { + updateDeviceProfileDefaultFields(expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB); + edgeEvent.setEntityId(deviceId.getId()); + + DownlinkMsg downlinkMsg = deviceEdgeProcessor.convertDeviceEventToDownlink(edgeEvent, edgeId, edgeVersion); + + verify(downlinkMsg, expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB); + } + +} \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessorTest.java new file mode 100644 index 0000000000..35d44852e9 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessorTest.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.device; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; + + +@SpringBootTest(classes = {DeviceProfileEdgeProcessor.class}) +class DeviceProfileEdgeProcessorTest extends AbstractDeviceProcessorTest { + + @SpyBean + DeviceProfileEdgeProcessor deviceProfileEdgeProcessor; + + @ParameterizedTest + @MethodSource("provideParameters") + public void testDeviceProfileDefaultFields_notSendToEdgeOlder3_6_0IfNotAssigned(EdgeVersion edgeVersion, long expectedDashboardIdMSB, long expectedDashboardIdLSB, + long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) { + updateDeviceProfileDefaultFields(expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB); + + edgeEvent.setEntityId(deviceProfileId.getId()); + + DownlinkMsg downlinkMsg = deviceProfileEdgeProcessor.convertDeviceProfileEventToDownlink(edgeEvent, edgeId, edgeVersion); + + verify(downlinkMsg, expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB); + } + +} \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java b/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java new file mode 100644 index 0000000000..26eead0ec7 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java @@ -0,0 +1,119 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.install; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; +import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.service.validator.RuleChainDataValidator; +import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.dao.widget.WidgetsBundleService; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willReturn; + +@Slf4j +@SpringBootTest(classes = {InstallScripts.class, RuleChainDataValidator.class}) +class InstallScriptsTest { + + @MockBean + RuleChainService ruleChainService; + @MockBean + DashboardService dashboardService; + @MockBean + WidgetTypeService widgetTypeService; + @MockBean + WidgetsBundleService widgetsBundleService; + @MockBean + OAuth2ConfigTemplateService oAuth2TemplateService; + @MockBean + ResourceService resourceService; + @SpyBean + InstallScripts installScripts; + + @MockBean + TenantService tenantService; + @MockBean + ApiLimitService apiLimitService; + @SpyBean + RuleChainDataValidator ruleChainValidator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @BeforeEach + void setUp() { + willReturn(true).given(tenantService).tenantExists(tenantId); + willReturn(true).given(apiLimitService).checkEntitiesLimit(any(), any()); + } + + @Test + void testDefaultRuleChainsTemplates() throws IOException { + Path dir = installScripts.getTenantRuleChainsDir(); + installScripts.findRuleChainsFromPath(dir) + .forEach(this::validateRuleChainTemplate); + } + + @Test + void testDefaultEdgeRuleChainsTemplates() throws IOException { + Path dir = installScripts.getEdgeRuleChainsDir(); + installScripts.findRuleChainsFromPath(dir) + .forEach(this::validateRuleChainTemplate); + } + + @Test + void testDeviceProfileDefaultRuleChainTemplate() { + validateRuleChainTemplate(installScripts.getDeviceProfileDefaultRuleChainTemplateFilePath()); + } + + private void validateRuleChainTemplate(Path templateFilePath) { + log.warn("validateRuleChainTemplate {}", templateFilePath); + JsonNode ruleChainJson = JacksonUtil.toJsonNode(templateFilePath.toFile()); + + RuleChain ruleChain = JacksonUtil.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class); + ruleChain.setTenantId(tenantId); + ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); + ruleChain.setId(new RuleChainId(UUID.randomUUID())); + + RuleChainMetaData ruleChainMetaData = JacksonUtil.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class); + ruleChainMetaData.setRuleChainId(ruleChain.getId()); + List throwables = RuleChainDataValidator.validateMetaData(ruleChainMetaData); + + assertThat(throwables).as("templateFilePath " + templateFilePath) + .containsExactlyInAnyOrderElementsOf(Collections.emptyList()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index 3361d516f0..dc95ab793b 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -257,6 +257,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { Asset asset1 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1"); Asset asset2 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 2"); Dashboard dashboard = createDashboard(tenantId1, null, "Dashboard 1"); + Dashboard otherDashboard = createDashboard(tenantId1, null, "Dashboard 2"); DeviceProfile existingDeviceProfile = createDeviceProfile(tenantId2, null, null, "Existing"); String aliasId = "23c4185d-1497-9457-30b2-6d91e69a5b2c"; @@ -265,20 +266,43 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { "\"" + aliasId + "\": {\n" + "\"alias\": \"assets\",\n" + "\"filter\": {\n" + - "\"entityList\": [\n" + - "\"" + asset1.getId().toString() + "\",\n" + - "\"" + asset2.getId().toString() + "\",\n" + - "\"" + tenantId1.getId().toString() + "\",\n" + - "\"" + existingDeviceProfile.getId().toString() + "\",\n" + - "\"" + unknownUuid + "\"\n" + - "],\n" + - "\"resolveMultiple\": true\n" + + " \"entityList\": [\n" + + " \"" + asset1.getId() + "\",\n" + + " \"" + asset2.getId() + "\",\n" + + " \"" + tenantId1.getId() + "\",\n" + + " \"" + existingDeviceProfile.getId() + "\",\n" + + " \"" + unknownUuid + "\"\n" + + " ],\n" + + " \"id\":\"" + asset1.getId() + "\",\n" + + " \"resolveMultiple\": true\n" + "},\n" + "\"id\": \"" + aliasId + "\"\n" + "}\n" + "}"; + String widgetId = "ea8f34a0-264a-f11f-cde3-05201bb4ff4b"; + String actionId = "4a8e6efa-3e68-fa59-7feb-d83366130cae"; + String widgets = "{\n" + + " \"" + widgetId + "\": {\n" + + " \"config\": {\n" + + " \"actions\": {\n" + + " \"rowClick\": [\n" + + " {\n" + + " \"name\": \"go to dashboard\",\n" + + " \"targetDashboardId\": \"" + otherDashboard.getId() + "\",\n" + + " \"id\": \"" + actionId + "\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"row\": 0,\n" + + " \"col\": 0,\n" + + " \"id\": \"" + widgetId + "\"\n" + + " }\n" + + "}"; + ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode(); dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases)); + dashboardConfiguration.set("widgets", JacksonUtil.toJsonNode(widgets)); dashboardConfiguration.set("description", new TextNode("hallo")); dashboard.setConfiguration(dashboardConfiguration); dashboard = dashboardService.saveDashboard(dashboard); @@ -288,10 +312,12 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { EntityExportData asset1ExportData = exportEntity(tenantAdmin1, asset1.getId()); EntityExportData asset2ExportData = exportEntity(tenantAdmin1, asset2.getId()); EntityExportData dashboardExportData = exportEntity(tenantAdmin1, dashboard.getId()); + EntityExportData otherDashboardExportData = exportEntity(tenantAdmin1, otherDashboard.getId()); AssetProfile importedProfile = importEntity(tenantAdmin2, profileExportData).getSavedEntity(); Asset importedAsset1 = importEntity(tenantAdmin2, asset1ExportData).getSavedEntity(); Asset importedAsset2 = importEntity(tenantAdmin2, asset2ExportData).getSavedEntity(); + Dashboard importedOtherDashboard = importEntity(tenantAdmin2, otherDashboardExportData).getSavedEntity(); Dashboard importedDashboard = importEntity(tenantAdmin2, dashboardExportData).getSavedEntity(); Map.Entry entityAlias = importedDashboard.getConfiguration().get("entityAliases").fields().next(); @@ -311,6 +337,17 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { .isEqualTo(existingDeviceProfile.getId().toString()); assertThat(aliasEntitiesIds).element(4).as("unresolved uuid was replaced with tenant id") .isEqualTo(tenantId2.toString()); + assertThat(entityAlias.getValue().get("filter").get("id").asText()).as("external asset 1 was replaced with imported one") + .isEqualTo(importedAsset1.getId().toString()); + + ObjectNode widgetConfig = importedDashboard.getWidgetsConfig().get(0); + assertThat(widgetConfig.get("id").asText()).as("widget id is not replaced") + .isEqualTo(widgetId); + JsonNode actionConfig = widgetConfig.get("config").get("actions").get("rowClick").get(0); + assertThat(actionConfig.get("id").asText()).as("action id is not replaced") + .isEqualTo(actionId); + assertThat(actionConfig.get("targetDashboardId").asText()).as("dashboard id is replaced with imported one") + .isEqualTo(importedOtherDashboard.getId().toString()); } diff --git a/build_proto.sh b/build_proto.sh new file mode 100755 index 0000000000..55fdea0880 --- /dev/null +++ b/build_proto.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# +# Copyright © 2016-2023 The Thingsboard Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +echo "Building ThingsBoard protobuf-containing packages..." +MAVEN_OPTS="-Xmx1024m" NODE_OPTIONS="--max_old_space_size=3072" \ +mvn clean compile -T4 --also-make --projects=' +common/cluster-api, +common/edge-api, +common/transport/coap, +common/transport/mqtt, +common/transport/transport-api' diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index 78614911d4..60dfcae1de 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -975,6 +975,8 @@ message ToCoreMsg { EdgeNotificationMsgProto edgeNotificationMsg = 5; DeviceActivityProto deviceActivityMsg = 6; NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = 7; + LifecycleEventProto lifecycleEventMsg = 8; + ErrorEventProto errorEventMsg = 9; } /* High priority messages with low latency are handled by ThingsBoard Core Service separately */ @@ -1066,3 +1068,24 @@ message NotificationSchedulerServiceMsg { message NotificationRuleProcessorMsg { bytes trigger = 1; } + +message ErrorEventProto { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + int64 entityIdMSB = 3; + int64 entityIdLSB = 4; + string serviceId = 5; + string method = 6; + string error = 7; +} + +message LifecycleEventProto { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + int64 entityIdMSB = 3; + int64 entityIdLSB = 4; + string serviceId = 5; + string lcEventType = 6; + bool success = 7; + string error = 8; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java index 7360f9b8ab..449f252411 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java @@ -163,6 +163,15 @@ public class StringUtils { return false; } + public static boolean equalsAnyIgnoreCase(String string, String... otherStrings) { + for (String otherString : otherStrings) { + if (equalsIgnoreCase(string, otherString)) { + return true; + } + } + return false; + } + public static String substringAfterLast(String str, String sep) { return org.apache.commons.lang3.StringUtils.substringAfterLast(str, sep); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java index 14b62fe02e..6c89c91006 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java @@ -18,7 +18,6 @@ package org.thingsboard.server.common.data.device.profile; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; -import org.thingsboard.server.common.data.transport.snmp.SnmpMapping; import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig; import java.util.List; @@ -45,8 +44,7 @@ public class SnmpDeviceProfileTransportConfiguration implements DeviceProfileTra private boolean isValid() { return timeoutMs != null && timeoutMs >= 0 && retries != null && retries >= 0 && communicationConfigs != null - && communicationConfigs.stream().allMatch(config -> config != null && config.isValid()) - && communicationConfigs.stream().flatMap(config -> config.getAllMappings().stream()).map(SnmpMapping::getOid) - .distinct().count() == communicationConfigs.stream().mapToInt(config -> config.getAllMappings().size()).sum(); + && communicationConfigs.stream().allMatch(config -> config != null && config.isValid()); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/importing/csv/BulkImportColumnType.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/importing/csv/BulkImportColumnType.java index c004187876..87582b4d6d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/importing/csv/BulkImportColumnType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/importing/csv/BulkImportColumnType.java @@ -43,6 +43,10 @@ public enum BulkImportColumnType { LWM2M_SERVER_SECURITY_MODE("securityMode", LwM2MSecurityMode.NO_SEC.name()), LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID("clientPublicKeyOrId"), LWM2M_SERVER_CLIENT_SECRET_KEY("clientSecretKey"), + SNMP_HOST, + SNMP_PORT, + SNMP_VERSION, + SNMP_COMMUNITY_STRING, IS_GATEWAY, DESCRIPTION, ROUTING_KEY, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpCommunicationSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpCommunicationSpec.java index a779d9a23d..1e0e5a745b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpCommunicationSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpCommunicationSpec.java @@ -15,11 +15,21 @@ */ package org.thingsboard.server.common.data.transport.snmp; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter public enum SnmpCommunicationSpec { - TELEMETRY_QUERYING, - CLIENT_ATTRIBUTES_QUERYING, - SHARED_ATTRIBUTES_SETTING, + TELEMETRY_QUERYING("telemetryQuerying"), + CLIENT_ATTRIBUTES_QUERYING("clientAttributesQuerying"), + + SHARED_ATTRIBUTES_SETTING("sharedAttributesSetting"), + TO_DEVICE_RPC_REQUEST("toDeviceRpcRequest"), + + TO_SERVER_RPC_REQUEST("toServerRpcRequest"); + + private final String label; - TO_DEVICE_RPC_REQUEST, } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpMethod.java b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpMethod.java index 257dd28217..e366cdb8b4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpMethod.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpMethod.java @@ -17,7 +17,8 @@ package org.thingsboard.server.common.data.transport.snmp; public enum SnmpMethod { GET(-96), - SET(-93); + SET(-93), + TRAP(-89); // codes taken from org.snmp4j.PDU class private final int code; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java index 36a7f64d3a..d09c9c147c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java @@ -37,7 +37,8 @@ import java.util.List; @Type(value = TelemetryQueryingSnmpCommunicationConfig.class, name = "TELEMETRY_QUERYING"), @Type(value = ClientAttributesQueryingSnmpCommunicationConfig.class, name = "CLIENT_ATTRIBUTES_QUERYING"), @Type(value = SharedAttributesSettingSnmpCommunicationConfig.class, name = "SHARED_ATTRIBUTES_SETTING"), - @Type(value = ToDeviceRpcRequestSnmpCommunicationConfig.class, name = "TO_DEVICE_RPC_REQUEST") + @Type(value = ToDeviceRpcRequestSnmpCommunicationConfig.class, name = "TO_DEVICE_RPC_REQUEST"), + @Type(value = ToServerRpcRequestSnmpCommunicationConfig.class, name = "TO_SERVER_RPC_REQUEST") }) public interface SnmpCommunicationConfig extends Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/ToServerRpcRequestSnmpCommunicationConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/ToServerRpcRequestSnmpCommunicationConfig.java new file mode 100644 index 0000000000..f8a7968b82 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/ToServerRpcRequestSnmpCommunicationConfig.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.transport.snmp.config; + +import org.thingsboard.server.common.data.transport.snmp.SnmpCommunicationSpec; + +public class ToServerRpcRequestSnmpCommunicationConfig extends MultipleMappingsSnmpCommunicationConfig { + + private static final long serialVersionUID = 4851028734093214L; + + @Override + public SnmpCommunicationSpec getSpec() { + return SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/utils/TypeCastUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java similarity index 58% rename from application/src/main/java/org/thingsboard/server/utils/TypeCastUtil.java rename to common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java index 3bf1efc8f4..cc40c0d13f 100644 --- a/application/src/main/java/org/thingsboard/server/utils/TypeCastUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java @@ -13,35 +13,53 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.utils; +package org.thingsboard.server.common.data.util; import org.apache.commons.lang3.math.NumberUtils; +import org.apache.commons.lang3.tuple.Pair; import org.thingsboard.server.common.data.kv.DataType; import java.math.BigDecimal; -import java.util.Map; public class TypeCastUtil { private TypeCastUtil() {} - public static Map.Entry castValue(String value) { + public static Pair castValue(String value) { if (isNumber(value)) { String formattedValue = value.replace(',', '.'); try { BigDecimal bd = new BigDecimal(formattedValue); if (bd.stripTrailingZeros().scale() > 0 || isSimpleDouble(formattedValue)) { if (bd.scale() <= 16) { - return Map.entry(DataType.DOUBLE, bd.doubleValue()); + return Pair.of(DataType.DOUBLE, bd.doubleValue()); } } else { - return Map.entry(DataType.LONG, bd.longValueExact()); + return Pair.of(DataType.LONG, bd.longValueExact()); } } catch (RuntimeException ignored) {} } else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { - return Map.entry(DataType.BOOLEAN, Boolean.parseBoolean(value)); + return Pair.of(DataType.BOOLEAN, Boolean.parseBoolean(value)); + } + return Pair.of(DataType.STRING, value); + } + + public static Pair castToNumber(String value) { + if (isNumber(value)) { + String formattedValue = value.replace(',', '.'); + BigDecimal bd = new BigDecimal(formattedValue); + if (bd.stripTrailingZeros().scale() > 0 || isSimpleDouble(formattedValue)) { + if (bd.scale() <= 16) { + return Pair.of(DataType.DOUBLE, bd.doubleValue()); + } else { + return Pair.of(DataType.DOUBLE, bd); + } + } else { + return Pair.of(DataType.LONG, bd.longValueExact()); + } + } else { + throw new IllegalArgumentException("'" + value + "' can't be parsed as number"); } - return Map.entry(DataType.STRING, value); } private static boolean isNumber(String value) { diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index dcd543a9a6..35a4ab9cd5 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -111,7 +111,7 @@ public class EdgeGrpcClient implements EdgeRpcClient { .setConnectRequestMsg(ConnectRequestMsg.newBuilder() .setEdgeRoutingKey(edgeKey) .setEdgeSecret(edgeSecret) - .setEdgeVersion(EdgeVersion.V_3_4_0) + .setEdgeVersion(EdgeVersion.V_3_6_0) .setMaxInboundMessageSize(maxInboundMessageSize) .build()) .build()); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 3e8310222b..50d5ad3c7f 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -34,6 +34,7 @@ enum EdgeVersion { V_3_3_0 = 0; V_3_3_3 = 1; V_3_4_0 = 2; + V_3_6_0 = 3; } /** @@ -85,7 +86,7 @@ message ConnectResponseMsg { } message SyncRequestMsg { - bool syncRequired = 1; // deprecated + bool syncRequired = 1 [deprecated = true]; optional bool fullSync = 2; } @@ -113,7 +114,7 @@ enum UpdateMsgType { ENTITY_DELETED_RPC_MESSAGE = 2; ALARM_ACK_RPC_MESSAGE = 3; ALARM_CLEAR_RPC_MESSAGE = 4; - ENTITY_MERGE_RPC_MESSAGE = 5; // deprecated + ENTITY_MERGE_RPC_MESSAGE = 5 [deprecated = true]; } message EntityDataProto { @@ -204,7 +205,7 @@ message DeviceUpdateMsg { string type = 9; optional string label = 10; optional string additionalInfo = 11; - optional string conflictName = 12; // deprecated + optional string conflictName = 12 [deprecated = true]; optional int64 firmwareIdMSB = 13; optional int64 firmwareIdLSB = 14; optional bytes deviceDataBytes = 15; @@ -365,8 +366,8 @@ message WidgetTypeUpdateMsg { UpdateMsgType msgType = 1; int64 idMSB = 2; int64 idLSB = 3; - optional string bundleAlias = 4; // deprecated - optional string alias = 5; // deprecated + optional string bundleAlias = 4 [deprecated = true]; + optional string alias = 5 [deprecated = true]; optional string name = 6; optional string descriptorJson = 7; bool isSystem = 8; @@ -447,8 +448,8 @@ message DeviceCredentialsRequestMsg { int64 deviceIdLSB = 2; } -// deprecated message DeviceProfileDevicesRequestMsg { + option deprecated = true; int64 deviceProfileIdMSB = 1; int64 deviceProfileIdLSB = 2; } @@ -562,7 +563,7 @@ message UplinkMsg { repeated UserCredentialsRequestMsg userCredentialsRequestMsg = 10; repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 11; repeated DeviceRpcCallMsg deviceRpcCallMsg = 12; - repeated DeviceProfileDevicesRequestMsg deviceProfileDevicesRequestMsg = 13; // deprecated + repeated DeviceProfileDevicesRequestMsg deviceProfileDevicesRequestMsg = 13 [deprecated = true]; repeated WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg = 14; repeated EntityViewsRequestMsg entityViewsRequestMsg = 15; repeated AssetUpdateMsg assetUpdateMsg = 16; diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java index ac196a8edd..23274e863d 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java @@ -150,7 +150,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService } apiUsageReportClient.ifPresent(client -> client.report(tenantId, customerId, ApiUsageRecordKey.JS_EXEC_COUNT, 1)); pushedMsgs.incrementAndGet(); - log.trace("InvokeScript uuid {} with timeout {}ms", scriptId, getMaxInvokeRequestsTimeout()); + log.trace("[{}] InvokeScript uuid {} with timeout {}ms", tenantId, scriptId, getMaxInvokeRequestsTimeout()); var task = doInvokeFunction(scriptId, args); var resultFuture = Futures.transformAsync(task.getResultFuture(), output -> { @@ -167,7 +167,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService } else { String message = "Script invocation is blocked due to maximum error count " + getMaxErrors() + ", scriptId " + scriptId + "!"; - log.warn(message); + log.warn("[{}] " + message, tenantId); return error(message); } } else { diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DateTimeFormatOptions.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DateTimeFormatOptions.java new file mode 100644 index 0000000000..93d35c55b4 --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DateTimeFormatOptions.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.script.api.tbel; + +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.StringUtils; + +import java.time.format.FormatStyle; +import java.util.TimeZone; + +@NoArgsConstructor +@Data +class DateTimeFormatOptions { + private static final TimeZone DEFAULT_TZ = TimeZone.getDefault(); + + private String timeZone; + private String dateStyle; + private String timeStyle; + @Getter + private String pattern; + + public DateTimeFormatOptions(String timeZone) { + this.timeZone = timeZone; + } + + TimeZone getTimeZone() { + return StringUtils.isNotEmpty(timeZone) ? TimeZone.getTimeZone(timeZone) : TimeZone.getDefault(); + } + + FormatStyle getDateStyle() { + return getFormatStyle(dateStyle, FormatStyle.SHORT); + } + + FormatStyle getTimeStyle() { + return getFormatStyle(timeStyle, FormatStyle.MEDIUM); + } + + private static FormatStyle getFormatStyle(String style, FormatStyle defaultStyle) { + if (StringUtils.isNotEmpty(style)) { + try { + return FormatStyle.valueOf(style.toUpperCase()); + } catch (IllegalArgumentException e) { + return defaultStyle; + } + } else { + return defaultStyle; + } + } + +} diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java index c22e2c329d..3f79dc2cf7 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java @@ -15,6 +15,9 @@ */ package org.thingsboard.script.api.tbel; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; + import java.text.DateFormat; import java.text.SimpleDateFormat; import java.time.Instant; @@ -22,13 +25,15 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.chrono.IsoChronology; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAccessor; import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale; -import java.util.TimeZone; +import java.util.function.BiFunction; public class TbDate extends Date { @@ -77,15 +82,77 @@ public class TbDate extends Date { return isoDateFormat.get().format(this); } + public String toLocaleDateString() { + return toLocaleDateString(null, null); + } + + public String toLocaleDateString(String locale) { + return toLocaleDateString(locale, null); + } + + public String toLocaleDateString(String localeStr, String optionsStr) { + return toLocaleString(localeStr, optionsStr, (locale, options) -> DateTimeFormatter.ofLocalizedDate(options.getDateStyle()).withLocale(locale)); + } + + public String toLocaleTimeString() { + return toLocaleTimeString(null, null); + } + + public String toLocaleTimeString(String locale) { + return toLocaleTimeString(locale, null); + } + + public String toLocaleTimeString(String localeStr, String optionsStr) { + return toLocaleString(localeStr, optionsStr, (locale, options) -> DateTimeFormatter.ofLocalizedTime(options.getTimeStyle()).withLocale(locale)); + } + + @Override + public String toLocaleString() { + return toLocaleString(null, null); + } + public String toLocaleString(String locale) { - DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.forLanguageTag(locale)); - return formatter.format(this); + return toLocaleString(locale, null); } - public String toLocaleString(String locale, String tz) { - DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.forLanguageTag(locale)); - formatter.setTimeZone(TimeZone.getTimeZone(tz)); - return formatter.format(this); + public String toLocaleString(String localeStr, String optionsStr) { + return toLocaleString(localeStr, optionsStr, (locale, options) -> { + String formatPattern = + DateTimeFormatterBuilder.getLocalizedDateTimePattern( + options.getDateStyle(), + options.getTimeStyle(), + IsoChronology.INSTANCE, + locale); + return DateTimeFormatter.ofPattern(formatPattern, locale); + }); + } + + public String toLocaleString(String localeStr, String optionsStr, BiFunction formatterBuilder) { + Locale locale = StringUtils.isNotEmpty(localeStr) ? Locale.forLanguageTag(localeStr) : Locale.getDefault(); + DateTimeFormatOptions options = getDateFormattingOptions(optionsStr); + ZonedDateTime zdt = this.toInstant().atZone(options.getTimeZone().toZoneId()); + DateTimeFormatter formatter; + if (StringUtils.isNotEmpty(options.getPattern())) { + formatter = new DateTimeFormatterBuilder().appendPattern(options.getPattern()).toFormatter(locale); + } else { + formatter = formatterBuilder.apply(locale, options); + } + return formatter.format(zdt); + } + + private static DateTimeFormatOptions getDateFormattingOptions(String options) { + DateTimeFormatOptions opt = null; + if (StringUtils.isNotEmpty(options)) { + try { + opt = JacksonUtil.fromString(options, DateTimeFormatOptions.class); + } catch (IllegalArgumentException iae) { + opt = new DateTimeFormatOptions(options); + } + } + if (opt == null) { + opt = new DateTimeFormatOptions(); + } + return opt; } public static long now() { diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index bffd60f029..bd14871fe2 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -36,6 +36,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -63,6 +64,7 @@ public class TbUtils { ExecutionContext.class, String.class))); parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes", ExecutionContext.class, String.class, String.class))); + parserConfig.registerNonConvertableMethods(TbUtils.class, Collections.singleton("stringToBytes")); parserConfig.addImport("parseInt", new MethodStub(TbUtils.class.getMethod("parseInt", String.class))); parserConfig.addImport("parseInt", new MethodStub(TbUtils.class.getMethod("parseInt", diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java index 88f1f825c1..87dd4f0ebf 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java @@ -21,12 +21,24 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.JacksonUtil; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.chrono.IsoChronology; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.FormatStyle; import java.util.ArrayList; import java.util.Calendar; import java.util.List; +import java.util.Locale; +import java.util.TimeZone; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; @@ -124,4 +136,174 @@ class TbDateTest { .startsWith(datePrefix); } + @Test + void testToLocaleDateString() { + TbDate d = new TbDate(1693962245000L); + + // Depends on time zone, so we just check it works; + Assert.assertNotNull(d.toLocaleDateString()); + Assert.assertNotNull(d.toLocaleDateString("en-US")); + + Assert.assertEquals("9/5/23", d.toLocaleDateString("en-US", "America/New_York")); + Assert.assertEquals("23. 9. 5.", d.toLocaleDateString("ko-KR", "America/New_York")); + Assert.assertEquals("06.09.23", d.toLocaleDateString( "uk-UA", "Europe/Kiev")); + Assert.assertEquals("5\u200F/9\u200F/2023", d.toLocaleDateString( "ar-EG", "America/New_York")); + + Assert.assertEquals("Tuesday, September 5, 2023", d.toLocaleDateString("en-US", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("dateStyle", "full") + .toString())); + Assert.assertEquals("2023년 9월 5일 화요일", d.toLocaleDateString("ko-KR", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("dateStyle", "full") + .toString())); + Assert.assertEquals("середа, 6 вересня 2023 р.", d.toLocaleDateString("uk-UA", JacksonUtil.newObjectNode() + .put("timeZone", "Europe/Kiev") + .put("dateStyle", "full") + .toString())); + Assert.assertEquals("الثلاثاء، 5 سبتمبر 2023", d.toLocaleDateString("ar-EG", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("dateStyle", "full") + .toString())); + + Assert.assertEquals("Tuesday 9/5/2023", d.toLocaleDateString("en-US", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("pattern", "EEEE M/d/yyyy") + .toString())); + Assert.assertEquals("화요일 9/5/2023", d.toLocaleDateString("ko-KR", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("pattern", "EEEE M/d/yyyy") + .toString())); + Assert.assertEquals("середа 9/6/2023", d.toLocaleDateString("uk-UA", JacksonUtil.newObjectNode() + .put("timeZone", "Europe/Kiev") + .put("pattern", "EEEE M/d/yyyy") + .toString())); + Assert.assertEquals("الثلاثاء 9/5/2023", d.toLocaleDateString("ar-EG", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("pattern", "EEEE M/d/yyyy") + .toString())); + } + + @Test + void testToLocaleTimeString() { + TbDate d = new TbDate(1693962245000L); + + // Depends on time zone, so we just check it works; + Assert.assertNotNull(d.toLocaleTimeString()); + Assert.assertNotNull(d.toLocaleTimeString("en-US")); + + + Assert.assertEquals("9:04:05 PM", d.toLocaleTimeString("en-US", "America/New_York")); + Assert.assertEquals("오후 9:04:05", d.toLocaleTimeString("ko-KR", "America/New_York")); + Assert.assertEquals("04:04:05", d.toLocaleTimeString( "uk-UA", "Europe/Kiev")); + Assert.assertEquals("9:04:05 م", d.toLocaleTimeString( "ar-EG", "America/New_York")); + + Assert.assertEquals("9:04:05 PM Eastern Daylight Time", d.toLocaleTimeString("en-US", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("timeStyle", "full") + .toString())); + Assert.assertEquals("오후 9시 4분 5초 미 동부 하계 표준시", d.toLocaleTimeString("ko-KR", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("timeStyle", "full") + .toString())); + Assert.assertEquals("04:04:05 за східноєвропейським літнім часом", d.toLocaleTimeString("uk-UA", JacksonUtil.newObjectNode() + .put("timeZone", "Europe/Kiev") + .put("timeStyle", "full") + .toString())); + Assert.assertEquals("9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية", d.toLocaleTimeString("ar-EG", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("timeStyle", "full") + .toString())); + + Assert.assertEquals("9:04:05 PM", d.toLocaleTimeString("en-US", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("pattern", "h:mm:ss a") + .toString())); + Assert.assertEquals("9:04:05 오후", d.toLocaleTimeString("ko-KR", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("pattern", "h:mm:ss a") + .toString())); + Assert.assertEquals("4:04:05 дп", d.toLocaleTimeString("uk-UA", JacksonUtil.newObjectNode() + .put("timeZone", "Europe/Kiev") + .put("pattern", "h:mm:ss a") + .toString())); + Assert.assertEquals("9:04:05 م", d.toLocaleTimeString("ar-EG", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("pattern", "h:mm:ss a") + .toString())); + } + + @Test + void testToLocaleString() { + TbDate d = new TbDate(1693962245000L); + + // Depends on time zone, so we just check it works; + Assert.assertNotNull(d.toLocaleString()); + Assert.assertNotNull(d.toLocaleString("en-US")); + + Assert.assertEquals("9/5/23, 9:04:05 PM", d.toLocaleString("en-US", "America/New_York")); + Assert.assertEquals("23. 9. 5. 오후 9:04:05", d.toLocaleString("ko-KR", "America/New_York")); + Assert.assertEquals("06.09.23, 04:04:05", d.toLocaleString( "uk-UA", "Europe/Kiev")); + Assert.assertEquals("5\u200F/9\u200F/2023 9:04:05 م", d.toLocaleString( "ar-EG", "America/New_York")); + + Assert.assertEquals("Tuesday, September 5, 2023 at 9:04:05 PM Eastern Daylight Time", d.toLocaleString("en-US", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("dateStyle", "full") + .put("timeStyle", "full") + .toString())); + Assert.assertEquals("2023년 9월 5일 화요일 오후 9시 4분 5초 미 동부 하계 표준시", d.toLocaleString("ko-KR", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("dateStyle", "full") + .put("timeStyle", "full") + .toString())); + Assert.assertEquals("середа, 6 вересня 2023 р. о 04:04:05 за східноєвропейським літнім часом", d.toLocaleString("uk-UA", JacksonUtil.newObjectNode() + .put("timeZone", "Europe/Kiev") + .put("dateStyle", "full") + .put("timeStyle", "full") + .toString())); + Assert.assertEquals("الثلاثاء، 5 سبتمبر 2023 9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية", d.toLocaleString("ar-EG", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("dateStyle", "full") + .put("timeStyle", "full") + .toString())); + + Assert.assertEquals("9/5/2023, 9:04:05 PM", d.toLocaleString("en-US", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("pattern", "M/d/yyyy, h:mm:ss a") + .toString())); + Assert.assertEquals("9/5/2023, 9:04:05 오후", d.toLocaleString("ko-KR", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("pattern", "M/d/yyyy, h:mm:ss a") + .toString())); + Assert.assertEquals("9/6/2023, 4:04:05 дп", d.toLocaleString("uk-UA", JacksonUtil.newObjectNode() + .put("timeZone", "Europe/Kiev") + .put("pattern", "M/d/yyyy, h:mm:ss a") + .toString())); + Assert.assertEquals("9/5/2023, 9:04:05 م", d.toLocaleString("ar-EG", JacksonUtil.newObjectNode() + .put("timeZone", "America/New_York") + .put("pattern", "M/d/yyyy, h:mm:ss a") + .toString())); + } + + private static String toLocalString(TbDate d, Locale locale, ZoneId tz) { + LocalDateTime ldt = d.toInstant().atZone(tz).toLocalDateTime(); + +// new DateTimeFormatterBuilder().appendPattern(pattern).toFormatter(locale) + + String formatPattern = + DateTimeFormatterBuilder.getLocalizedDateTimePattern( + FormatStyle.SHORT, + FormatStyle.MEDIUM, + IsoChronology.INSTANCE, + locale); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatPattern, locale); + return formatter.format(ldt); + } + + private static String toLocalString2(TbDate d, Locale locale, ZoneId tz) { + DateFormat dateFormat = SimpleDateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, locale); + dateFormat.setTimeZone(TimeZone.getTimeZone(tz)); + return dateFormat.format(d); + } + } diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java index 5dcffbf62f..f3c6d8bc83 100644 --- a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java +++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfigu import org.thingsboard.server.common.data.device.profile.SnmpDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.transport.DeviceUpdatedEvent; @@ -113,19 +114,25 @@ public class SnmpTransportContext extends TransportContext { SnmpDeviceProfileTransportConfiguration profileTransportConfiguration = (SnmpDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration(); SnmpDeviceTransportConfiguration deviceTransportConfiguration = (SnmpDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration(); - DeviceSessionContext deviceSessionContext; + DeviceSessionContext sessionContext; try { - deviceSessionContext = new DeviceSessionContext( - device, deviceProfile, credentials.getCredentialsId(), - profileTransportConfiguration, deviceTransportConfiguration, this - ); - registerSessionMsgListener(deviceSessionContext); + sessionContext = DeviceSessionContext.builder() + .tenantId(deviceProfile.getTenantId()) + .device(device) + .deviceProfile(deviceProfile) + .token(credentials.getCredentialsId()) + .profileTransportConfiguration(profileTransportConfiguration) + .deviceTransportConfiguration(deviceTransportConfiguration) + .snmpTransportContext(this) + .build(); + registerSessionMsgListener(sessionContext); } catch (Exception e) { log.error("Failed to establish session for SNMP device {}: {}", device.getId(), e.toString()); + transportService.errorEvent(device.getTenantId(), device.getId(), "sessionEstablishing", e); return; } - sessions.put(device.getId(), deviceSessionContext); - snmpTransportService.createQueryingTasks(deviceSessionContext); + sessions.put(device.getId(), sessionContext); + snmpTransportService.createQueryingTasks(sessionContext); log.info("Established SNMP device session for device {}", device.getId()); } @@ -148,14 +155,17 @@ public class SnmpTransportContext extends TransportContext { sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration); snmpTransportService.cancelQueryingTasks(sessionContext); snmpTransportService.createQueryingTasks(sessionContext); + transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null); } else if (!newDeviceTransportConfiguration.equals(sessionContext.getDeviceTransportConfiguration())) { sessionContext.setDeviceTransportConfiguration(newDeviceTransportConfiguration); sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration); + transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null); } else { log.trace("Configuration of the device {} was not updated", device); } } catch (Exception e) { log.error("Failed to update session for SNMP device {}: {}", sessionContext.getDeviceId(), e.getMessage()); + transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, false, e); destroyDeviceSession(sessionContext); } } @@ -168,43 +178,58 @@ public class SnmpTransportContext extends TransportContext { transportService.deregisterSession(sessionContext.getSessionInfo()); snmpTransportService.cancelQueryingTasks(sessionContext); sessions.remove(sessionContext.getDeviceId()); + transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.STOPPED, true, null); log.trace("Unregistered and removed session"); } - private void registerSessionMsgListener(DeviceSessionContext deviceSessionContext) { + private void registerSessionMsgListener(DeviceSessionContext sessionContext) { transportService.process(DeviceTransportType.SNMP, - TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceSessionContext.getToken()).build(), + TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(sessionContext.getToken()).build(), new TransportServiceCallback<>() { @Override public void onSuccess(ValidateDeviceCredentialsResponse msg) { if (msg.hasDeviceInfo()) { - SessionInfoProto sessionInfo = SessionInfoCreator.create( - msg, SnmpTransportContext.this, UUID.randomUUID() - ); - - transportService.registerAsyncSession(sessionInfo, deviceSessionContext); - transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build(), TransportServiceCallback.EMPTY); - transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.newBuilder().build(), TransportServiceCallback.EMPTY); - - deviceSessionContext.setSessionInfo(sessionInfo); - deviceSessionContext.setDeviceInfo(msg.getDeviceInfo()); - deviceSessionContext.setConnected(true); + registerTransportSession(sessionContext, msg); + sessionContext.setSessionTimeoutHandler(() -> { + registerTransportSession(sessionContext, msg); + }); + transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.STARTED, true, null); } else { - log.warn("[{}] Failed to process device auth", deviceSessionContext.getDeviceId()); + log.warn("[{}] Failed to process device auth", sessionContext.getDeviceId()); } } @Override public void onError(Throwable e) { - log.warn("[{}] Failed to process device auth: {}", deviceSessionContext.getDeviceId(), e); + log.warn("[{}] Failed to process device auth: {}", sessionContext.getDeviceId(), e); + transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.STARTED, false, e); } }); } + private void registerTransportSession(DeviceSessionContext deviceSessionContext, ValidateDeviceCredentialsResponse msg) { + SessionInfoProto sessionInfo = SessionInfoCreator.create( + msg, SnmpTransportContext.this, UUID.randomUUID() + ); + log.debug("Registering transport session: {}", sessionInfo); + + transportService.registerAsyncSession(sessionInfo, deviceSessionContext); + transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder() + .setSessionType(TransportProtos.SessionType.ASYNC) + .build(), TransportServiceCallback.EMPTY); + transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.newBuilder() + .setSessionType(TransportProtos.SessionType.ASYNC) + .build(), TransportServiceCallback.EMPTY); + + deviceSessionContext.setSessionInfo(sessionInfo); + deviceSessionContext.setDeviceInfo(msg.getDeviceInfo()); + deviceSessionContext.setConnected(true); + } + @EventListener(DeviceUpdatedEvent.class) public void onDeviceUpdatedOrCreated(DeviceUpdatedEvent deviceUpdatedEvent) { Device device = deviceUpdatedEvent.getDevice(); - log.trace("Got creating or updating device event for device {}", device); + log.debug("Got creating or updating device event for device {}", device); DeviceTransportType transportType = Optional.ofNullable(device.getDeviceData().getTransportConfiguration()) .map(DeviceTransportConfiguration::getType) .orElse(null); @@ -238,6 +263,7 @@ public class SnmpTransportContext extends TransportContext { } public void onDeviceProfileUpdated(DeviceProfile deviceProfile, DeviceSessionContext sessionContext) { + log.debug("Handling device profile {} update event for device {}", deviceProfile.getId(), sessionContext.getDeviceId()); updateDeviceSession(sessionContext, sessionContext.getDevice(), deviceProfile); } diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/PduService.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/PduService.java index b395672c00..1cfcfd2c85 100644 --- a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/PduService.java +++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/PduService.java @@ -15,7 +15,9 @@ */ package org.thingsboard.server.transport.snmp.service; +import com.google.common.collect.Lists; import com.google.gson.JsonObject; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.snmp4j.PDU; import org.snmp4j.ScopedPDU; @@ -25,43 +27,61 @@ import org.snmp4j.smi.OID; import org.snmp4j.smi.OctetString; import org.snmp4j.smi.Variable; import org.snmp4j.smi.VariableBinding; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration; import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.transport.snmp.SnmpMapping; import org.thingsboard.server.common.data.transport.snmp.SnmpMethod; import org.thingsboard.server.common.data.transport.snmp.SnmpProtocolVersion; import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig; +import org.thingsboard.server.common.data.util.TypeCastUtil; import org.thingsboard.server.queue.util.TbSnmpTransportComponent; import org.thingsboard.server.transport.snmp.session.DeviceSessionContext; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; -import java.util.stream.IntStream; @TbSnmpTransportComponent @Service @Slf4j +@RequiredArgsConstructor public class PduService { - public PDU createPdu(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig, Map values) { - PDU pdu = setUpPdu(sessionContext); - pdu.setType(communicationConfig.getMethod().getCode()); - pdu.addAll(communicationConfig.getAllMappings().stream() - .filter(mapping -> values.isEmpty() || values.containsKey(mapping.getKey())) - .map(mapping -> Optional.ofNullable(values.get(mapping.getKey())) - .map(value -> { - Variable variable = toSnmpVariable(value, mapping.getDataType()); - return new VariableBinding(new OID(mapping.getOid()), variable); - }) - .orElseGet(() -> new VariableBinding(new OID(mapping.getOid())))) - .collect(Collectors.toList())); + @Value("${transport.snmp.max_request_oids:100}") + private int maxRequestOids; + + @Value("${transport.snmp.response.ignore_type_cast_errors:false}") + private boolean ignoreTypeCastErrors; + + public List createPdus(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig, Map values) { + List pdus = new ArrayList<>(); + List allMappings = communicationConfig.getAllMappings(); + + for (List mappings : Lists.partition(allMappings, maxRequestOids)) { + PDU pdu = setUpPdu(sessionContext); + pdu.setType(communicationConfig.getMethod().getCode()); + pdu.addAll(mappings.stream() + .filter(mapping -> values.isEmpty() || values.containsKey(mapping.getKey())) + .map(mapping -> Optional.ofNullable(values.get(mapping.getKey())) + .map(value -> { + Variable variable = toSnmpVariable(value, mapping.getDataType()); + return new VariableBinding(new OID(mapping.getOid()), variable); + }) + .orElseGet(() -> new VariableBinding(new OID(mapping.getOid())))) + .collect(Collectors.toList())); + if (pdu.size() > 0) { + pdus.add(pdu); + } + } - return pdu; + return pdus; } public PDU createSingleVariablePdu(DeviceSessionContext sessionContext, SnmpMethod snmpMethod, String oid, String value, DataType dataType) { @@ -116,8 +136,8 @@ public class PduService { } - public JsonObject processPdu(PDU pdu, List responseMappings) { - Map values = processPdu(pdu); + public JsonObject processPdus(List pdus, List responseMappings) { + Map values = processPdus(pdus); Map mappings = new HashMap<>(); if (responseMappings != null) { @@ -143,29 +163,40 @@ public class PduService { return data; } - public Map processPdu(PDU pdu) { - return IntStream.range(0, pdu.size()) - .mapToObj(pdu::get) + public Map processPdus(List pdus) { + return pdus.stream() + .flatMap(pdu -> pdu.getVariableBindings().stream()) .filter(Objects::nonNull) .filter(variableBinding -> !(variableBinding.getVariable() instanceof Null)) .collect(Collectors.toMap(VariableBinding::getOid, VariableBinding::toValueString)); } public void processValue(String key, DataType dataType, String value, JsonObject result) { - switch (dataType) { - case LONG: - result.addProperty(key, Long.parseLong(value)); - break; - case BOOLEAN: - result.addProperty(key, Boolean.parseBoolean(value)); - break; - case DOUBLE: - result.addProperty(key, Double.parseDouble(value)); - break; - case STRING: - case JSON: - default: - result.addProperty(key, value); + try { + switch (dataType) { + case STRING: + case JSON: + result.addProperty(key, value); + break; + case LONG: + case DOUBLE: + result.addProperty(key, TypeCastUtil.castToNumber(value).getValue()); + break; + case BOOLEAN: + if (StringUtils.equalsAnyIgnoreCase(value, "true", "false")) { + result.addProperty(key, Boolean.parseBoolean(value)); + } else { + throw new IllegalArgumentException("Can't parse '" + value + "' as boolean"); + } + break; + } + } catch (IllegalArgumentException e) { + if (ignoreTypeCastErrors) { + log.debug("Ignoring value '{}' for key '{}' because of data type mismatch ({} required)", value, key, dataType); + } else { + throw e; + } } } + } diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java index 6e68a0bd44..0b60db68b1 100644 --- a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java +++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java @@ -17,10 +17,13 @@ package org.thingsboard.server.transport.snmp.service; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import lombok.Builder; import lombok.Data; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.snmp4j.CommandResponder; +import org.snmp4j.CommandResponderEvent; import org.snmp4j.PDU; import org.snmp4j.Snmp; import org.snmp4j.TransportMapping; @@ -29,10 +32,15 @@ import org.snmp4j.mp.MPv3; import org.snmp4j.security.SecurityModels; import org.snmp4j.security.SecurityProtocols; import org.snmp4j.security.USM; +import org.snmp4j.smi.Address; import org.snmp4j.smi.OctetString; +import org.snmp4j.smi.TcpAddress; +import org.snmp4j.smi.UdpAddress; import org.snmp4j.transport.DefaultTcpTransportMapping; import org.snmp4j.transport.DefaultUdpTransportMapping; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; @@ -45,15 +53,16 @@ import org.thingsboard.server.common.data.transport.snmp.SnmpMethod; import org.thingsboard.server.common.data.transport.snmp.config.RepeatingQueryingSnmpCommunicationConfig; import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig; import org.thingsboard.server.common.transport.TransportService; -import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbSnmpTransportComponent; +import org.thingsboard.server.transport.snmp.SnmpTransportContext; import org.thingsboard.server.transport.snmp.session.DeviceSessionContext; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumMap; @@ -71,9 +80,11 @@ import java.util.stream.Collectors; @Service @Slf4j @RequiredArgsConstructor -public class SnmpTransportService implements TbTransportService { +public class SnmpTransportService implements TbTransportService, CommandResponder { private final TransportService transportService; private final PduService pduService; + @Autowired @Lazy + private SnmpTransportContext transportContext; @Getter private Snmp snmp; @@ -83,6 +94,8 @@ public class SnmpTransportService implements TbTransportService { private final Map responseDataMappers = new EnumMap<>(SnmpCommunicationSpec.class); private final Map responseProcessors = new EnumMap<>(SnmpCommunicationSpec.class); + @Value("${transport.snmp.bind_port:1620}") + private Integer snmpBindPort; @Value("${transport.snmp.response_processing.parallelism_level}") private Integer responseProcessingParallelismLevel; @Value("${transport.snmp.underlying_protocol}") @@ -114,15 +127,16 @@ public class SnmpTransportService implements TbTransportService { TransportMapping transportMapping; switch (snmpUnderlyingProtocol) { case "udp": - transportMapping = new DefaultUdpTransportMapping(); + transportMapping = new DefaultUdpTransportMapping(new UdpAddress(snmpBindPort)); break; case "tcp": - transportMapping = new DefaultTcpTransportMapping(); + transportMapping = new DefaultTcpTransportMapping(new TcpAddress(snmpBindPort)); break; default: throw new IllegalArgumentException("Underlying protocol " + snmpUnderlyingProtocol + " for SNMP is not supported"); } snmp = new Snmp(transportMapping); + snmp.addNotificationListener(transportMapping, transportMapping.getListenAddress(), this); snmp.listen(); USM usm = new USM(SecurityProtocols.getInstance(), new OctetString(MPv3.createLocalEngineID()), 0); @@ -143,6 +157,7 @@ public class SnmpTransportService implements TbTransportService { } } catch (Exception e) { log.error("Failed to send SNMP request for device {}: {}", sessionContext.getDeviceId(), e.toString()); + transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), config.getSpec().getLabel(), e); } }, queryingFrequency, queryingFrequency, TimeUnit.MILLISECONDS); }) @@ -161,18 +176,24 @@ public class SnmpTransportService implements TbTransportService { } private void sendRequest(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig, Map values) { - PDU request = pduService.createPdu(sessionContext, communicationConfig, values); - RequestInfo requestInfo = new RequestInfo(communicationConfig.getSpec(), communicationConfig.getAllMappings()); - sendRequest(sessionContext, request, requestInfo); + List request = pduService.createPdus(sessionContext, communicationConfig, values); + RequestContext requestContext = RequestContext.builder() + .communicationSpec(communicationConfig.getSpec()) + .method(communicationConfig.getMethod()) + .responseMappings(communicationConfig.getAllMappings()) + .requestSize(request.size()) + .build(); + sendRequest(sessionContext, request, requestContext); } - private void sendRequest(DeviceSessionContext sessionContext, PDU request, RequestInfo requestInfo) { - if (request.size() > 0) { - log.trace("Executing SNMP request for device {}. Variables bindings: {}", sessionContext.getDeviceId(), request.getVariableBindings()); + private void sendRequest(DeviceSessionContext sessionContext, List request, RequestContext requestContext) { + for (PDU pdu : request) { + log.debug("Executing SNMP request for device {} with {} variable bindings", sessionContext.getDeviceId(), pdu.size()); try { - snmp.send(request, sessionContext.getTarget(), requestInfo, sessionContext); + snmp.send(pdu, sessionContext.getTarget(), requestContext, sessionContext); } catch (IOException e) { log.error("Failed to send SNMP request to device {}: {}", sessionContext.getDeviceId(), e.toString()); + transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), e); } } } @@ -215,51 +236,135 @@ public class SnmpTransportService implements TbTransportService { DataType dataType = snmpMapping.getDataType(); PDU request = pduService.createSingleVariablePdu(sessionContext, snmpMethod, oid, value, dataType); - RequestInfo requestInfo = new RequestInfo(toDeviceRpcRequestMsg.getRequestId(), communicationConfig.getSpec(), communicationConfig.getAllMappings()); - sendRequest(sessionContext, request, requestInfo); + RequestContext requestContext = RequestContext.builder() + .requestId(toDeviceRpcRequestMsg.getRequestId()) + .communicationSpec(communicationConfig.getSpec()) + .method(snmpMethod) + .responseMappings(communicationConfig.getAllMappings()) + .requestSize(1) + .build(); + sendRequest(sessionContext, List.of(request), requestContext); } public void processResponseEvent(DeviceSessionContext sessionContext, ResponseEvent event) { ((Snmp) event.getSource()).cancel(event.getRequest(), sessionContext); - + RequestContext requestContext = (RequestContext) event.getUserObject(); if (event.getError() != null) { log.warn("SNMP response error: {}", event.getError().toString()); + transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), new RuntimeException(event.getError())); return; } - PDU response = event.getResponse(); - if (response == null) { - log.debug("No response from SNMP device {}, requestId: {}", sessionContext.getDeviceId(), event.getRequest().getRequestID()); + PDU responsePdu = event.getResponse(); + if (log.isTraceEnabled()) { + log.trace("Received PDU for device {}: {}", sessionContext.getDeviceId(), responsePdu); + } + + List response; + if (requestContext.getRequestSize() == 1) { + if (responsePdu == null) { + log.debug("No response from SNMP device {}, requestId: {}", sessionContext.getDeviceId(), event.getRequest().getRequestID()); + if (requestContext.getMethod() == SnmpMethod.GET) { + transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), new RuntimeException("No response from device")); + } + return; + } + response = List.of(responsePdu); + } else { + List responseParts = requestContext.getResponseParts(); + responseParts.add(responsePdu); + if (responseParts.size() == requestContext.getRequestSize()) { + response = new ArrayList<>(); + for (PDU responsePart : responseParts) { + if (responsePart != null) { + response.add(responsePart); + } + } + log.debug("All response parts are collected for request to device {}", sessionContext.getDeviceId()); + } else { + log.trace("Awaiting other response parts for request to device {}", sessionContext.getDeviceId()); + return; + } + } + + responseProcessingExecutor.execute(() -> { + try { + processResponse(sessionContext, response, requestContext); + } catch (Exception e) { + transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), e); + } + }); + } + + /* + * SNMP notifications handler + * + * TODO: add check for host uniqueness when saving device (for backward compatibility - only for the ones using from-device RPC requests) + * + * NOTE: SNMP TRAPs support won't work properly when there is more than one SNMP transport, + * due to load-balancing of requests from devices: session might not be on this instance + * */ + @Override + public void processPdu(CommandResponderEvent event) { + Address sourceAddress = event.getPeerAddress(); + DeviceSessionContext sessionContext = transportContext.getSessions().stream() + .filter(session -> session.getTarget().getAddress().equals(sourceAddress)) + .findFirst().orElse(null); + if (sessionContext == null) { + log.warn("SNMP TRAP processing failed: couldn't find device session for address {}", sourceAddress); return; } - RequestInfo requestInfo = (RequestInfo) event.getUserObject(); + try { + processIncomingTrap(sessionContext, event); + } catch (Throwable e) { + transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), + SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST.getLabel(), e); + } + } + + private void processIncomingTrap(DeviceSessionContext sessionContext, CommandResponderEvent event) { + PDU pdu = event.getPDU(); + if (pdu == null) { + log.warn("Got empty trap from device {}", sessionContext.getDeviceId()); + throw new IllegalArgumentException("Received TRAP with no data"); + } + + log.debug("Processing SNMP trap from device {} (PDU: {}}", sessionContext.getDeviceId(), pdu); + SnmpCommunicationConfig communicationConfig = sessionContext.getProfileTransportConfiguration().getCommunicationConfigs().stream() + .filter(config -> config.getSpec() == SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST).findFirst() + .orElseThrow(() -> new IllegalArgumentException("No config found for to-server RPC requests")); + RequestContext requestContext = RequestContext.builder() + .communicationSpec(communicationConfig.getSpec()) + .responseMappings(communicationConfig.getAllMappings()) + .method(SnmpMethod.TRAP) + .build(); + responseProcessingExecutor.execute(() -> { - processResponse(sessionContext, response, requestInfo); + processResponse(sessionContext, List.of(pdu), requestContext); }); } - private void processResponse(DeviceSessionContext sessionContext, PDU response, RequestInfo requestInfo) { - ResponseProcessor responseProcessor = responseProcessors.get(requestInfo.getCommunicationSpec()); + private void processResponse(DeviceSessionContext sessionContext, List response, RequestContext requestContext) { + ResponseProcessor responseProcessor = responseProcessors.get(requestContext.getCommunicationSpec()); if (responseProcessor == null) return; - JsonObject responseData = responseDataMappers.get(requestInfo.getCommunicationSpec()).map(response, requestInfo); - - if (responseData.entrySet().isEmpty()) { - log.debug("No values is the SNMP response for device {}. Request id: {}", sessionContext.getDeviceId(), response.getRequestID()); - return; + JsonObject responseData = responseDataMappers.get(requestContext.getCommunicationSpec()).map(response, requestContext); + if (responseData.size() == 0) { + log.warn("No values in the SNMP response for device {}", sessionContext.getDeviceId()); + throw new IllegalArgumentException("No values in the response"); } - responseProcessor.process(responseData, requestInfo, sessionContext); + responseProcessor.process(responseData, requestContext, sessionContext); reportActivity(sessionContext.getSessionInfo()); } private void configureResponseDataMappers() { - responseDataMappers.put(SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST, (pdu, requestInfo) -> { + responseDataMappers.put(SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST, (pdus, requestContext) -> { JsonObject responseData = new JsonObject(); - pduService.processPdu(pdu).forEach((oid, value) -> { - requestInfo.getResponseMappings().stream() + pduService.processPdus(pdus).forEach((oid, value) -> { + requestContext.getResponseMappings().stream() .filter(snmpMapping -> snmpMapping.getOid().equals(oid.toDottedString())) .findFirst() .ifPresent(snmpMapping -> { @@ -269,8 +374,8 @@ public class SnmpTransportService implements TbTransportService { return responseData; }); - ResponseDataMapper defaultResponseDataMapper = (pdu, requestInfo) -> { - return pduService.processPdu(pdu, requestInfo.getResponseMappings()); + ResponseDataMapper defaultResponseDataMapper = (pdus, requestContext) -> { + return pduService.processPdus(pdus, requestContext.getResponseMappings()); }; Arrays.stream(SnmpCommunicationSpec.values()) .forEach(communicationSpec -> { @@ -279,34 +384,39 @@ public class SnmpTransportService implements TbTransportService { } private void configureResponseProcessors() { - responseProcessors.put(SnmpCommunicationSpec.TELEMETRY_QUERYING, (responseData, requestInfo, sessionContext) -> { + responseProcessors.put(SnmpCommunicationSpec.TELEMETRY_QUERYING, (responseData, requestContext, sessionContext) -> { TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(responseData); transportService.process(sessionContext.getSessionInfo(), postTelemetryMsg, null); log.debug("Posted telemetry for SNMP device {}: {}", sessionContext.getDeviceId(), responseData); }); - responseProcessors.put(SnmpCommunicationSpec.CLIENT_ATTRIBUTES_QUERYING, (responseData, requestInfo, sessionContext) -> { + responseProcessors.put(SnmpCommunicationSpec.CLIENT_ATTRIBUTES_QUERYING, (responseData, requestContext, sessionContext) -> { TransportProtos.PostAttributeMsg postAttributesMsg = JsonConverter.convertToAttributesProto(responseData); transportService.process(sessionContext.getSessionInfo(), postAttributesMsg, null); log.debug("Posted attributes for SNMP device {}: {}", sessionContext.getDeviceId(), responseData); }); - responseProcessors.put(SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST, (responseData, requestInfo, sessionContext) -> { + responseProcessors.put(SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST, (responseData, requestContext, sessionContext) -> { TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() - .setRequestId(requestInfo.getRequestId()) + .setRequestId(requestContext.getRequestId()) .setPayload(JsonConverter.toJson(responseData)) .build(); transportService.process(sessionContext.getSessionInfo(), rpcResponseMsg, null); log.debug("Posted RPC response {} for device {}", responseData, sessionContext.getDeviceId()); }); + + responseProcessors.put(SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST, (responseData, requestContext, sessionContext) -> { + TransportProtos.ToServerRpcRequestMsg toServerRpcRequestMsg = TransportProtos.ToServerRpcRequestMsg.newBuilder() + .setRequestId(0) + .setMethodName(requestContext.getMethod().name()) + .setParams(JsonConverter.toJson(responseData)) + .build(); + transportService.process(sessionContext.getSessionInfo(), toServerRpcRequestMsg, null); + }); } private void reportActivity(TransportProtos.SessionInfoProto sessionInfo) { - transportService.process(sessionInfo, TransportProtos.SubscriptionInfoProto.newBuilder() - .setAttributeSubscription(true) - .setRpcSubscription(true) - .setLastActivityTime(System.currentTimeMillis()) - .build(), TransportServiceCallback.EMPTY); + transportService.reportActivity(sessionInfo); } @@ -335,29 +445,34 @@ public class SnmpTransportService implements TbTransportService { } @Data - private static class RequestInfo { - private Integer requestId; - private SnmpCommunicationSpec communicationSpec; - private List responseMappings; + private static class RequestContext { + private final Integer requestId; + private final SnmpCommunicationSpec communicationSpec; + private final SnmpMethod method; + private final List responseMappings; - public RequestInfo(Integer requestId, SnmpCommunicationSpec communicationSpec, List responseMappings) { - this.requestId = requestId; - this.communicationSpec = communicationSpec; - this.responseMappings = responseMappings; - } + private final int requestSize; + private List responseParts; - public RequestInfo(SnmpCommunicationSpec communicationSpec, List responseMappings) { + @Builder + public RequestContext(Integer requestId, SnmpCommunicationSpec communicationSpec, SnmpMethod method, List responseMappings, int requestSize) { + this.requestId = requestId; this.communicationSpec = communicationSpec; + this.method = method; this.responseMappings = responseMappings; + this.requestSize = requestSize; + if (requestSize > 1) { + this.responseParts = Collections.synchronizedList(new ArrayList<>()); + } } } private interface ResponseDataMapper { - JsonObject map(PDU pdu, RequestInfo requestInfo); + JsonObject map(List pdus, RequestContext requestContext); } private interface ResponseProcessor { - void process(JsonObject responseData, RequestInfo requestInfo, DeviceSessionContext sessionContext); + void process(JsonObject responseData, RequestContext requestContext, DeviceSessionContext sessionContext); } } diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java index 2b7c452ca7..b95a11e8c2 100644 --- a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java +++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.snmp.session; +import lombok.Builder; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -26,9 +27,12 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.profile.SnmpDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rpc.RpcStatus; +import org.thingsboard.server.common.data.transport.snmp.SnmpCommunicationSpec; import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.TransportServiceCallback; +import org.thingsboard.server.common.transport.service.DefaultTransportService; import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; @@ -57,17 +61,22 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S private SnmpDeviceTransportConfiguration deviceTransportConfiguration; @Getter private final Device device; + @Getter + private final TenantId tenantId; private final SnmpTransportContext snmpTransportContext; private final AtomicInteger msgIdSeq = new AtomicInteger(0); @Getter private boolean isActive = true; + @Setter + private Runnable sessionTimeoutHandler; @Getter private final List> queryingTasks = new LinkedList<>(); - public DeviceSessionContext(Device device, DeviceProfile deviceProfile, String token, + @Builder + public DeviceSessionContext(TenantId tenantId, Device device, DeviceProfile deviceProfile, String token, SnmpDeviceProfileTransportConfiguration profileTransportConfiguration, SnmpDeviceTransportConfiguration deviceTransportConfiguration, SnmpTransportContext snmpTransportContext) throws Exception { @@ -75,6 +84,7 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S super.setDeviceId(device.getId()); super.setDeviceProfile(deviceProfile); this.device = device; + this.tenantId = tenantId; this.token = token; this.snmpTransportContext = snmpTransportContext; @@ -131,19 +141,32 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S @Override public void onAttributeUpdate(UUID sessionId, AttributeUpdateNotificationMsg attributeUpdateNotification) { log.trace("[{}] Received attributes update notification to device", sessionId); - snmpTransportContext.getSnmpTransportService().onAttributeUpdate(this, attributeUpdateNotification); + try { + snmpTransportContext.getSnmpTransportService().onAttributeUpdate(this, attributeUpdateNotification); + } catch (Exception e) { + snmpTransportContext.getTransportService().errorEvent(getTenantId(), getDeviceId(), SnmpCommunicationSpec.SHARED_ATTRIBUTES_SETTING.getLabel(), e); + } } @Override public void onRemoteSessionCloseCommand(UUID sessionId, SessionCloseNotificationProto sessionCloseNotification) { log.trace("[{}] Received the remote command to close the session: {}", sessionId, sessionCloseNotification.getMessage()); + if (sessionCloseNotification.getMessage().equals(DefaultTransportService.SESSION_EXPIRED_MESSAGE)) { + if (sessionTimeoutHandler != null) { + sessionTimeoutHandler.run(); + } + } } @Override public void onToDeviceRpcRequest(UUID sessionId, ToDeviceRpcRequestMsg toDeviceRequest) { log.trace("[{}] Received RPC command to device", sessionId); - snmpTransportContext.getSnmpTransportService().onToDeviceRpcRequest(this, toDeviceRequest); - snmpTransportContext.getTransportService().process(getSessionInfo(), toDeviceRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY); + try { + snmpTransportContext.getSnmpTransportService().onToDeviceRpcRequest(this, toDeviceRequest); + snmpTransportContext.getTransportService().process(getSessionInfo(), toDeviceRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY); + } catch (Exception e) { + snmpTransportContext.getTransportService().errorEvent(getTenantId(), getDeviceId(), SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST.getLabel(), e); + } } @Override diff --git a/common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpDeviceSimulatorV2.java b/common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpDeviceSimulatorV2.java index 5fd7f3bed0..fa49b17f13 100644 --- a/common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpDeviceSimulatorV2.java +++ b/common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpDeviceSimulatorV2.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.transport.snmp; -import org.snmp4j.CommandResponderEvent; import org.snmp4j.CommunityTarget; import org.snmp4j.PDU; import org.snmp4j.Snmp; @@ -35,7 +34,6 @@ import org.snmp4j.agent.mo.snmp.SnmpTargetMIB; import org.snmp4j.agent.mo.snmp.StorageType; import org.snmp4j.agent.mo.snmp.VacmMIB; import org.snmp4j.agent.security.MutableVACM; -import org.snmp4j.mp.MPv3; import org.snmp4j.mp.SnmpConstants; import org.snmp4j.security.SecurityLevel; import org.snmp4j.security.SecurityModel; @@ -53,27 +51,11 @@ import org.snmp4j.transport.TransportMappings; import java.io.File; import java.io.IOException; import java.util.Map; -import java.util.function.Consumer; import java.util.stream.Collectors; @SuppressWarnings("deprecation") public class SnmpDeviceSimulatorV2 extends BaseAgent { - public static class RequestProcessor extends CommandProcessor { - private final Consumer processor; - - public RequestProcessor(Consumer processor) { - super(new OctetString(MPv3.createLocalEngineID())); - this.processor = processor; - } - - @Override - public void processPdu(CommandResponderEvent event) { - processor.accept(event); - } - } - - private final Target target; private final Address address; private Snmp snmp; @@ -81,10 +63,7 @@ public class SnmpDeviceSimulatorV2 extends BaseAgent { private final String password; public SnmpDeviceSimulatorV2(int port, String password) throws IOException { - super(new File("conf.agent"), new File("bootCounter.agent"), new RequestProcessor(event -> { - System.out.println("aboba"); - ((Snmp) event.getSource()).cancel(event.getPDU(), event1 -> System.out.println("canceled")); - })); + super(new File("conf.agent"), new File("bootCounter.agent"), new CommandProcessor(new OctetString("12312"))); CommunityTarget target = new CommunityTarget(); target.setCommunity(new OctetString(password)); this.address = GenericAddress.parse("udp:0.0.0.0/" + port); diff --git a/common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpTestV2.java b/common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpTestV2.java index bf371058aa..5367ef93cf 100644 --- a/common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpTestV2.java +++ b/common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpTestV2.java @@ -16,34 +16,41 @@ package org.thingsboard.server.transport.snmp; import java.io.IOException; +import java.util.HashMap; import java.util.Map; import java.util.Scanner; public class SnmpTestV2 { - public static void main(String[] args) throws IOException { - SnmpDeviceSimulatorV2 device = new SnmpDeviceSimulatorV2(1610, "public"); - device.start(); - device.setUpMappings(Map.of( - ".1.3.6.1.2.1.1.1.50", "12", - ".1.3.6.1.2.1.2.1.52", "56", - ".1.3.6.1.2.1.3.1.54", "yes", - ".1.3.6.1.2.1.7.1.58", "" - )); + private static final Scanner scanner = new Scanner(System.in); + public static void main(String[] args) throws IOException { + SnmpDeviceSimulatorV2 client = new SnmpDeviceSimulatorV2(1610, "public"); -// while (true) { -// new Scanner(System.in).nextLine(); -// device.sendTrap("127.0.0.1", 1062, Map.of(".1.3.6.1.2.87.1.56", "12")); -// System.out.println("sent"); + client.start(); + Map mappings = new HashMap<>(); +// for (int i = 1; i <= 500; i++) { +// String oid = String.format(".1.3.6.1.2.1.%s.1.52", i); +// mappings.put(oid, "value_" + i); // } + mappings.put("1.3.6.1.2.1.266.1.52", "****"); -// Snmp snmp = new Snmp(device.transportMappings[0]); -// device.snmp.addCommandResponder(event -> { -// System.out.println(event); -// }); + client.setUpMappings(mappings); + inputTraps(client); + + scanner.nextLine(); + } - new Scanner(System.in).nextLine(); + private static void inputTraps(SnmpDeviceSimulatorV2 client) throws IOException { + while (true) { + String data = scanner.nextLine(); + if (!data.isEmpty()) { + client.sendTrap("127.0.0.1", 1620, Map.of( + "1.3.6.1.2.1.266.1.52", data + " (266)", + "1.3.6.1.2.1.267.1.52", data + " (267)" + )); + } + } } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index ceef62368a..ab62b4b0df 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java @@ -17,6 +17,9 @@ package org.thingsboard.server.common.transport; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; @@ -144,6 +147,10 @@ public interface TransportService { void reportActivity(SessionInfoProto sessionInfo); + void lifecycleEvent(TenantId tenantId, DeviceId deviceId, ComponentLifecycleEvent eventType, boolean success, Throwable error); + + void errorEvent(TenantId tenantId, DeviceId deviceId, String method, Throwable error); + void deregisterSession(SessionInfoProto sessionInfo); void log(SessionInfoProto sessionInfo, String msg); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 48af9b9195..bc11570fc1 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -22,6 +22,7 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; @@ -49,6 +50,8 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.limit.LimitedApi; +import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger; import org.thingsboard.server.common.data.rpc.RpcStatus; @@ -827,6 +830,37 @@ public class DefaultTransportService implements TransportService { sessionsToRemove.forEach(sessionsActivity::remove); } + @Override + public void lifecycleEvent(TenantId tenantId, DeviceId deviceId, ComponentLifecycleEvent eventType, boolean success, Throwable error) { + ToCoreMsg msg = ToCoreMsg.newBuilder() + .setLifecycleEventMsg(TransportProtos.LifecycleEventProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setEntityIdMSB(deviceId.getId().getMostSignificantBits()) + .setEntityIdLSB(deviceId.getId().getLeastSignificantBits()) + .setServiceId(serviceInfoProvider.getServiceId()) + .setLcEventType(eventType.name()) + .setSuccess(success) + .setError(error != null ? ExceptionUtils.getStackTrace(error) : "")) + .build(); + sendToCore(tenantId, deviceId, msg, deviceId.getId(), TransportServiceCallback.EMPTY); + } + + @Override + public void errorEvent(TenantId tenantId, DeviceId deviceId, String method, Throwable error) { + ToCoreMsg msg = ToCoreMsg.newBuilder() + .setErrorEventMsg(TransportProtos.ErrorEventProto.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setEntityIdMSB(deviceId.getId().getMostSignificantBits()) + .setEntityIdLSB(deviceId.getId().getLeastSignificantBits()) + .setServiceId(serviceInfoProvider.getServiceId()) + .setMethod(method) + .setError(ExceptionUtils.getStackTrace(error))) + .build(); + sendToCore(tenantId, deviceId, msg, deviceId.getId(), TransportServiceCallback.EMPTY); + } + @Override public SessionMetaData registerSyncSession(TransportProtos.SessionInfoProto sessionInfo, SessionMsgListener listener, long timeout) { SessionMetaData currentSession = new SessionMetaData(sessionInfo, TransportProtos.SessionType.SYNC, listener); @@ -1118,18 +1152,21 @@ public class DefaultTransportService implements TransportService { } protected void sendToDeviceActor(TransportProtos.SessionInfoProto sessionInfo, TransportToDeviceActorMsg toDeviceActorMsg, TransportServiceCallback callback) { - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, getTenantId(sessionInfo), getDeviceId(sessionInfo)); + ToCoreMsg toCoreMsg = ToCoreMsg.newBuilder().setToDeviceActorMsg(toDeviceActorMsg).build(); + sendToCore(getTenantId(sessionInfo), getDeviceId(sessionInfo), toCoreMsg, getRoutingKey(sessionInfo), callback); + } + + private void sendToCore(TenantId tenantId, EntityId entityId, ToCoreMsg msg, UUID routingKey, TransportServiceCallback callback) { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); if (log.isTraceEnabled()) { - log.trace("[{}][{}] Pushing to topic {} message {}", getTenantId(sessionInfo), getDeviceId(sessionInfo), tpi.getFullTopicName(), toDeviceActorMsg); + log.trace("[{}][{}] Pushing to topic {} message {}", tenantId, entityId, tpi.getFullTopicName(), msg); } + TransportTbQueueCallback transportTbQueueCallback = callback != null ? new TransportTbQueueCallback(callback) : null; tbCoreProducerStats.incrementTotal(); StatsCallback wrappedCallback = new StatsCallback(transportTbQueueCallback, tbCoreProducerStats); - tbCoreMsgProducer.send(tpi, - new TbProtoQueueMsg<>(getRoutingKey(sessionInfo), - ToCoreMsg.newBuilder().setToDeviceActorMsg(toDeviceActorMsg).build()), - wrappedCallback); + tbCoreMsgProducer.send(tpi, new TbProtoQueueMsg<>(routingKey, msg), wrappedCallback); } private void sendToRuleEngine(TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) { diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index 9f980f36c8..7c8d174af2 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -28,6 +28,7 @@ import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.common.collect.Lists; import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.KvEntry; @@ -35,7 +36,6 @@ import java.io.File; import java.io.IOException; import java.io.Reader; import java.io.Writer; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; @@ -228,27 +228,24 @@ public class JacksonUtil { return node; } - public static void replaceUuidsRecursively(JsonNode node, Set skipFieldsSet, Pattern includedFieldsPattern, UnaryOperator replacer) { + public static void replaceUuidsRecursively(JsonNode node, Set skippedRootFields, Pattern includedFieldsPattern, UnaryOperator replacer, boolean root) { if (node == null) { return; } if (node.isObject()) { ObjectNode objectNode = (ObjectNode) node; - List fieldNames = new ArrayList<>(objectNode.size()); - objectNode.fieldNames().forEachRemaining(fieldNames::add); + List fieldNames = Lists.newArrayList(objectNode.fieldNames()); for (String fieldName : fieldNames) { - if (skipFieldsSet.contains(fieldName)) { + if (root && skippedRootFields.contains(fieldName)) { continue; } - if (includedFieldsPattern != null) { - if (!RegexUtils.matches(fieldName, includedFieldsPattern)) { - continue; - } - } var child = objectNode.get(fieldName); if (child.isObject() || child.isArray()) { - replaceUuidsRecursively(child, skipFieldsSet, includedFieldsPattern, replacer); + replaceUuidsRecursively(child, skippedRootFields, includedFieldsPattern, replacer, false); } else if (child.isTextual()) { + if (includedFieldsPattern != null && !RegexUtils.matches(fieldName, includedFieldsPattern)) { + continue; + } String text = child.asText(); String newText = RegexUtils.replace(text, RegexUtils.UUID_PATTERN, uuid -> replacer.apply(UUID.fromString(uuid)).toString()); if (!text.equals(newText)) { @@ -261,7 +258,7 @@ public class JacksonUtil { for (int i = 0; i < array.size(); i++) { JsonNode arrayElement = array.get(i); if (arrayElement.isObject() || arrayElement.isArray()) { - replaceUuidsRecursively(arrayElement, skipFieldsSet, includedFieldsPattern, replacer); + replaceUuidsRecursively(arrayElement, skippedRootFields, includedFieldsPattern, replacer, false); } else if (arrayElement.isTextual()) { String text = arrayElement.asText(); String newText = RegexUtils.replace(text, RegexUtils.UUID_PATTERN, uuid -> replacer.apply(UUID.fromString(uuid)).toString()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java index 3d9d920e42..28d8f20b81 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java @@ -69,6 +69,8 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String INCORRECT_DEVICE_ID = "Incorrect deviceId "; public static final String DEFAULT_DEVICE_TELEMETRY_TOPIC = "v1/devices/me/telemetry"; + public static final String HTTP_DEFAULT_PORT = "80"; + public static final String HTTPS_DEFAULT_PORT = "443"; private final Map certs = new ConcurrentHashMap<>(); @@ -225,8 +227,9 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService return null; } String hostName = getHost(baseUrl, properties); - String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort(); - + String propertiesPort = properties.getPort(); + String port = (propertiesPort.isEmpty() || HTTP_DEFAULT_PORT.equals(propertiesPort) || HTTPS_DEFAULT_PORT.equals(propertiesPort)) + ? "" : ":" + propertiesPort; return DeviceConnectivityUtil.getHttpPublishCommand(protocol, hostName, port, deviceCredentials); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeDao.java index 7df394ddd4..cd8fbb0a3c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeDao.java @@ -30,7 +30,7 @@ import java.util.List; */ public interface RuleNodeDao extends Dao { - List findRuleNodesByTenantIdAndType(TenantId tenantId, String type, String search); + List findRuleNodesByTenantIdAndType(TenantId tenantId, String type, String configurationSearch); PageData findAllRuleNodesByType(String type, PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java index 08e047904f..43878adc93 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java @@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.util.ReflectionUtils; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.service.DataValidator; @@ -41,15 +40,14 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; @Component @Slf4j public class RuleChainDataValidator extends DataValidator { - @Autowired - private RuleChainDao ruleChainDao; - @Autowired @Lazy private RuleChainService ruleChainService; @@ -88,15 +86,19 @@ public class RuleChainDataValidator extends DataValidator { } } - public static void validateMetaData(RuleChainMetaData ruleChainMetaData) { + public static List validateMetaData(RuleChainMetaData ruleChainMetaData) { ConstraintValidator.validateFields(ruleChainMetaData); - ruleChainMetaData.getNodes().forEach(RuleChainDataValidator::validateRuleNode); + List throwables = ruleChainMetaData.getNodes().stream() + .map(RuleChainDataValidator::validateRuleNode) + .filter(Objects::nonNull) + .collect(Collectors.toList()); if (CollectionUtils.isNotEmpty(ruleChainMetaData.getConnections())) { validateCircles(ruleChainMetaData.getConnections()); } + return throwables; } - public static void validateRuleNode(RuleNode ruleNode) { + public static Throwable validateRuleNode(RuleNode ruleNode) { String errorPrefix = "'" + ruleNode.getName() + "' node configuration is invalid: "; ConstraintValidator.validateFields(ruleNode, errorPrefix); Object nodeConfig; @@ -104,11 +106,12 @@ public class RuleChainDataValidator extends DataValidator { Class nodeConfigType = ReflectionUtils.getAnnotationProperty(ruleNode.getType(), "org.thingsboard.rule.engine.api.RuleNode", "configClazz"); nodeConfig = JacksonUtil.treeToValue(ruleNode.getConfiguration(), nodeConfigType); - } catch (Exception e) { - log.warn("Failed to validate node configuration: {}", ExceptionUtils.getRootCauseMessage(e)); - return; + } catch (Throwable t) { + log.warn("Failed to validate node configuration: {}", ExceptionUtils.getRootCauseMessage(t)); + return t; } ConstraintValidator.validateFields(nodeConfig, errorPrefix); + return null; } private static void validateCircles(List connectionInfos) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java index 7adf8aa0c8..827b73a0c1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java @@ -56,8 +56,8 @@ public class JpaRuleNodeDao extends JpaAbstractDao imp } @Override - public List findRuleNodesByTenantIdAndType(TenantId tenantId, String type, String search) { - return DaoUtil.convertDataList(ruleNodeRepository.findRuleNodesByTenantIdAndType(tenantId.getId(), type, search)); + public List findRuleNodesByTenantIdAndType(TenantId tenantId, String type, String configurationSearch) { + return DaoUtil.convertDataList(ruleNodeRepository.findRuleNodesByTenantIdAndType(tenantId.getId(), type, configurationSearch)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java index e016462255..c8ecb8000b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java @@ -29,19 +29,22 @@ import java.util.UUID; public interface RuleNodeRepository extends JpaRepository { - @Query("SELECT r FROM RuleNodeEntity r WHERE r.ruleChainId in " + - "(select id from RuleChainEntity rc WHERE rc.tenantId = :tenantId) " + - "AND r.type = :ruleType AND LOWER(r.configuration) LIKE LOWER(CONCAT('%', :searchText, '%')) ") + @Query(nativeQuery = true, value = "SELECT * FROM rule_node r WHERE r.rule_chain_id in " + + "(select id from rule_chain rc WHERE rc.tenant_id = :tenantId) AND r.type = :ruleType " + + " AND (:searchText IS NULL OR r.configuration ILIKE CONCAT('%', :searchText, '%'))") List findRuleNodesByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("ruleType") String ruleType, @Param("searchText") String searchText); - @Query("SELECT r FROM RuleNodeEntity r WHERE r.type = :ruleType AND LOWER(r.configuration) LIKE LOWER(CONCAT('%', :searchText, '%')) ") + @Query(nativeQuery = true, value = "SELECT * FROM rule_node r WHERE r.type = :ruleType " + + " AND (:searchText IS NULL OR r.configuration ILIKE CONCAT('%', :searchText, '%'))") Page findAllRuleNodesByType(@Param("ruleType") String ruleType, @Param("searchText") String searchText, Pageable pageable); - @Query("SELECT r FROM RuleNodeEntity r WHERE r.type = :ruleType AND r.configurationVersion < :version AND LOWER(r.configuration) LIKE LOWER(CONCAT('%', :searchText, '%')) ") + @Query(nativeQuery = true, value = "SELECT * FROM rule_node r WHERE r.type = :ruleType " + + " AND configuration_version < :version " + + " AND (:searchText IS NULL OR r.configuration ILIKE CONCAT('%', :searchText, '%'))") Page findAllRuleNodesByTypeAndVersionLessThan(@Param("ruleType") String ruleType, @Param("version") int version, @Param("searchText") String searchText, diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java b/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java index e417a5a50a..31bb00ebd9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.util; import com.fasterxml.jackson.databind.JsonNode; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; @@ -48,8 +49,8 @@ public class KvUtils { String key = tsKvEntry.getKey(); - if (key == null) { - throw new DataValidationException("Key can't be null"); + if (StringUtils.isBlank(key)) { + throw new DataValidationException("Key can't be null or empty"); } if (key.length() > 255) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidatorTest.java index 7f9d3e1fc2..0fdd1a04d2 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidatorTest.java @@ -23,7 +23,6 @@ import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; -import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; @@ -35,8 +34,6 @@ import static org.mockito.Mockito.verify; @SpringBootTest(classes = RuleChainDataValidator.class) class RuleChainDataValidatorTest { - @MockBean - RuleChainDao ruleChainDao; @MockBean RuleChainService ruleChainService; @MockBean diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java new file mode 100644 index 0000000000..6e77927233 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java @@ -0,0 +1,158 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.rule; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.common.util.concurrent.ListeningExecutorService; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.dao.AbstractJpaDaoTest; +import org.thingsboard.server.dao.rule.RuleChainDao; +import org.thingsboard.server.dao.rule.RuleNodeDao; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class JpaRuleNodeDaoTest extends AbstractJpaDaoTest { + + public static final int COUNT = 40; + public static final String PREFIX_FOR_RULE_NODE_NAME = "SEARCH_TEXT_"; + List ruleNodeIds; + TenantId tenantId1; + TenantId tenantId2; + RuleChainId ruleChainId1; + RuleChainId ruleChainId2; + + @Autowired + private RuleChainDao ruleChainDao; + + @Autowired + private RuleNodeDao ruleNodeDao; + + ListeningExecutorService executor; + + @Before + public void setUp() { + tenantId1 = TenantId.fromUUID(Uuids.timeBased()); + ruleChainId1 = new RuleChainId(UUID.randomUUID()); + tenantId2 = TenantId.fromUUID(Uuids.timeBased()); + ruleChainId2 = new RuleChainId(UUID.randomUUID()); + + ruleNodeIds = createRuleNodes(tenantId1, tenantId2, ruleChainId1, ruleChainId2, COUNT); + } + + @After + public void tearDown() throws Exception { + ruleNodeDao.removeAllByIds(ruleNodeIds); + if (executor != null) { + executor.shutdownNow(); + } + } + + @Test + public void testSaveRuleName0x00_thenSomeDatabaseException() { + RuleNode ruleNode = getRuleNode(ruleChainId1, "T", "\u0000"); + assertThatThrownBy(() -> ruleNodeIds.add(ruleNodeDao.save(tenantId1, ruleNode).getUuidId())); + } + + @Test + public void testFindRuleNodesByTenantIdAndType() { + List ruleNodes1 = ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId1, "A", PREFIX_FOR_RULE_NODE_NAME); + assertEquals(20, ruleNodes1.size()); + + List ruleNodes2 = ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId2, "B", PREFIX_FOR_RULE_NODE_NAME); + assertEquals(20, ruleNodes2.size()); + + ruleNodes1 = ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId1, "A", null); + assertEquals(20, ruleNodes1.size()); + + ruleNodes2 = ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId2, "B", null); + assertEquals(20, ruleNodes2.size()); + } + + @Test + public void testFindRuleNodesByType() { + PageData ruleNodes = ruleNodeDao.findAllRuleNodesByType( "A", new PageLink(10, 0, PREFIX_FOR_RULE_NODE_NAME)); + assertEquals(20, ruleNodes.getTotalElements()); + assertEquals(2, ruleNodes.getTotalPages()); + assertEquals(10, ruleNodes.getData().size()); + + ruleNodes = ruleNodeDao.findAllRuleNodesByType( "A", new PageLink(10, 0)); + assertEquals(20, ruleNodes.getTotalElements()); + assertEquals(2, ruleNodes.getTotalPages()); + assertEquals(10, ruleNodes.getData().size()); + } + + @Test + public void testFindRuleNodesByTypeAndVersionLessThan() { + PageData ruleNodes = ruleNodeDao.findAllRuleNodesByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0, PREFIX_FOR_RULE_NODE_NAME)); + assertEquals(20, ruleNodes.getTotalElements()); + assertEquals(2, ruleNodes.getTotalPages()); + assertEquals(10, ruleNodes.getData().size()); + + ruleNodes = ruleNodeDao.findAllRuleNodesByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0)); + assertEquals(20, ruleNodes.getTotalElements()); + assertEquals(2, ruleNodes.getTotalPages()); + assertEquals(10, ruleNodes.getData().size()); + } + + private List createRuleNodes(TenantId tenantId1, TenantId tenantId2, RuleChainId ruleChainId1, RuleChainId ruleChainId2, int count) { + var chain1 = new RuleChain(ruleChainId1); + chain1.setTenantId(tenantId1); + chain1.setName(ruleChainId1.toString()); + ruleChainDao.save(tenantId1, chain1); + var chain2 = new RuleChain(ruleChainId2); + chain2.setTenantId(tenantId2); + chain2.setName(ruleChainId2.toString()); + ruleChainDao.save(tenantId2, chain2); + List savedRuleNodeIds = new ArrayList<>(); + for (int i = 0; i < count / 2; i++) { + savedRuleNodeIds.add(ruleNodeDao.save(tenantId1, getRuleNode(ruleChainId1, "A", Integer.toString(i))).getUuidId()); + savedRuleNodeIds.add(ruleNodeDao.save(tenantId2, getRuleNode(ruleChainId2, "B", Integer.toString(i + count / 2))).getUuidId()); + } + return savedRuleNodeIds; + } + + private RuleNode getRuleNode(RuleChainId ruleChainId, String type, String nameSuffix) { + return getRuleNode(ruleChainId, Uuids.timeBased(), type, nameSuffix); + } + + private RuleNode getRuleNode(RuleChainId ruleChainId, UUID ruleNodeId, String type, String nameSuffix) { + RuleNode ruleNode = new RuleNode(); + ruleNode.setId(new RuleNodeId(ruleNodeId)); + ruleNode.setRuleChainId(ruleChainId); + ruleNode.setName(nameSuffix); + ruleNode.setType(type); + ruleNode.setConfiguration(JacksonUtil.newObjectNode().put("searchHint", PREFIX_FOR_RULE_NODE_NAME + nameSuffix)); + ruleNode.setConfigurationVersion(0); + return ruleNode; + } +} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index b4320577c7..78ca337e0a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -235,6 +235,8 @@ services: tb-snmp-transport: restart: always image: "${DOCKER_REPO}/${SNMP_TRANSPORT_DOCKER_NAME}:${TB_VERSION}" + ports: + - "1620:1620/udp" environment: TB_SERVICE_ID: tb-snmp-transport JAVA_OPTS: "${JAVA_OPTS}" diff --git a/docker/tb-snmp-transport.env b/docker/tb-snmp-transport.env index e2cc39d658..32160de100 100644 --- a/docker/tb-snmp-transport.env +++ b/docker/tb-snmp-transport.env @@ -1,6 +1,7 @@ ZOOKEEPER_ENABLED=true ZOOKEEPER_URL=zookeeper:2181 +SNMP_BIND_PORT=1620 METRICS_ENABLED=true METRICS_ENDPOINTS_EXPOSE=prometheus WEB_APPLICATION_ENABLE=true diff --git a/pom.xml b/pom.xml index 33c9c2a8e4..6eba036d51 100755 --- a/pom.xml +++ b/pom.xml @@ -78,7 +78,7 @@ 3.8.1 3.21.9 1.42.1 - 1.1.0 + 1.1.1 1.18.18 1.2.4 1.2.5 diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java index b857307247..25364bf1db 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java @@ -15,6 +15,8 @@ */ package org.thingsboard.rule.engine.api; +import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; @@ -35,4 +37,18 @@ public interface TbNode { default void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) { } + /** + * Upgrades the configuration from a specific version to the current version specified in the + * {@link RuleNode} annotation for the instance of {@link TbNode}. + * + * @param fromVersion The version from which the configuration needs to be upgraded. + * @param oldConfiguration The old configuration to be upgraded. + * @return A pair consisting of a Boolean flag indicating the success of the upgrade + * and a JsonNode representing the upgraded configuration. + * @throws TbNodeException If an error occurs during the upgrade process. + */ + default TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { + return new TbPair<>(false, oldConfiguration); + } + } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbVersionedNode.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbVersionedNode.java deleted file mode 100644 index 95b93fa222..0000000000 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbVersionedNode.java +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright © 2016-2023 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.rule.engine.api; - -import com.fasterxml.jackson.databind.JsonNode; -import org.thingsboard.server.common.data.util.TbPair; - -public interface TbVersionedNode extends TbNode { - - /** - * Upgrades the configuration from a specific version to the current version specified in the - * {@link RuleNode} annotation for the instance of {@link TbVersionedNode}. - * - * @param fromVersion The version from which the configuration needs to be upgraded. - * @param oldConfiguration The old configuration to be upgraded. - * @return A pair consisting of a Boolean flag indicating the success of the upgrade - * and a JsonNode representing the upgraded configuration. - * @throws TbNodeException If an error occurs during the upgrade process. - */ - TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException; - -} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java index 3af5c9bfe1..9933f9bb73 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java @@ -22,9 +22,9 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbVersionedNode; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.EntityId; @@ -58,7 +58,7 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; "Output connections: True, False, Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeCheckRelationConfig") -public class TbCheckRelationNode implements TbVersionedNode { +public class TbCheckRelationNode implements TbNode { private static final String DIRECTION_PROPERTY_NAME = "direction"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java index f7b4eb163c..095612b4e1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java @@ -84,7 +84,7 @@ import static org.thingsboard.rule.engine.math.TbMathArgumentType.CONSTANT; ) public class TbMathNode implements TbNode { - private static final ConcurrentMap> locks = new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); + private static final ConcurrentMap> locks = new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); private final ThreadLocal customExpression = new ThreadLocal<>(); private TbMathNodeConfiguration config; private boolean msgBodyToJsonConversionRequired; @@ -111,21 +111,21 @@ public class TbMathNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { var semaphoreWithQueue = locks.computeIfAbsent(msg.getOriginator(), SemaphoreWithQueue::new); - semaphoreWithQueue.getQueue().add(new TbMsgTbContext(msg, ctx)); + semaphoreWithQueue.getQueue().add(new TbMsgTbContextBiFunction(msg, ctx, this::processMsgAsync)); tryProcessQueue(semaphoreWithQueue); } - void tryProcessQueue(SemaphoreWithQueue lockAndQueue) { + void tryProcessQueue(SemaphoreWithQueue lockAndQueue) { final Semaphore semaphore = lockAndQueue.getSemaphore(); - final Queue queue = lockAndQueue.getQueue(); + final Queue queue = lockAndQueue.getQueue(); while (!queue.isEmpty()) { // The semaphore have to be acquired before EACH poll and released before NEXT poll. // Otherwise, some message will remain unprocessed in queue if (!semaphore.tryAcquire()) { return; } - TbMsgTbContext tbMsgTbContext = null; + TbMsgTbContextBiFunction tbMsgTbContext = null; try { tbMsgTbContext = queue.poll(); if (tbMsgTbContext == null) { @@ -140,7 +140,7 @@ public class TbMathNode implements TbNode { } //DO PROCESSING final TbContext ctx = tbMsgTbContext.getCtx(); - final ListenableFuture resultMsgFuture = processMsgAsync(ctx, msg); + final ListenableFuture resultMsgFuture = tbMsgTbContext.getBiFunction().apply(ctx, msg); DonAsynchron.withCallback(resultMsgFuture, resultMsg -> { try { ctx.tellSuccess(resultMsg); @@ -156,10 +156,17 @@ public class TbMathNode implements TbNode { tryProcessQueue(lockAndQueue); } }, ctx.getDbCallbackExecutor()); - } catch (Throwable e) { + } catch (Throwable t) { semaphore.release(); - log.warn("[{}] Failed to process message: {}", lockAndQueue.getEntityId(), tbMsgTbContext == null ? null : tbMsgTbContext.getMsg(), e); - throw e; + if (tbMsgTbContext == null) { // if no message polled, the loop become infinite, will throw exception + log.error("[{}] Failed to process TbMsgTbContext queue", lockAndQueue.getEntityId(), t); + throw t; + } + TbMsg msg = tbMsgTbContext.getMsg(); + TbContext ctx = tbMsgTbContext.getCtx(); + log.warn("[{}] Failed to process message: {}", lockAndQueue.getEntityId(), msg, t); + ctx.tellFailure(msg, t); // you are not allowed to throw here, because queue will remain unprocessed + continue; // We are probably the last who process the queue. We have to continue poll until get successful callback or queue is empty } break; //submitted async exact one task. next poll will try on callback } @@ -367,7 +374,7 @@ public class TbMathNode implements TbNode { return function.apply(arg1.getValue(), arg2.getValue()); } - private ListenableFuture resolveArguments(TbContext ctx, TbMsg msg, Optional msgBodyOpt, TbMathArgument arg) { + ListenableFuture resolveArguments(TbContext ctx, TbMsg msg, Optional msgBodyOpt, TbMathArgument arg) { String argKey = getKeyFromTemplate(msg, arg.getType(), arg.getKey()); switch (arg.getType()) { case CONSTANT: @@ -433,9 +440,10 @@ public class TbMathNode implements TbNode { @Data @RequiredArgsConstructor - static public class TbMsgTbContext { + static public class TbMsgTbContextBiFunction { final TbMsg msg; final TbContext ctx; + final BiFunction> biFunction; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java index 0f87ba37f8..a3b7fead3f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java @@ -22,9 +22,9 @@ import com.google.common.util.concurrent.Futures; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbVersionedNode; import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.KvEntry; @@ -35,7 +35,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import java.util.NoSuchElementException; @Slf4j -public abstract class TbAbstractNodeWithFetchTo implements TbVersionedNode { +public abstract class TbAbstractNodeWithFetchTo implements TbNode { protected final static String FETCH_TO_PROPERTY_NAME = "fetchTo"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java index 0ae2a89abf..9bbee45477 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java @@ -24,9 +24,9 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbVersionedNode; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @@ -43,9 +43,7 @@ import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE; -import static org.thingsboard.server.common.data.DataConstants.NOTIFY_DEVICE_METADATA_KEY; -import static org.thingsboard.server.common.data.DataConstants.SCOPE; +import static org.thingsboard.server.common.data.DataConstants.*; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; @Slf4j @@ -64,7 +62,7 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_R configDirective = "tbActionNodeAttributesConfig", icon = "file_upload" ) -public class TbMsgAttributesNode implements TbVersionedNode { +public class TbMsgAttributesNode implements TbNode { static final String UPDATE_ATTRIBUTES_ONLY_ON_VALUE_CHANGE_KEY = "updateAttributesOnlyOnValueChange"; private TbMsgAttributesNodeConfiguration config; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java index 292292b89b..9eec38cfae 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java @@ -18,6 +18,9 @@ package org.thingsboard.rule.engine.math; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Triple; +import org.assertj.core.api.SoftAssertions; +import org.junit.Assert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,7 +30,9 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.verification.Timeout; import org.thingsboard.common.util.AbstractListeningExecutor; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; @@ -72,6 +77,10 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.willAnswer; import static org.mockito.BDDMockito.willReturn; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.timeout; @@ -128,7 +137,15 @@ public class TbMathNodeTest { return initNode(TbRuleNodeMathFunctionType.CUSTOM, expression, result, arguments); } + private TbMathNode initNodeWithCustomFunction(TbContext ctx, String expression, TbMathResult result, TbMathArgument... arguments) { + return initNode(ctx, TbRuleNodeMathFunctionType.CUSTOM, expression, result, arguments); + } + private TbMathNode initNode(TbRuleNodeMathFunctionType operation, String expression, TbMathResult result, TbMathArgument... arguments) { + return initNode(this.ctx, operation, expression, result, arguments); + } + + private TbMathNode initNode(TbContext ctx, TbRuleNodeMathFunctionType operation, String expression, TbMathResult result, TbMathArgument... arguments) { try { TbMathNodeConfiguration configuration = new TbMathNodeConfiguration(); configuration.setOperation(operation); @@ -521,10 +538,11 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey") ); TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 10).toString()); - Throwable thrown = assertThrows(RuntimeException.class, () -> { - node.onMsg(ctx, msg); - }); - assertNotNull(thrown.getMessage()); + node.onMsg(ctx, msg); + + ArgumentCaptor tCaptor = ArgumentCaptor.forClass(Throwable.class); + Mockito.verify(ctx, Mockito.timeout(5000)).tellFailure(eq(msg), tCaptor.capture()); + Assert.assertNotNull(tCaptor.getValue().getMessage()); } @Test @@ -534,11 +552,12 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); - TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); - Throwable thrown = assertThrows(RuntimeException.class, () -> { - node.onMsg(ctx, msg); - }); - assertNotNull(thrown.getMessage()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); + node.onMsg(ctx, msg); + + ArgumentCaptor tCaptor = ArgumentCaptor.forClass(Throwable.class); + Mockito.verify(ctx, Mockito.timeout(5000)).tellFailure(eq(msg), tCaptor.capture()); + Assert.assertNotNull(tCaptor.getValue().getMessage()); } @Test @@ -553,10 +572,10 @@ public class TbMathNodeTest { CountDownLatch slowProcessingLatch = new CountDownLatch(1); List slowMsgList = IntStream.range(0, 5) - .mapToObj(x -> TbMsg.newMsg("TEST", originatorSlow, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString())) + .mapToObj(x -> TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originatorSlow, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString())) .collect(Collectors.toList()); List fastMsgList = IntStream.range(0, 2) - .mapToObj(x -> TbMsg.newMsg("TEST", originatorFast, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString())) + .mapToObj(x -> TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originatorFast, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString())) .collect(Collectors.toList()); assertThat(slowMsgList.size()).as("slow msgs >= rule-dispatcher pool size").isGreaterThanOrEqualTo(RULE_DISPATCHER_POOL_SIZE); @@ -609,6 +628,115 @@ public class TbMathNodeTest { verify(ctx, never()).tellFailure(any(), any()); } + @Test + public void testExp4j_concurrentBySingleOriginator_processMsgAsyncException() { + TbMathNode node = spy(initNodeWithCustomFunction("2a+3b", + new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null), + new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a"), + new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b") + )); + + willThrow(new RuntimeException("Message body has no 'delta'")).given(node).resolveArguments(any(), any(), any(), any()); + + EntityId originatorSlow = DeviceId.fromString("7f01170d-6bba-419c-b95c-2b4c3ba32f30"); + CountDownLatch slowProcessingLatch = new CountDownLatch(1); + + List slowMsgList = IntStream.range(0, 5) + .mapToObj(x -> TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originatorSlow, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString())) + .collect(Collectors.toList()); + + assertThat(slowMsgList.size()).as("slow msgs >= rule-dispatcher pool size").isGreaterThanOrEqualTo(RULE_DISPATCHER_POOL_SIZE); + + log.debug("rule-dispatcher [{}], db-callback [{}], slowMsg [{}]", RULE_DISPATCHER_POOL_SIZE, DB_CALLBACK_POOL_SIZE, slowMsgList.size()); + + willAnswer(invocation -> { + TbMsg msg = invocation.getArgument(1); + if (slowProcessingLatch.getCount() > 0) { + log.debug("Await on slowProcessingLatch before processMsgAsync"); + try { + assertThat(slowProcessingLatch.await(30, TimeUnit.SECONDS)).as("await on slowProcessingLatch").isTrue(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + log.debug("\uD83D\uDC0C processMsgAsync with exception [{}][{}]", msg.getOriginator(), msg); + return invocation.callRealMethod(); + }).given(node).processMsgAsync(eq(ctx), argThat(slowMsgList::contains)); + + willAnswer(invocation -> { + TbMsg msg = invocation.getArgument(1); + log.debug("submit slow originator onMsg [{}][{}]", msg.getOriginator(), msg); + return invocation.callRealMethod(); + }).given(node).onMsg(eq(ctx), argThat(slowMsgList::contains)); + + // submit slow msg may block all rule engine dispatcher threads + slowMsgList.forEach(msg -> ruleEngineDispatcherExecutor.executeAsync(() -> node.onMsg(ctx, msg))); + // wait until dispatcher threads started with all slowMsg + verify(node, new Timeout(TimeUnit.SECONDS.toMillis(5), times(slowMsgList.size()))).onMsg(eq(ctx), argThat(slowMsgList::contains)); + + slowProcessingLatch.countDown(); + + verify(ctx, new Timeout(TimeUnit.SECONDS.toMillis(5), times(slowMsgList.size()))).tellFailure(any(), any()); + verify(ctx, never()).tellSuccess(any()); + + } + + @Test + public void testExp4j_concurrentBySingleOriginator_SingleMsg_manyNodesWithDifferentOutput() { + assertThat(RULE_DISPATCHER_POOL_SIZE).as("dispatcher pool size have to be > 1").isGreaterThan(1); + CountDownLatch processingLatch = new CountDownLatch(1); + List> ctxNodes = IntStream.range(0, RULE_DISPATCHER_POOL_SIZE * 2) + .mapToObj(x -> { + final TbContext ctx = mock(TbContext.class); // many rule nodes - many contexts + willReturn(dbCallbackExecutor).given(ctx).getDbCallbackExecutor(); + final String resultKey = "result" + x; + final TbMathNode node = spy(initNodeWithCustomFunction(ctx, "2a+3b", + new TbMathResult(TbMathArgumentType.MESSAGE_METADATA, resultKey, 1, false, true, null), + new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a"), + new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b"))); + willAnswer(invocation -> { + if (processingLatch.getCount() > 0) { + log.debug("Await on processingLatch before processMsgAsync"); + try { + assertThat(processingLatch.await(30, TimeUnit.SECONDS)).as("await on processingLatch").isTrue(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + log.debug("\uD83D\uDC0C processMsgAsync on node with expected resultKey [{}]", resultKey); + return invocation.callRealMethod(); + }).given(node).processMsgAsync(any(), any()); + willAnswer(invocation -> { + TbMsg msg = invocation.getArgument(1); + log.debug("submit originator onMsg [{}][{}]", msg.getOriginator(), msg); + return invocation.callRealMethod(); + }).given(node).onMsg(any(), any()); + return Triple.of(ctx, resultKey, node); + }) + .collect(Collectors.toList()); + ctxNodes.forEach(ctxNode -> ruleEngineDispatcherExecutor.executeAsync(() -> ctxNode.getRight() + .onMsg(ctxNode.getLeft(), TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, "{\"a\":2,\"b\":2}")))); + ctxNodes.forEach(ctxNode -> verify(ctxNode.getRight(), timeout(5000)).onMsg(eq(ctxNode.getLeft()), any())); + processingLatch.countDown(); + + SoftAssertions softly = new SoftAssertions(); + ctxNodes.forEach(ctxNode -> { + final TbContext ctx = ctxNode.getLeft(); + final String resultKey = ctxNode.getMiddle(); + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, timeout(5000)).tellSuccess(msgCaptor.capture()); + + TbMsg resultMsg = msgCaptor.getValue(); + assertThat(resultMsg).as("result msg non null for result key " + resultKey).isNotNull(); + log.debug("asserting result key [{}] in metadata [{}]", resultKey, resultMsg.getMetaData().getData()); + softly.assertThat(resultMsg.getMetaData().getValue(resultKey)).as("asserting result key " + resultKey) + .isEqualTo("10.0"); + }); + + softly.assertAll(); + verify(ctx, never()).tellFailure(any(), any()); + } + static class RuleDispatcherExecutor extends AbstractListeningExecutor { @Override protected int getThreadPollSize() { diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 84fdf5ddbb..e141f80321 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -99,11 +99,15 @@ redis: transport: snmp: enabled: "${SNMP_ENABLED:true}" + bind_port: "${SNMP_BIND_PORT:1620}" response_processing: # parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:20}" # to configure SNMP to work over UDP or TCP underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}" + max_request_oids: "${SNMP_MAX_REQUEST_OIDS:100}" + response: + ignore_type_cast_errors: "${SNMP_RESPONSE_IGNORE_TYPE_CAST_ERRORS:false}" sessions: inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:3000}" diff --git a/ui-ngx/src/app/core/translate/missing-translate-handler.ts b/ui-ngx/src/app/core/translate/missing-translate-handler.ts index 229efd8227..4d89e1453d 100644 --- a/ui-ngx/src/app/core/translate/missing-translate-handler.ts +++ b/ui-ngx/src/app/core/translate/missing-translate-handler.ts @@ -21,7 +21,18 @@ export class TbMissingTranslationHandler implements MissingTranslationHandler { handle(params: MissingTranslationHandlerParams) { if (params.key && !params.key.startsWith(customTranslationsPrefix)) { console.warn('Translation for \'' + params.key + '\' doesn\'t exist'); - params.translateService.set(params.key, params.key); + let translations: any; + const parts = params.key.split('.'); + for (let i=parts.length-1; i>=0; i--) { + const newTranslations = {}; + if (i === parts.length-1) { + newTranslations[parts[i]] = params.key; + } else { + newTranslations[parts[i]] = translations; + } + translations = newTranslations; + } + params.translateService.setTranslation(params.translateService.currentLang, translations, true); } } } diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html index 17e31854cc..d387abe2e0 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html @@ -17,7 +17,7 @@ -->
-

{{ title | translate }}

+

{{ (isTelemetry ? 'attribute.add-telemetry' : 'attribute.add') | translate }}

diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts index aadb96c142..65f4c0512d 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts @@ -45,7 +45,7 @@ export class AddAttributeDialogComponent extends DialogComponent, protected router: Router, @@ -62,8 +62,7 @@ export class AddAttributeDialogComponent extends DialogComponent diff --git a/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts b/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts index 539c9930db..4ce038a4ed 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts @@ -62,6 +62,11 @@ export class EditAttributeValuePanelComponent extends PageComponent implements O return originalErrorState || customErrorState; } + invalid(): boolean { + const value = this.attributeFormGroup.get('value').value; + return !Array.isArray(value) && this.attributeFormGroup.invalid; + } + cancel(): void { this.overlayRef.dispose(); } diff --git a/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts index 5889ebcdf2..f970ad80f4 100644 --- a/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts @@ -107,9 +107,15 @@ export class EdgeDownlinkTableConfig extends EntityTableConfig('createdTime', 'event.event-time', this.datePipe, '120px'), new EntityTableColumn('type', 'event.type', '25%', - entity => this.translate.instant(edgeEventTypeTranslations.get(entity.type)), entity => ({}), false), + entity => { + let key = edgeEventTypeTranslations.get(entity.type); + return key ? this.translate.instant(key) : entity.type; + }, entity => ({}), false), new EntityTableColumn('action', 'edge.event-action', '25%', - entity => this.translate.instant(edgeEventActionTypeTranslations.get(entity.action)), entity => ({}), false), + entity => { + let key = edgeEventActionTypeTranslations.get(entity.action); + return key ? this.translate.instant(key) : entity.action; + }, entity => ({}), false), new EntityTableColumn('entityId', 'edge.entity-id', '40%', (entity) => entity.entityId ? entity.entityId : '', () => ({}), false), new EntityTableColumn('status', 'event.status', '10%', diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts b/ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts index 4b41f00994..91d4cd0f05 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts +++ b/ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts @@ -65,6 +65,10 @@ export enum ImportEntityColumnType { lwm2mServerSecurityMode = 'LWM2M_SERVER_SECURITY_MODE', lwm2mServerClientPublicKeyOrId = 'LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID', lwm2mServerClientSecretKey = 'LWM2M_SERVER_CLIENT_SECRET_KEY', + snmpHost = 'SNMP_HOST', + snmpPort = 'SNMP_PORT', + snmpVersion = 'SNMP_VERSION', + snmpCommunityString = 'SNMP_COMMUNITY_STRING', isGateway = 'IS_GATEWAY', description = 'DESCRIPTION', routingKey = 'ROUTING_KEY', @@ -96,6 +100,10 @@ export const importEntityColumnTypeTranslations = new Map diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts index 9840db6efe..0314120995 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts @@ -286,6 +286,7 @@ export class GatewayFormComponent extends PageComponent implements OnInit, OnDes panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { jsonValue: config, + required: true, title: this.translate.instant('gateway.title-connectors-json', {typeName: type}) } }).afterClosed().subscribe( diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts index 0c2e8f8c55..d3002ed748 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts @@ -23,6 +23,7 @@ import { JsonObjectEditDialogComponent, JsonObjectEditDialogData } from '@shared/components/dialog/json-object-edit-dialog.component'; +import { jsonRequired } from '@shared/components/json-object-edit.component'; @Component({ selector: 'tb-gateway-service-rpc', @@ -60,7 +61,7 @@ export class GatewayServiceRPCComponent implements AfterViewInit { this.commandForm = this.fb.group({ command: [null,[Validators.required]], time: [60, [Validators.required, Validators.min(1)]], - params: ['{}', [Validators.required]], + params: [{}, [jsonRequired]], result: [null] }); } @@ -94,7 +95,8 @@ export class GatewayServiceRPCComponent implements AfterViewInit { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - jsonValue: JSON.parse(this.commandForm.get('params').value) + jsonValue: JSON.parse(this.commandForm.get('params').value), + required: true } }).afterClosed().subscribe( (res) => { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html index 24fe07d92b..40636a9d1b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html @@ -26,14 +26,14 @@ -
+
- - - {{ getErrorMessageText(key.settings,'required') }} - - - {{ getErrorMessageText(key.settings,'invalidJSON') | translate }} - - -
-
- - - {{key.settings.icon}} - - icon - - - {{key.label}} - -
-
- - {{key.label}} - - - - - {{ getErrorMessageText(key.settings, 'required') }} - - - {{ getErrorMessageText(key.settings, 'invalidDate') }} - - -
-
- - {{key.label}} - - - {{ getCustomTranslationText(option.label ? option.label : option.value) }} - - - - {{ getErrorMessageText(key.settings, 'required') }} - - -
- -
-
- - {{key.settings.icon}} - - icon - - - {{key.label}} -
- +
+ + + {{key.label}} + + + + + + {{ getErrorMessageText(key.settings, 'required') }} + + + + {{key.label}} + + + + + + {{ getErrorMessageText(key.settings,'required') }} + + + {{ getErrorMessageText(key.settings,'min') }} + + + {{ getErrorMessageText(key.settings,'max') }} + + + + {{key.label}} + + + + + + + {{ getErrorMessageText(key.settings,'required') }} + + + {{ getErrorMessageText(key.settings,'invalidJSON') | translate }} + + + + {{key.label}} + + + + + {{ getErrorMessageText(key.settings, 'required') }} + + + {{ getErrorMessageText(key.settings, 'invalidDate') }} + + + + {{key.label}} + + + {{ getCustomTranslationText(option.label ? option.label : option.value) }} + + + + + + + {{ getErrorMessageText(key.settings, 'required') }} + + + + {{key.label}} + + + + + -
-
+ + {{ getErrorMessageText(key.settings, 'required') }} + + + + + + + {{key.label}} + + + + + + {{key.label}} + +
@@ -203,7 +193,7 @@ @@ -216,3 +206,9 @@ + + {{key.settings.icon}} + + icon + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss index 90ed0d5af3..69e4905f94 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss @@ -38,34 +38,7 @@ } .tb-multiple-input-layout { - display: flex; - flex-direction: row; - align-items: start; - gap: 8px; - } - - .color-picker-input { - padding: 7px 16px 7px 12px; - margin: 0 10px 22px 0; - border-color: rgba(0, 0, 0, 0.4); - - .label-container { - display: flex; - flex-direction: row; - align-items: center; - } - - .mat-icon, img { - margin-right: 5px; - } - } - - .input-field { - padding-right: 10px; - - mat-form-field { - margin-bottom: 5px; - } + display: grid; } .mat-mdc-slide-toggle { @@ -74,17 +47,6 @@ margin-bottom: 8px; } - .date-time-input { - .mat-mdc-form-field { - width: 100%; - margin: 2px 0; - } - } - - .vertical-alignment { - flex-direction: column; - } - &--buttons-container { display: flex; flex-direction: row; @@ -113,18 +75,33 @@ :host ::ng-deep { .tb-multiple-input { - .mat-mdc-slide-toggle .mdc-form-field { - width: 100%; - & > label { - display: flex; - align-items: center; + .mat-mdc-slide-toggle, .mat-mdc-checkbox { + .mdc-form-field { width: 100%; - margin: 0; - .mat-icon { - margin-left: 8px; - margin-right: 8px; + & > label { + display: flex; + align-items: center; + width: 100%; + margin: 0; + .mat-icon { + margin-right: 8px; + } } } } + .mat-mdc-slide-toggle { + .mdc-form-field { + & > label { + .mat-icon { + margin-left: 8px; + } + } + } + } + .color-input { + .mat-mdc-form-field-icon-suffix { + padding: 0 8px 0 4px; + } + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts index f667c785c1..eb3b0c9abf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts @@ -48,6 +48,7 @@ import { } from '@shared/components/dialog/json-object-edit-dialog.component'; import { MatDialog } from '@angular/material/dialog'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; type FieldAlignment = 'row' | 'column'; @@ -55,7 +56,7 @@ type MultipleInputWidgetDataKeyType = 'server' | 'shared' | 'timeseries'; export type MultipleInputWidgetDataKeyValueType = 'string' | 'double' | 'integer' | 'JSON' | 'booleanCheckbox' | 'booleanSwitch' | 'dateTime' | 'date' | 'time' | 'select' | 'color'; -type MultipleInputWidgetDataKeyEditableType = 'editable' | 'disabled' | 'readonly'; +export type MultipleInputWidgetDataKeyEditableType = 'editable' | 'disabled' | 'readonly'; type ConvertGetValueFunction = (value: any, ctx: WidgetContext) => any; type ConvertSetValueFunction = (value: any, originValue: any, ctx: WidgetContext) => any; @@ -71,6 +72,8 @@ interface MultipleInputWidgetSettings { groupTitle: string; fieldsAlignment: FieldAlignment; fieldsInRow: number; + columnGap: number; + rowGap: number; attributesShared?: boolean; } @@ -88,6 +91,8 @@ interface MultipleInputWidgetDataKeySettings { isEditable: MultipleInputWidgetDataKeyEditableType; disabledOnDataKey: string; dataKeyHidden: boolean; + appearance: MatFormFieldAppearance; + subscriptSizing: SubscriptSizing; step?: number; minValue?: number; maxValue?: number; @@ -148,8 +153,7 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni private isSavingInProgress = false; isVerticalAlignment: boolean; - inputWidthSettings: string; - changeAlignment: boolean; + columns: number; saveButtonLabel: string; resetButtonLabel: string; @@ -228,15 +232,15 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni if (isUndefined(this.settings.fieldsInRow)) { this.settings.fieldsInRow = 2; } - // For backward compatibility - this.isVerticalAlignment = !(this.settings.fieldsAlignment === 'row'); - - if (!this.isVerticalAlignment && this.settings.fieldsInRow) { - this.inputWidthSettings = 100 / this.settings.fieldsInRow + '%'; + if (isUndefined(this.settings.columnGap)) { + this.settings.columnGap = 10; + } + if (isUndefined(this.settings.rowGap)) { + this.settings.rowGap = 5; } - this.updateWidgetDisplaying(); + this.updateColumns(); } private updateDatasources() { @@ -284,6 +288,15 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni dataKey.settings.isEditable = 'editable'; } } + + if (isUndefined(dataKey.settings.appearance)) { + dataKey.settings.appearance = 'outline'; + } + + if (isUndefined(dataKey.settings.subscriptSizing)) { + dataKey.settings.subscriptSizing = 'fixed'; + } + // For backward compatibility if (dataKey.settings.dataKeyValueType === 'select') { @@ -387,12 +400,6 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni } }); } - } else if (key.settings.dataKeyValueType === 'color') { - formControl.valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe(() => { - this.inputChanged(source, key); - }); } this.multipleInputFormGroup.addControl(key.formId, formControl); } @@ -452,9 +459,8 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni } if (key.settings.isEditable === 'editable' && key.settings.disabledOnDataKey) { - const conditions = data.filter((item) => { - return source.datasource === item.datasource && item.dataKey.name === key.settings.disabledOnDataKey; - }); + const conditions = data.filter((item) => + source.datasource === item.datasource && item.dataKey.name === key.settings.disabledOnDataKey); if (conditions && conditions.length) { if (conditions[0].data.length) { if (conditions[0].data[0][1] === 'false') { @@ -494,8 +500,17 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni return data; } - private updateWidgetDisplaying() { - this.changeAlignment = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 620); + private updateColumns() { + const changeAlignment = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 620); + if (changeAlignment) { + this.columns = 1; + } else { + if (!this.isVerticalAlignment && this.settings.fieldsInRow) { + this.columns = this.settings.fieldsInRow; + } else { + this.columns = 1; + } + } } public onDataUpdated() { @@ -504,7 +519,7 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni } private resize() { - this.updateWidgetDisplaying(); + this.updateColumns(); this.ctx.detectChanges(); } @@ -589,7 +604,8 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni } public inputChanged(source: MultipleInputWidgetSource, key: MultipleInputWidgetDataKey) { - if (!this.settings.showActionButtons && !this.isSavingInProgress && this.multipleInputFormGroup.get(key.formId).valid) { + const control = this.multipleInputFormGroup.get(key.formId); + if (!this.settings.showActionButtons && !this.isSavingInProgress && (Array.isArray(control.value) || control.valid)) { this.isSavingInProgress = true; const dataToSave: MultipleInputWidgetSource = { datasource: source.datasource, @@ -599,6 +615,13 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni } } + public colorChanged(source: MultipleInputWidgetSource, key: MultipleInputWidgetDataKey, color: string) { + this.multipleInputFormGroup.get(key.formId).setValue(color); + this.multipleInputFormGroup.get(key.formId).markAsDirty(); + this.multipleInputFormGroup.get(key.formId).markAsTouched(); + this.inputChanged(source, key); + } + public saveForm() { if (this.settings.showActionButtons) { this.save(); @@ -737,7 +760,7 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni this.multipleInputFormGroup.reset(undefined, {emitEvent: false}); this.sources.forEach((source) => { for (const key of this.visibleKeys(source)) { - this.multipleInputFormGroup.get(key.formId).patchValue(key.value, {emitEvent: false}); + this.multipleInputFormGroup.get(key.formId).patchValue(key.value); } }); this.multipleInputFormGroup.markAsPristine(); @@ -753,6 +776,7 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { jsonValue: formControl.value, + required: key.settings.required, title: key.settings.dialogTitle, saveLabel: key.settings.saveButtonLabel, cancelLabel: key.settings.cancelButtonLabel @@ -769,4 +793,16 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni } ); } + + invalid(): boolean { + for (const source of this.sources) { + for (const key of this.visibleKeys(source)) { + const control = this.multipleInputFormGroup.get(key.formId); + if (!Array.isArray(control.value) && control.invalid) { + return true; + } + } + } + return false; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html index d69a4b0713..22571baab4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html @@ -93,11 +93,36 @@ - + widgets.input-widgets.disable-on-datakey-name +
+ + widgets.input-widgets.field-appearance + + + {{ 'widgets.input-widgets.appearance-fill' | translate }} + + + {{ 'widgets.input-widgets.appearance-outline' | translate }} + + + + + widgets.input-widgets.subscript-sizing + + + {{ 'widgets.input-widgets.subscript-sizing-fixed' | translate }} + + + {{ 'widgets.input-widgets.subscript-sizing-dynamic' | translate }} + + + +
@@ -208,7 +233,9 @@
-
+
widgets.input-widgets.icon-settings {{ 'widgets.input-widgets.use-custom-icon' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts index 7eff1bdc80..785ae0cca6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts @@ -24,7 +24,10 @@ import { dataKeySelectOptionValidator } from '@home/components/widget/lib/settings/input/datakey-select-option.component'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; -import { MultipleInputWidgetDataKeyValueType } from '@home/components/widget/lib/multiple-input-widget.component'; +import { + MultipleInputWidgetDataKeyEditableType, + MultipleInputWidgetDataKeyValueType +} from '@home/components/widget/lib/multiple-input-widget.component'; @Component({ selector: 'tb-update-multiple-attributes-key-settings', @@ -52,6 +55,8 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings required: false, isEditable: 'editable', disabledOnDataKey: '', + appearance: 'outline', + subscriptSizing: 'fixed', slideToggleLabelPosition: 'after', selectOptions: [], @@ -92,6 +97,8 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings required: [settings.required, []], isEditable: [settings.isEditable, []], disabledOnDataKey: [settings.disabledOnDataKey, []], + appearance: [settings.appearance, []], + subscriptSizing: [settings.subscriptSizing, []], // Slide toggle settings @@ -146,7 +153,7 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings const dataKeyValueType: MultipleInputWidgetDataKeyValueType = this.updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value; const required: boolean = this.updateMultipleAttributesKeySettingsForm.get('required').value; - const isEditable: string = this.updateMultipleAttributesKeySettingsForm.get('isEditable').value; + const isEditable: MultipleInputWidgetDataKeyEditableType = this.updateMultipleAttributesKeySettingsForm.get('isEditable').value; const useCustomIcon: boolean = this.updateMultipleAttributesKeySettingsForm.get('useCustomIcon').value; const useGetValueFunction: boolean = this.updateMultipleAttributesKeySettingsForm.get('useGetValueFunction').value; const useSetValueFunction: boolean = this.updateMultipleAttributesKeySettingsForm.get('useSetValueFunction').value; @@ -163,10 +170,15 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings this.updateMultipleAttributesKeySettingsForm.get('useGetValueFunction').enable({emitEvent: false}); this.updateMultipleAttributesKeySettingsForm.get('useSetValueFunction').enable({emitEvent: false}); - if (isEditable !== 'disabled') { + if (isEditable === 'editable') { this.updateMultipleAttributesKeySettingsForm.get('disabledOnDataKey').enable({emitEvent: false}); } + if (!['booleanSwitch', 'booleanCheckbox'].includes(dataKeyValueType)) { + this.updateMultipleAttributesKeySettingsForm.get('appearance').enable({emitEvent: false}); + this.updateMultipleAttributesKeySettingsForm.get('subscriptSizing').enable({emitEvent: false}); + } + if (dataKeyValueType === 'booleanSwitch') { this.updateMultipleAttributesKeySettingsForm.get('slideToggleLabelPosition').enable({emitEvent: false}); } else if (dataKeyValueType === 'select') { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html index 9c250e5968..c1536040dd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html @@ -72,7 +72,7 @@
- widgets.input-widgets.fields-alignment + widgets.input-widgets.layout
widgets.input-widgets.fields-alignment @@ -90,5 +90,15 @@
+
+ + widgets.input-widgets.row-gap + + + + widgets.input-widgets.column-gap + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts index c1cf455f9c..9b55e131a8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts @@ -49,7 +49,9 @@ export class UpdateMultipleAttributesWidgetSettingsComponent extends WidgetSetti showGroupTitle: false, groupTitle: '', fieldsAlignment: 'row', - fieldsInRow: 2 + fieldsInRow: 2, + rowGap: 5, + columnGap: 10 }; } @@ -77,6 +79,11 @@ export class UpdateMultipleAttributesWidgetSettingsComponent extends WidgetSetti fieldsAlignment: [settings.fieldsAlignment, []], fieldsInRow: [settings.fieldsInRow, [Validators.min(1)]], + + // Layout gap + + rowGap: [settings.rowGap, [Validators.min(0)]], + columnGap: [settings.columnGap, [Validators.min(0)]] }); } @@ -105,13 +112,16 @@ export class UpdateMultipleAttributesWidgetSettingsComponent extends WidgetSetti } if (fieldsAlignment === 'row') { this.updateMultipleAttributesWidgetSettingsForm.get('fieldsInRow').enable(); + this.updateMultipleAttributesWidgetSettingsForm.get('columnGap').enable(); } else { this.updateMultipleAttributesWidgetSettingsForm.get('fieldsInRow').disable(); + this.updateMultipleAttributesWidgetSettingsForm.get('columnGap').disable(); } this.updateMultipleAttributesWidgetSettingsForm.get('updateAllValues').updateValueAndValidity({emitEvent}); this.updateMultipleAttributesWidgetSettingsForm.get('saveButtonLabel').updateValueAndValidity({emitEvent}); this.updateMultipleAttributesWidgetSettingsForm.get('resetButtonLabel').updateValueAndValidity({emitEvent}); this.updateMultipleAttributesWidgetSettingsForm.get('groupTitle').updateValueAndValidity({emitEvent}); this.updateMultipleAttributesWidgetSettingsForm.get('fieldsInRow').updateValueAndValidity({emitEvent}); + this.updateMultipleAttributesWidgetSettingsForm.get('columnGap').updateValueAndValidity({emitEvent}); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 732d6e3119..cdd1d8e2da 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -89,7 +89,6 @@ diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts index 99d8697dc5..19e4ed03ea 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts @@ -98,7 +98,6 @@ export class ResourcesLibraryComponent extends EntityComponent impleme } updateForm(entity: Resource) { - this.entity.name = entity.title; if (this.isEdit) { this.entityForm.get('resourceType').disable({emitEvent: false}); if (entity.resourceType !== ResourceType.JS_MODULE) { diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.models.ts b/ui-ngx/src/app/modules/home/pages/home-pages.models.ts index cd6190bb00..3979730f0c 100644 --- a/ui-ngx/src/app/modules/home/pages/home-pages.models.ts +++ b/ui-ngx/src/app/modules/home/pages/home-pages.models.ts @@ -16,9 +16,18 @@ import { BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; +import { EntityType } from '@shared/models/entity-type.models'; +import { ResourceInfo } from '@shared/models/resource.models'; +import { OtaPackage } from '@shared/models/ota-package.models'; export const entityDetailsPageBreadcrumbLabelFunction: BreadCrumbLabelFunction = ((route, translate, component) => { - return component.entity?.name; + switch (component.entitiesTableConfig.entityType) { + case EntityType.TB_RESOURCE: + case EntityType.OTA_PACKAGE: + return (component.entity as ResourceInfo | OtaPackage)?.title; + default: + return component.entity?.name; + } }); diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html index 97f2214f2e..6d059fddec 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html @@ -22,7 +22,7 @@ class="tb-rule-node-configuration-json" formControlName="configuration" [label]="'rulenode.configuration' | translate" - [required]="required" + [jsonRequired]="required" [fillHeight]="true"> 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 68ec5854f2..cd40dde783 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.html +++ b/ui-ngx/src/app/shared/components/color-input.component.html @@ -40,7 +40,7 @@ class="tb-box-button" [disabled]="disabled" #matButton - (click)="openColorPickerPopup($event, matButton)"> + (click)="openColorPickerPopup($event, matButton._elementRef)">
diff --git a/ui-ngx/src/app/shared/components/color-input.component.ts b/ui-ngx/src/app/shared/components/color-input.component.ts index 834fcb8176..6d7c7cf8b9 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.ts +++ b/ui-ngx/src/app/shared/components/color-input.component.ts @@ -14,7 +14,16 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + ElementRef, + forwardRef, + Input, + OnInit, + Renderer2, + ViewContainerRef +} from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -84,6 +93,10 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro @Input() disabled: boolean; + @Input() + @coerceBoolean() + readonly = false; + private modelValue: string; private propagateChange = null; @@ -151,43 +164,47 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro showColorPicker($event: MouseEvent) { $event.stopPropagation(); - this.dialogs.colorPicker(this.colorFormGroup.get('color').value, - this.colorClearButton).subscribe( - (result) => { - if (!result?.canceled) { - this.colorFormGroup.patchValue( - {color: result?.color}, {emitEvent: true} - ); - this.cd.markForCheck(); - } - } - ); + if (!this.disabled && !this.readonly) { + this.dialogs.colorPicker(this.colorFormGroup.get('color').value, + this.colorClearButton).subscribe( + (result) => { + if (!result?.canceled) { + this.colorFormGroup.patchValue( + {color: result?.color}, {emitEvent: true} + ); + this.cd.markForCheck(); + } + } + ); + } } - openColorPickerPopup($event: Event, matButton: MatButton) { + openColorPickerPopup($event: Event, element?: ElementRef) { if ($event) { $event.stopPropagation(); } - const trigger = matButton._elementRef.nativeElement; - if (this.popoverService.hasPopover(trigger)) { - this.popoverService.hidePopover(trigger); - } else { - const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, ColorPickerPanelComponent, 'left', true, null, - { - color: this.colorFormGroup.get('color').value, - colorClearButton: this.colorClearButton - }, - {}, - {}, {}, true); - colorPickerPopover.tbComponentRef.instance.popover = colorPickerPopover; - colorPickerPopover.tbComponentRef.instance.colorSelected.subscribe((color) => { - colorPickerPopover.hide(); - this.colorFormGroup.patchValue( - {color}, {emitEvent: true} - ); - this.cd.markForCheck(); - }); + if (!this.disabled && !this.readonly) { + const trigger = element ? element.nativeElement : $event.target; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, ColorPickerPanelComponent, 'left', true, null, + { + color: this.colorFormGroup.get('color').value, + colorClearButton: this.colorClearButton + }, + {}, + {}, {}, true); + colorPickerPopover.tbComponentRef.instance.popover = colorPickerPopover; + colorPickerPopover.tbComponentRef.instance.colorSelected.subscribe((color) => { + colorPickerPopover.hide(); + this.colorFormGroup.patchValue( + {color}, {emitEvent: true} + ); + this.cd.markForCheck(); + }); + } } } diff --git a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html index 141979d8ff..3b216bda71 100644 --- a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html @@ -33,8 +33,7 @@
diff --git a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts index e55431a327..4aeed2a360 100644 --- a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts @@ -26,6 +26,7 @@ import { isNotEmptyStr } from '@core/utils'; export interface JsonObjectEditDialogData { jsonValue: object; + required?: boolean; title?: string; saveLabel?: string; cancelLabel?: string; @@ -43,6 +44,8 @@ export class JsonObjectEditDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: JsonObjectEditDialogData, diff --git a/ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts b/ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts index 8da5fb1396..c70de1ab48 100644 --- a/ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts +++ b/ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts @@ -17,19 +17,23 @@ import { Directive, ElementRef, forwardRef, HostListener, Renderer2, SkipSelf } from '@angular/core'; import { ControlValueAccessor, - UntypedFormControl, FormGroupDirective, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgForm, + UntypedFormControl, ValidationErrors, Validator } from '@angular/forms'; import { ErrorStateMatcher } from '@angular/material/core'; -import { isObject } from "@core/utils"; +import { isObject } from '@core/utils'; @Directive({ selector: '[tb-json-to-string]', + // eslint-disable-next-line @angular-eslint/no-host-metadata-property + host: { + '(blur)': 'onTouched()' + }, providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TbJsonToStringDirective), @@ -48,18 +52,26 @@ import { isObject } from "@core/utils"; export class TbJsonToStringDirective implements ControlValueAccessor, Validator, ErrorStateMatcher { private propagateChange = null; + public onTouched = () => {}; private parseError: boolean; private data: any; @HostListener('input', ['$event.target.value']) input(newValue: any): void { try { - this.data = JSON.parse(newValue); - if (isObject(this.data)) { - this.parseError = false; + if (newValue) { + this.data = JSON.parse(newValue); + if (isObject(this.data)) { + this.parseError = false; + } else { + this.data = null; + this.parseError = true; + } } else { - this.parseError = true; + this.data = null; + this.parseError = false; } } catch (e) { + this.data = null; this.parseError = true; } @@ -73,9 +85,7 @@ export class TbJsonToStringDirective implements ControlValueAccessor, Validator, } isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { - const originalErrorState = this.errorStateMatcher.isErrorState(control, form); - const customErrorState = !!(control && control.invalid && this.parseError); - return originalErrorState || customErrorState; + return !!(control && control.invalid && !Array.isArray(control.value) && control.touched); } validate(c: UntypedFormControl): ValidationErrors { @@ -87,11 +97,9 @@ export class TbJsonToStringDirective implements ControlValueAccessor, Validator, } writeValue(obj: any): void { - if (obj) { - this.data = obj; - this.parseError = false; - this.render.setProperty(this.element.nativeElement, 'value', JSON.stringify(obj)); - } + this.data = obj; + this.parseError = false; + this.render.setProperty(this.element.nativeElement, 'value', obj ? JSON.stringify(obj) : ''); } registerOnChange(fn: any): void { @@ -99,5 +107,6 @@ export class TbJsonToStringDirective implements ControlValueAccessor, Validator, } registerOnTouched(fn: any): void { + this.onTouched = fn; } } diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.html b/ui-ngx/src/app/shared/components/json-object-edit.component.html index c0024da9dd..3ed828e6cf 100644 --- a/ui-ngx/src/app/shared/components/json-object-edit.component.html +++ b/ui-ngx/src/app/shared/components/json-object-edit.component.html @@ -20,7 +20,7 @@ [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.ts b/ui-ngx/src/app/shared/components/json-object-edit.component.ts index f7e3116b0f..f89430f98b 100644 --- a/ui-ngx/src/app/shared/components/json-object-edit.component.ts +++ b/ui-ngx/src/app/shared/components/json-object-edit.component.ts @@ -24,7 +24,14 @@ import { OnInit, ViewChild } from '@angular/core'; -import { ControlValueAccessor, UntypedFormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; +import { + ControlValueAccessor, + UntypedFormControl, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + AbstractControl, ValidationErrors +} from '@angular/forms'; import { Ace } from 'ace-builds'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; @@ -34,6 +41,9 @@ import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; import { guid, isDefinedAndNotNull, isObject, isUndefined } from '@core/utils'; import { ResizeObserver } from '@juggle/resize-observer'; import { getAce } from '@shared/models/ace/ace.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +export const jsonRequired = (control: AbstractControl): ValidationErrors | null => !control.value ? {required: true} : null; @Component({ selector: 'tb-json-object-edit', @@ -73,27 +83,13 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va @Input() sort: (key: string, value: any) => any; - private requiredValue: boolean; - - get required(): boolean { - return this.requiredValue; - } - + @coerceBoolean() @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } - - private readonlyValue: boolean; - - get readonly(): boolean { - return this.readonlyValue; - } + jsonRequired: boolean; + @coerceBoolean() @Input() - set readonly(value: boolean) { - this.readonlyValue = coerceBooleanProperty(value); - } + readonly: boolean; fullscreen = false; @@ -245,12 +241,10 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va try { if (isDefinedAndNotNull(this.modelValue)) { this.contentValue = JSON.stringify(this.modelValue, isUndefined(this.sort) ? undefined : - (key, objectValue) => { - return this.sort(key, objectValue); - }, 2); + (key, objectValue) => this.sort(key, objectValue), 2); this.objectValid = true; } else { - this.objectValid = !this.required; + this.objectValid = !this.jsonRequired; this.validationError = 'Json object is required.'; } } catch (e) { @@ -288,8 +282,8 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va this.validationError = errorInfo; } } else { - this.objectValid = !this.required; - this.validationError = this.required ? 'Json object is required.' : ''; + this.objectValid = !this.jsonRequired; + this.validationError = this.jsonRequired ? 'Json object is required.' : ''; } this.modelValue = data; this.propagateChange(data); diff --git a/ui-ngx/src/app/shared/components/value-input.component.html b/ui-ngx/src/app/shared/components/value-input.component.html index 57d6954699..9f7f9d0ffc 100644 --- a/ui-ngx/src/app/shared/components/value-input.component.html +++ b/ui-ngx/src/app/shared/components/value-input.component.html @@ -67,7 +67,7 @@ - + {{ (requiredText ? requiredText : 'value.json-value-required') | translate }} diff --git a/ui-ngx/src/app/shared/components/value-input.component.ts b/ui-ngx/src/app/shared/components/value-input.component.ts index 4ba7abf157..6d57b1ddd2 100644 --- a/ui-ngx/src/app/shared/components/value-input.component.ts +++ b/ui-ngx/src/app/shared/components/value-input.component.ts @@ -73,7 +73,8 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - jsonValue: this.modelValue + jsonValue: this.modelValue, + required: true } }).afterClosed().subscribe( (res) => { @@ -115,7 +116,8 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor { } updateView() { - if (this.inputForm.valid || this.valueType === ValueType.BOOLEAN) { + if (this.inputForm.valid || this.valueType === ValueType.BOOLEAN || + (this.valueType === ValueType.JSON && Array.isArray(this.modelValue))) { this.propagateChange(this.modelValue); } else { this.propagateChange(null); diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index b2380d770e..260fbee33a 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -298,14 +298,16 @@ export enum SnmpSpecType { TELEMETRY_QUERYING = 'TELEMETRY_QUERYING', CLIENT_ATTRIBUTES_QUERYING = 'CLIENT_ATTRIBUTES_QUERYING', SHARED_ATTRIBUTES_SETTING = 'SHARED_ATTRIBUTES_SETTING', - TO_DEVICE_RPC_REQUEST = 'TO_DEVICE_RPC_REQUEST' + TO_DEVICE_RPC_REQUEST = 'TO_DEVICE_RPC_REQUEST', + TO_SERVER_RPC_REQUEST = 'TO_SERVER_RPC_REQUEST' } export const SnmpSpecTypeTranslationMap = new Map([ - [SnmpSpecType.TELEMETRY_QUERYING, ' Telemetry'], - [SnmpSpecType.CLIENT_ATTRIBUTES_QUERYING, 'Client attributes'], - [SnmpSpecType.SHARED_ATTRIBUTES_SETTING, 'Shared attributes'], - [SnmpSpecType.TO_DEVICE_RPC_REQUEST, 'RPC request'] + [SnmpSpecType.TELEMETRY_QUERYING, ' Telemetry (SNMP GET)'], + [SnmpSpecType.CLIENT_ATTRIBUTES_QUERYING, 'Client attributes (SNMP GET)'], + [SnmpSpecType.SHARED_ATTRIBUTES_SETTING, 'Shared attributes (SNMP SET)'], + [SnmpSpecType.TO_DEVICE_RPC_REQUEST, 'To-device RPC request (SNMP GET/SET)'], + [SnmpSpecType.TO_SERVER_RPC_REQUEST, 'From-device RPC request (SNMP TRAP)'] ]); export interface SnmpCommunicationConfig { diff --git a/ui-ngx/src/app/shared/models/edge.models.ts b/ui-ngx/src/app/shared/models/edge.models.ts index 372a910402..1acfa2ca28 100644 --- a/ui-ngx/src/app/shared/models/edge.models.ts +++ b/ui-ngx/src/app/shared/models/edge.models.ts @@ -59,9 +59,12 @@ export enum EdgeEventType { CUSTOMER = 'CUSTOMER', RELATION = 'RELATION', TENANT = 'TENANT', + TENANT_PROFILE = 'TENANT_PROFILE', WIDGETS_BUNDLE = 'WIDGETS_BUNDLE', WIDGET_TYPE = 'WIDGET_TYPE', - ADMIN_SETTINGS = 'ADMIN_SETTINGS' + ADMIN_SETTINGS = 'ADMIN_SETTINGS', + OTA_PACKAGE = 'OTA_PACKAGE', + QUEUE = 'QUEUE' } export enum EdgeEventActionType { @@ -80,6 +83,8 @@ export enum EdgeEventActionType { RPC_CALL = 'RPC_CALL', ALARM_ACK = 'ALARM_ACK', ALARM_CLEAR = 'ALARM_CLEAR', + ALARM_ASSIGNED = 'ALARM_ASSIGNED', + ALARM_UNASSIGNED = 'ALARM_UNASSIGNED', ASSIGNED_TO_EDGE = 'ASSIGNED_TO_EDGE', UNASSIGNED_FROM_EDGE = 'UNASSIGNED_FROM_EDGE', CREDENTIALS_REQUEST = 'CREDENTIALS_REQUEST', @@ -107,9 +112,12 @@ export const edgeEventTypeTranslations = new Map( [EdgeEventType.CUSTOMER, 'edge-event.type-customer'], [EdgeEventType.RELATION, 'edge-event.type-relation'], [EdgeEventType.TENANT, 'edge-event.type-tenant'], + [EdgeEventType.TENANT_PROFILE, 'edge-event.type-tenant-profile'], [EdgeEventType.WIDGETS_BUNDLE, 'edge-event.type-widgets-bundle'], [EdgeEventType.WIDGET_TYPE, 'edge-event.type-widgets-type'], - [EdgeEventType.ADMIN_SETTINGS, 'edge-event.type-admin-settings'] + [EdgeEventType.ADMIN_SETTINGS, 'edge-event.type-admin-settings'], + [EdgeEventType.OTA_PACKAGE, 'edge-event.type-ota-package'], + [EdgeEventType.QUEUE, 'edge-event.type-queue'] ] ); @@ -130,6 +138,8 @@ export const edgeEventActionTypeTranslations = new Map .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}\n\n.tb-widget-container > .tb-widget .flot-base {\n opacity: 0.48;\n}\n", "pageSize": 1024, - "noDataDisplayMessage": "", - "legendConfig": { - "direction": "column", - "position": "bottom", - "sortDataKeys": false, - "showMin": false, - "showMax": false, - "showAvg": true, - "showTotal": false, - "showLatest": false - } + "noDataDisplayMessage": "" }, "row": 0, "col": 0, - "id": "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e" + "id": "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e", + "typeFullFqn": "system.charts.timeseries_bars_flot" }, "4b5e47ed-c197-a937-d727-041ba8decec2": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -307,16 +291,11 @@ }, "row": 0, "col": 0, - "id": "4b5e47ed-c197-a937-d727-041ba8decec2" + "id": "4b5e47ed-c197-a937-d727-041ba8decec2", + "typeFullFqn": "system.cards.markdown_card" }, "eace9148-b02a-48fe-1a95-acd9928aa8c5": { - "isSystemType": true, - "bundleAlias": "charts", - "typeAlias": "basic_timeseries", "type": "timeseries", - "title": "New widget", - "image": null, - "description": null, "sizeX": 8, "sizeY": 5, "config": { @@ -501,7 +480,18 @@ "showLabels": true }, "customLegendEnabled": false, - "dataKeysListForLabels": [] + "dataKeysListForLabels": [], + "showLegend": true, + "legendConfig": { + "direction": "row", + "position": "top", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": false, + "showLatest": false + } }, "title": "System Info Chart", "dropShadow": false, @@ -518,31 +508,15 @@ "pageSize": 1024, "units": "%", "noDataDisplayMessage": "", - "showLegend": true, - "legendConfig": { - "direction": "row", - "position": "top", - "sortDataKeys": false, - "showMin": false, - "showMax": false, - "showAvg": false, - "showTotal": false, - "showLatest": false - }, "displayTimewindow": true }, "row": 0, "col": 0, - "id": "eace9148-b02a-48fe-1a95-acd9928aa8c5" + "id": "eace9148-b02a-48fe-1a95-acd9928aa8c5", + "typeFullFqn": "system.charts.basic_timeseries" }, "8acbf5df-f9fc-114d-216f-86f081aa4779": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -601,16 +575,11 @@ }, "row": 0, "col": 0, - "id": "8acbf5df-f9fc-114d-216f-86f081aa4779" + "id": "8acbf5df-f9fc-114d-216f-86f081aa4779", + "typeFullFqn": "system.cards.markdown_card" }, "d2784f6c-0518-fd95-1d28-b21f70bdcb10": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -668,16 +637,11 @@ }, "row": 0, "col": 0, - "id": "d2784f6c-0518-fd95-1d28-b21f70bdcb10" + "id": "d2784f6c-0518-fd95-1d28-b21f70bdcb10", + "typeFullFqn": "system.cards.markdown_card" }, "66dcf1a2-9d83-6873-c693-d6a5d989b3f8": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -735,16 +699,11 @@ }, "row": 0, "col": 0, - "id": "66dcf1a2-9d83-6873-c693-d6a5d989b3f8" + "id": "66dcf1a2-9d83-6873-c693-d6a5d989b3f8", + "typeFullFqn": "system.cards.markdown_card" }, "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": { - "isSystemType": true, - "bundleAlias": "home_page_widgets", - "typeAlias": "documentation_links", "type": "static", - "title": "New widget", - "image": null, - "description": null, "sizeX": 7.5, "sizeY": 3, "config": { @@ -772,16 +731,11 @@ }, "row": 0, "col": 0, - "id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4" + "id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4", + "typeFullFqn": "system.home_page_widgets.documentation_links" }, "a23185ad-dc46-806c-0e50-5b21fb080ace": { - "isSystemType": true, - "bundleAlias": "home_page_widgets", - "typeAlias": "getting_started", "type": "static", - "title": "New widget", - "image": null, - "description": null, "sizeX": 7.5, "sizeY": 6.5, "config": { @@ -809,16 +763,11 @@ }, "row": 0, "col": 0, - "id": "a23185ad-dc46-806c-0e50-5b21fb080ace" + "id": "a23185ad-dc46-806c-0e50-5b21fb080ace", + "typeFullFqn": "system.home_page_widgets.getting_started" }, "b35fa1b0-7f95-0b23-fade-d066653a40e1": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -930,16 +879,11 @@ }, "row": 0, "col": 0, - "id": "b35fa1b0-7f95-0b23-fade-d066653a40e1" + "id": "b35fa1b0-7f95-0b23-fade-d066653a40e1", + "typeFullFqn": "system.cards.markdown_card" }, "5eabde4c-095f-defc-dd09-6c0afb9b9f7b": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -1063,16 +1007,11 @@ }, "row": 0, "col": 0, - "id": "5eabde4c-095f-defc-dd09-6c0afb9b9f7b" + "id": "5eabde4c-095f-defc-dd09-6c0afb9b9f7b", + "typeFullFqn": "system.cards.markdown_card" }, "451e2588-80b8-cd08-2199-1263ed4dc12c": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -1196,16 +1135,11 @@ }, "row": 0, "col": 0, - "id": "451e2588-80b8-cd08-2199-1263ed4dc12c" + "id": "451e2588-80b8-cd08-2199-1263ed4dc12c", + "typeFullFqn": "system.cards.markdown_card" }, "01b94c17-84eb-4e49-6a92-c3f108ecff8e": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -1417,16 +1351,11 @@ }, "row": 0, "col": 0, - "id": "01b94c17-84eb-4e49-6a92-c3f108ecff8e" + "id": "01b94c17-84eb-4e49-6a92-c3f108ecff8e", + "typeFullFqn": "system.cards.markdown_card" }, "f0479b00-ed47-0a30-0c36-bde7847aae00": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -1488,16 +1417,11 @@ }, "row": 0, "col": 0, - "id": "f0479b00-ed47-0a30-0c36-bde7847aae00" + "id": "f0479b00-ed47-0a30-0c36-bde7847aae00", + "typeFullFqn": "system.cards.markdown_card" }, "57ed0ad9-8fcf-4454-1fa7-d39e22f84570": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -1579,16 +1503,11 @@ }, "row": 0, "col": 0, - "id": "57ed0ad9-8fcf-4454-1fa7-d39e22f84570" + "id": "57ed0ad9-8fcf-4454-1fa7-d39e22f84570", + "typeFullFqn": "system.cards.markdown_card" }, "0464b6bc-7863-ba75-4b84-64b8acae2af9": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -1670,16 +1589,11 @@ }, "row": 0, "col": 0, - "id": "0464b6bc-7863-ba75-4b84-64b8acae2af9" + "id": "0464b6bc-7863-ba75-4b84-64b8acae2af9", + "typeFullFqn": "system.cards.markdown_card" }, "4c043bba-6ac1-dd23-0796-fe6ae4c3c0a3": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -1761,16 +1675,11 @@ }, "row": 0, "col": 0, - "id": "4c043bba-6ac1-dd23-0796-fe6ae4c3c0a3" + "id": "4c043bba-6ac1-dd23-0796-fe6ae4c3c0a3", + "typeFullFqn": "system.cards.markdown_card" }, "8b8d5d4f-2c5f-c768-d4fb-68d6e0b716f4": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -1852,16 +1761,11 @@ }, "row": 0, "col": 0, - "id": "8b8d5d4f-2c5f-c768-d4fb-68d6e0b716f4" + "id": "8b8d5d4f-2c5f-c768-d4fb-68d6e0b716f4", + "typeFullFqn": "system.cards.markdown_card" }, "d58bfdd4-18b0-a70c-5d5f-73d12c2ea468": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -1943,16 +1847,11 @@ }, "row": 0, "col": 0, - "id": "d58bfdd4-18b0-a70c-5d5f-73d12c2ea468" + "id": "d58bfdd4-18b0-a70c-5d5f-73d12c2ea468", + "typeFullFqn": "system.cards.markdown_card" }, "a3d83738-a4f2-ea92-17d7-1a3c52aa1770": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -2034,16 +1933,11 @@ }, "row": 0, "col": 0, - "id": "a3d83738-a4f2-ea92-17d7-1a3c52aa1770" + "id": "a3d83738-a4f2-ea92-17d7-1a3c52aa1770", + "typeFullFqn": "system.cards.markdown_card" }, "ff4f04a2-f739-2f3e-17dc-9ede9f2df75d": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -2235,16 +2129,11 @@ }, "row": 0, "col": 0, - "id": "ff4f04a2-f739-2f3e-17dc-9ede9f2df75d" + "id": "ff4f04a2-f739-2f3e-17dc-9ede9f2df75d", + "typeFullFqn": "system.cards.markdown_card" }, "163025d8-3ca4-dd1e-c17b-e40d8e03e8a8": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -2306,7 +2195,8 @@ }, "row": 0, "col": 0, - "id": "163025d8-3ca4-dd1e-c17b-e40d8e03e8a8" + "id": "163025d8-3ca4-dd1e-c17b-e40d8e03e8a8", + "typeFullFqn": "system.cards.markdown_card" } }, "states": { @@ -2721,4 +2611,4 @@ }, "externalId": null, "name": "System Administrator Home Page" -} +} \ No newline at end of file diff --git a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json index 6f9215b4f9..a2edebb7c4 100644 --- a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json +++ b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json @@ -7,13 +7,7 @@ "description": "", "widgets": { "d70cc256-4c7b-ee06-9905-b8c5e546605f": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -72,16 +66,11 @@ }, "row": 0, "col": 0, - "id": "d70cc256-4c7b-ee06-9905-b8c5e546605f" + "id": "d70cc256-4c7b-ee06-9905-b8c5e546605f", + "typeFullFqn": "system.cards.markdown_card" }, "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e": { - "isSystemType": true, - "bundleAlias": "charts", - "typeAlias": "timeseries_bars_flot", "type": "timeseries", - "title": "New widget", - "image": null, - "description": null, "sizeX": 8, "sizeY": 5, "config": { @@ -179,7 +168,18 @@ "showLabels": true }, "customLegendEnabled": false, - "dataKeysListForLabels": [] + "dataKeysListForLabels": [], + "showLegend": false, + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": true, + "showTotal": false, + "showLatest": false + } }, "title": "Transport messages", "dropShadow": false, @@ -192,37 +192,21 @@ "padding": "0" }, "useDashboardTimewindow": false, - "showLegend": false, "actions": {}, "displayTimewindow": true, "showTitleIcon": false, "titleTooltip": "", "widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}\n\n.tb-widget-container > .tb-widget .flot-base {\n opacity: 0.48;\n}\n", "pageSize": 1024, - "noDataDisplayMessage": "", - "legendConfig": { - "direction": "column", - "position": "bottom", - "sortDataKeys": false, - "showMin": false, - "showMax": false, - "showAvg": true, - "showTotal": false, - "showLatest": false - } + "noDataDisplayMessage": "" }, "row": 0, "col": 0, - "id": "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e" + "id": "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e", + "typeFullFqn": "system.charts.timeseries_bars_flot" }, "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": { - "isSystemType": true, - "bundleAlias": "home_page_widgets", - "typeAlias": "documentation_links", "type": "static", - "title": "New widget", - "image": null, - "description": null, "sizeX": 7.5, "sizeY": 3, "config": { @@ -250,16 +234,11 @@ }, "row": 0, "col": 0, - "id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4" + "id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4", + "typeFullFqn": "system.home_page_widgets.documentation_links" }, "a23185ad-dc46-806c-0e50-5b21fb080ace": { - "isSystemType": true, - "bundleAlias": "home_page_widgets", - "typeAlias": "getting_started", "type": "static", - "title": "New widget", - "image": null, - "description": null, "sizeX": 7.5, "sizeY": 6.5, "config": { @@ -287,16 +266,11 @@ }, "row": 0, "col": 0, - "id": "a23185ad-dc46-806c-0e50-5b21fb080ace" + "id": "a23185ad-dc46-806c-0e50-5b21fb080ace", + "typeFullFqn": "system.home_page_widgets.getting_started" }, "d26e5cd7-75ef-d475-00c7-1a2d1114efe8": { - "isSystemType": true, - "bundleAlias": "charts", - "typeAlias": "basic_timeseries", "type": "timeseries", - "title": "New widget", - "image": null, - "description": null, "sizeX": 8, "sizeY": 5, "config": { @@ -412,7 +386,18 @@ "showLabels": true }, "customLegendEnabled": false, - "dataKeysListForLabels": [] + "dataKeysListForLabels": [], + "showLegend": false, + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": true, + "showTotal": false, + "showLatest": false + } }, "title": "Devices activity", "dropShadow": false, @@ -430,31 +415,15 @@ }, "widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}\n", "pageSize": 1024, - "noDataDisplayMessage": "", - "showLegend": false, - "legendConfig": { - "direction": "column", - "position": "bottom", - "sortDataKeys": false, - "showMin": false, - "showMax": false, - "showAvg": true, - "showTotal": false, - "showLatest": false - } + "noDataDisplayMessage": "" }, "row": 0, "col": 0, - "id": "d26e5cd7-75ef-d475-00c7-1a2d1114efe8" + "id": "d26e5cd7-75ef-d475-00c7-1a2d1114efe8", + "typeFullFqn": "system.charts.basic_timeseries" }, "ebbd0a6e-8a47-e770-5086-7f4974250f2d": { - "isSystemType": true, - "bundleAlias": "home_page_widgets", - "typeAlias": "usage_info", "type": "static", - "title": "New widget", - "image": null, - "description": null, "sizeX": 7.5, "sizeY": 6.5, "config": { @@ -475,16 +444,11 @@ }, "row": 0, "col": 0, - "id": "ebbd0a6e-8a47-e770-5086-7f4974250f2d" + "id": "ebbd0a6e-8a47-e770-5086-7f4974250f2d", + "typeFullFqn": "system.home_page_widgets.usage_info" }, "9e3ef045-d8bc-1640-a3f4-2dd10b19d50e": { - "isSystemType": true, - "bundleAlias": "home_page_widgets", - "typeAlias": "quick_links", "type": "static", - "title": "New widget", - "image": null, - "description": null, "sizeX": 7.5, "sizeY": 3, "config": { @@ -507,16 +471,11 @@ }, "row": 0, "col": 0, - "id": "9e3ef045-d8bc-1640-a3f4-2dd10b19d50e" + "id": "9e3ef045-d8bc-1640-a3f4-2dd10b19d50e", + "typeFullFqn": "system.home_page_widgets.quick_links" }, "6d6e2b1d-6ce7-4678-3745-c6b0897b2674": { - "isSystemType": true, - "bundleAlias": "home_page_widgets", - "typeAlias": "dashboards", "type": "static", - "title": "New widget", - "image": null, - "description": null, "sizeX": 7.5, "sizeY": 6.5, "config": { @@ -537,16 +496,11 @@ }, "row": 0, "col": 0, - "id": "6d6e2b1d-6ce7-4678-3745-c6b0897b2674" + "id": "6d6e2b1d-6ce7-4678-3745-c6b0897b2674", + "typeFullFqn": "system.home_page_widgets.dashboards" }, "35d3ac3b-f1af-dc3d-a1d1-11f351d16fc6": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -672,16 +626,11 @@ }, "row": 0, "col": 0, - "id": "35d3ac3b-f1af-dc3d-a1d1-11f351d16fc6" + "id": "35d3ac3b-f1af-dc3d-a1d1-11f351d16fc6", + "typeFullFqn": "system.cards.markdown_card" }, "7ac20b6a-dc40-b18e-9f5f-bca20bc693bb": { - "isSystemType": true, - "bundleAlias": "cards", - "typeAlias": "markdown_card", "type": "latest", - "title": "New widget", - "image": null, - "description": null, "sizeX": 5, "sizeY": 3.5, "config": { @@ -826,7 +775,8 @@ }, "row": 0, "col": 0, - "id": "7ac20b6a-dc40-b18e-9f5f-bca20bc693bb" + "id": "7ac20b6a-dc40-b18e-9f5f-bca20bc693bb", + "typeFullFqn": "system.cards.markdown_card" } }, "states": { @@ -1101,4 +1051,4 @@ }, "externalId": null, "name": "Tenant Administrator Home Page" -} +} \ No newline at end of file diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 987610004d..6feb6580e7 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -722,6 +722,8 @@ "key-required": "Attribute key is required.", "value": "Value", "value-required": "Attribute value is required.", + "telemetry-key-required": "Telemetry key is required", + "telemetry-value-required": "Telemetry value is required", "delete-attributes-title": "Are you sure you want to delete { count, plural, =1 {1 attribute} other {# attributes} }?", "delete-attributes-text": "Be careful, after the confirmation all selected attributes will be removed.", "delete-attributes": "Delete attributes", @@ -2007,11 +2009,15 @@ "type-rule-chain-metadata": "Rule Chain Metadata", "type-edge": "Edge", "type-user": "User", + "type-tenant": "Tenant", + "type-tenant-profile": "Tenant Profile", "type-customer": "Customer", "type-relation": "Relation", "type-widgets-bundle": "Widgets Bundle", "type-widgets-type": "Widgets Type", "type-admin-settings": "Admin Settings", + "type-ota-package": "Ota Package", + "type-queue": "Queue", "action-type-added": "Added", "action-type-deleted": "Deleted", "action-type-updated": "Updated", @@ -2027,6 +2033,8 @@ "action-type-rpc-call": "RPC Call", "action-type-alarm-ack": "Alarm Ack", "action-type-alarm-clear": "Alarm Clear", + "action-type-alarm-assigned": "Alarm Assigned", + "action-type-alarm-unassigned": "Alarm Unassigned", "action-type-assigned-to-edge": "Assigned to Edge", "action-type-unassigned-from-edge": "Unassigned from Edge", "action-type-credentials-request": "Credentials Request", @@ -2958,6 +2966,12 @@ "lwm2m-server-secret-key": "LwM2M server secret key", "lwm2m-server-public-key-id": "LwM2M server public key or id" }, + "snmp": { + "host": "SNMP host", + "port": "SNMP port", + "version": "SNMP version (v1, v2c or v3)", + "community-string": "SNMP community string" + }, "isgateway": "Is Gateway", "activity-time-from-gateway-device": "Activity time from gateway device", "description": "Description", @@ -5515,6 +5529,9 @@ "input-fields-alignment": "Input fields alignment", "input-fields-alignment-column": "Column (default)", "input-fields-alignment-row": "Row", + "layout": "Layout", + "row-gap": "Gap between rows in pixels", + "column-gap": "Gap between columns in pixels", "latitude-field-required": "Latitude field required", "longitude-field-required": "Longitude field required", "attribute-settings": "Attribute settings", @@ -5570,6 +5587,12 @@ "ability-to-edit-attribute-disabled": "Disabled", "ability-to-edit-attribute-readonly": "Read-only", "disable-on-datakey-name": "Disable on false value of another datakey (specify datakey name)", + "field-appearance": "Field appearance", + "appearance-fill": "Fill", + "appearance-outline": "Outline", + "subscript-sizing": "Subscript sizing", + "subscript-sizing-fixed": "Fixed", + "subscript-sizing-dynamic": "Dynamic", "slide-toggle-settings": "Slide toggle settings", "slide-toggle-label-position": "Slide toggle label position", "slide-toggle-label-position-after": "After",