diff --git a/.gitignore b/.gitignore index ae3b49af21..3a4af54b75 100644 --- a/.gitignore +++ b/.gitignore @@ -33,5 +33,6 @@ pom.xml.versionsBackup **/.env .instance_id rebuild-docker.sh +*/.run/** .run/** .run \ No newline at end of file diff --git a/application/pom.xml b/application/pom.xml index 1d02f09d16..6ba8bd6db5 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -101,6 +101,10 @@ org.thingsboard.common stats + + org.thingsboard.common + edge-api + org.thingsboard dao diff --git a/application/src/main/data/json/demo/edge_management/rule_chains/edge_root_rule_chain.json b/application/src/main/data/json/demo/edge_management/rule_chains/edge_root_rule_chain.json new file mode 100644 index 0000000000..740d64627e --- /dev/null +++ b/application/src/main/data/json/demo/edge_management/rule_chains/edge_root_rule_chain.json @@ -0,0 +1,162 @@ +{ + "ruleChain": { + "additionalInfo": null, + "name": "Edge Root Rule Chain", + "type": "EDGE", + "firstRuleNodeId": null, + "root": true, + "debugMode": false, + "configuration": null + }, + "metadata": { + "firstNodeIndex": 0, + "nodes": [ + { + "additionalInfo": { + "description": "Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \"Success\" relation type.", + "layoutX": 203, + "layoutY": 259 + }, + "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", + "name": "Device Profile Node", + "debugMode": false, + "configuration": { + "persistAlarmRulesState": false, + "fetchAlarmRulesStateOnStart": false + } + }, + { + "additionalInfo": { + "layoutX": 823, + "layoutY": 157 + }, + "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", + "name": "Save Timeseries", + "debugMode": false, + "configuration": { + "defaultTTL": 0 + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 52 + }, + "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", + "name": "Save Client Attributes", + "debugMode": false, + "configuration": { + "scope": "CLIENT_SCOPE" + } + }, + { + "additionalInfo": { + "layoutX": 347, + "layoutY": 149 + }, + "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", + "name": "Message Type Switch", + "debugMode": false, + "configuration": { + "version": 0 + } + }, + { + "additionalInfo": { + "layoutX": 825, + "layoutY": 266 + }, + "type": "org.thingsboard.rule.engine.action.TbLogNode", + "name": "Log RPC from Device", + "debugMode": false, + "configuration": { + "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 378 + }, + "type": "org.thingsboard.rule.engine.action.TbLogNode", + "name": "Log Other", + "debugMode": false, + "configuration": { + "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 466 + }, + "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", + "name": "RPC Call Request", + "debugMode": false, + "configuration": { + "timeoutInSeconds": 60 + } + }, + { + "additionalInfo": { + "layoutX": 1134, + "layoutY": 132 + }, + "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", + "name": "Push to cloud", + "debugMode": false, + "configuration": { + "version": 0 + } + } + ], + "connections": [ + { + "fromIndex": 0, + "toIndex": 3, + "type": "Success" + }, + { + "fromIndex": 1, + "toIndex": 7, + "type": "Success" + }, + { + "fromIndex": 2, + "toIndex": 7, + "type": "Success" + }, + { + "fromIndex": 3, + "toIndex": 6, + "type": "RPC Request to Device" + }, + { + "fromIndex": 3, + "toIndex": 5, + "type": "Other" + }, + { + "fromIndex": 3, + "toIndex": 2, + "type": "Post attributes" + }, + { + "fromIndex": 3, + "toIndex": 1, + "type": "Post telemetry" + }, + { + "fromIndex": 3, + "toIndex": 4, + "type": "RPC Request from Device" + }, + { + "fromIndex": 4, + "toIndex": 7, + "type": "Success" + } + ], + "ruleChainConnections": null + } +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/edge_widgets.json b/application/src/main/data/json/system/widget_bundles/edge_widgets.json new file mode 100644 index 0000000000..8b64885d80 --- /dev/null +++ b/application/src/main/data/json/system/widget_bundles/edge_widgets.json @@ -0,0 +1,25 @@ +{ + "widgetsBundle": { + "alias": "edge_widgets", + "title": "Edge widgets", + "image": null + }, + "widgetTypes": [ + { + "alias": "edges_overview", + "name": "Edge Quick Overview", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.onDestroy = function() {\n};\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EdgeOverviewSettings\",\n \"properties\": {\n \"enableDefaultTitle\": {\n \"title\": \"Display default title\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"enableDefaultTitle\"\n ]\n}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"showTitleIcon\":true,\"titleIcon\":\"router\",\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{},\"title\":\"Edge Quick Overview\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"widgetStyle\":{},\"actions\":{}}" + } + } + ] +} diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json index b19c5d40b9..8a80aa80f1 100644 --- a/application/src/main/data/json/system/widget_bundles/input_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json @@ -455,6 +455,24 @@ "dataKeySettingsSchema": "{}\n", "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\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } + }, + { + "alias": "update_json_attribute", + "name": "Update JSON attribute", + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUoAAADhCAMAAACZWwyuAAAB71BMVEX////w8PDt7e3z8/Pg4ODAwMDc3Nz19fUZhJQ4lKK9vb17e3v9/f2mpqbj4+P4+Pi+vr58fHzT09P7+/uTxcz2+/ucy9GpqakCAgK93OHNzc0eh5b09PSw1dusrKwvkZ+ampqysrIzMzPV1dXCwsKurq4iiZnZ2dnX19fIyMgrj50ABsiKiopmZmbo8/XQ5+ro6OjPz89JnqqCgoLX6u2cnJwmjJv39/ff39+12N7ExMQ2NjaioqKYmJjs7OzD4OTS0tLKysrh8PLq6uq6uroCCMjDw8OOjo6hzdO0tLQuj56UlJS4uvDZ7O7l5eVvsrw0k6EchZWEhIRwcHDU6ezG4eaGv8d1tb+3t7dCmqeIiIj4+/3j8fO62+AKD8tWpbGgoKBqamouLi7u7u47lqTP0PWr0tgXHM1/u8N6uMFqsLpfqrVNoKxFRUXU1faYyM92dnY7Ozvz+Pry8vK5ubm4uLh+fn5sbGzGx/OipOtzduG42t8uM9KNw8pTo690dHT6+vrt9viOkefb29um0NYfHx/p6vu9vvGqrO2YmulXW9uhztRnrrgmJiYQEBDt7fzk5frf4PjL4+eMj+a/3eJpbN9MUNk7P9SQxMuNwslfX19TU1OcnurI4uY9QdYyN9ImK9BVVVVPT097fuNYWFjSJwPyAAALvElEQVR42uzbTWviQBjA8ad5qgNONI05FCSIshUNKERCSFlM2WIxSKCH5FI8RfoFCrvVYwv7xTfG4susxqgpqJkfHoLM6c9MTCYROI7jOC4doiIRoqrEkhQRuIOJEsEVROI1Dw2pIkPlMQ8PyWMez8ItLOD2QnArAlxyIsEYhK/x5CSMJQGX0Cfu8AmXTCEqbkWUPQaBiDtd8hJXMJ6SbNByeWd3iROMRxIPAhEx09MSd1ATDwIJMdPTEndJNIid4D/kkLk4yMbFZTop2fXdonSZktb2XuFKpwRnBzeoGoeklNZS1tjj+BVu2hDKfYhxKXUXTtnXHIqgXd+esk/pNSK+GrQcjZsbuotBVrKUFmzSGs++b1djZqVV1m01D6crqlSpVOldpYKFSmzKWUCN0nI0bq5bWAwiuHquNNnj+JOlSDWA144OJW86KUcp38oAtQZIPZ/mICQEBvWGcLpw7i6acu0iCkXDtQ1hcos4MtdTOt4VYs2h5dk42y4azj3ejhYpVUyEwEZ1F+BpBJKviQVfWUkZuOTZz0e5ba8FJ4xNWZcLPw0De97vezpYTzmUK1iiL18p5T+aEaymxGRU2OhhTMAbgFISATvCMqU0LgEUo4U/KOreKV9LsSlHAeKtgY+09STr6ylvzABfRuWvlC5i4DCz8oiU4Az6Uwug25blzsMyZb8zDpkQkgj04YSxKSf16FwpTOquzZwrnx5lvT3sLlP21lKSoxY4aM4wANCn10DmKemvaFZ2HuBMMCnDT5QSe205x6R8ufICWqowKQ/92WFJ/lsTID+9vhp29FlKpye1/AbUXELqTTgDbMq60S2PDMRHKutsSqzTGrIpD74YYvUohBr+x1D+O0tZMqaO0wASfPjmWdwjbfgFd0wDUZjY+F/Ke6ptSRl/iZ6Nm3Dc4sq7Se/GMRtbQ7hZvjrR+XZGKimrhoZ7pOSbbKntV/Kt39R20fkDiZSf7Yi4i3rBkzLVJ46g4M7mHH+l4Bjpv+hiXfTyTr9lVk+U38Diqzs1n1l8XeibiJaKLP7O74FEiYdMT/RfE0RVJZICe8txSSRKmU9KEIRm8V0QnitNIWvST6m93whhy3/sm/lTEmEYx7/SOLPbdFgjCDGEWm0FBUVEGRRF0WEmlCgxeFSaeKHpNF1ajWW3V4cddh9/aLs078PqCMOCxjrs55dn3mff9/3hM7x7fGfYrSs1ll9leUWFrmrK+ERXahSk0uc9srTKJ7c31+hKjQJUHnnorc2g0lijK9eVGgWovHF1fUaV5zSVyg54RUaVNzWVearcK5FWWWPcqanMU2VjU1NTI6kcNjbpNJX5HvDGq43pA7510/Q+TaVylcyl/F65z1iuqVSi8vgpX23VqaUfO5c1lUpUrq+V2LXUh6PxnPbhuExxhnHrfi3OKFylbseV8ic/tz/WlRoroFK8V9ZMGad0pUZuKstyBSWMpnJ1qfS0tXmk6hhFHmRfR/sXnQJUVswPJy8dy0Vlvd9fL1WDFXmRfR3tv5hn39oT9+JU5TyyNwBQi8rOz3vWVnurc1HZ5XR2rZhK2n8x68buNbwNPqIq43lCVSrLpB/k1aZcVD5wODoAXOQk9ND3hD39DrzhzR39QgiIhs22Xj1YH4EQL5hjQIQ3C5OO9DrbWaCSZ9dpPu2PjdeuHZK7BPD+JdU0g4nrQVWplPizJxeVAaAVgLvF5na7gT6+rnlgAJZ6p6ujWfCD72uesEWpDxP3qvlrGzA+6hwNh2gdU8mus/m0PzYYjYexkO/3qBLxsbtQnUpf8vDCRi4HNcBFADcXsPDoDcF1FnwlYOFaWV9UpQdjiGfrSCW7TvPTVB/FQl4knlIlfr+H6lRWDZ8uU66yjjOLcM2Syj64DCmV41wd65PK1sl6m9WWUSXNz8zd9jtUiaftg6pTuXX4VFkeKk2c84KIXlTZE2IqRzgT65PKrgF/pEem0jpEKuX7ZDPZQDXNC3swGLSnnkNqUbnfW6PwFb1bgEgr1w0JUSXAVJq4AOszVSNcHTBho3VwRUkl7bOQg9WQMdj+nOo/vnwBMDMoEnwxA0AtKm/MbxE5qUBlhGt5Z9AjKtw3WSwylTFLd7gfrM9Uge/wO3ts6XWfhFf+PlJJ84nFj52XH66LxKli1m5/DQl2wM9MX1aFyl//8koFKvHRY3Y5sO5+2Gxtkau0CpMjYH1SdaFDMNf3p9fpo8LF+hBdp/nEopehH3aJ11QRHxuLL1D5y3hbFSqXAvnAV6JIbJuu1lQuC0eStRs1lctC1aU16njsaCFb8VRW5pCxvWnBauW/5ZXZDvKQJ0K6ednYMarGXHIFVFZ99ib36BTklRlVRjmTXCWNDdaccsmgXaIBSlGPSt9BMa/0KcgrM6rUOyFTSWNSmT2XxB2RucQsFKMalRLztxTklbxVEKJ6FktQXhngOK4OIiOTgjXGszHllA5p1Mo1L5FLErPtd6EcFansXDt8TkFeyff7J4RPTCXllXG3+2JKXX/YYhFVsjHllLFewCLEs+WSc2MoOoWoXFtbu6kzg8pM74+9LlJJeSWQUhfg/Kk+G9MBb+GB3g5kySWfioFF0SlE5b4TV4YvKVQ5ZCWVLK9k6tq4kaVVOjk3YkPIwtuECv4gV+C90uc9pkzlWRvljiyvZOpMmVSiq2WciyALH+ZQfApUuV65SsodKRkCPN2p/NGS6rMx5ZQYCk+Es+SSmAk+Q/EpQOX5mgpf8mGZYpUsd5SpfDBgMugR8hikPhtTTolxwdqDzLkkntlHUHzyV9l5I+lNHmhUrpLljjKVzpg55kC8x2a2hsDGlFMCvVwEmXNJPEpABayOOMPggvpZBSrHnRaPCepnBVRuOFFefmIDlg3TX/buXqVhKIzD+FuonEEJJQ4eKogEOrjFQRRSisEiuIl3YPYOvQId7Kg34OSdGgd1SaD5aPmH8zwQsv8g5Jx3OOf08c0GUP+UUezMXDzA8/3lKHNnZS630Oqf8sAK//5QvkKrG+VVsqygXEQvGyibUc5WlZTl44+gbEJ5u5pm1ZT3/hjKBpSLr5NRDeWFf4WyyR78eVRHeecnUG5PebOOaynt0K8ttNpTzpMsy5LkvIry+uNpZqHVnjK6LMvm4yrKqXd84A2X6PzBd0mZux/KERvHfsYZE884o5ch2/LscxOe5C4orfDFxMJrAFP0oQQllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBuWdK2qLwDj8lIiIiIiIKLpdG45ZFqTP6y407heV/aTfK1Oi3qBtlgNdmfLdrxypyw0AAhifJ2AeDZtcohUAEgZGFrdJVjIsgk2DY5urt9xFu9wn2xWMFLklzV3gb45u/GHD7YSML5s3wwUASSqHcbkIplNtLKIVyewll7onaZR7oB/yfJ/UYZfvLmbMXyscpy2CKxplOKB+mtBwRNRcflLJJvhh/Anybir4nBV9sMeJKyonnZaYO/cm4GX1+JC6xr02dPgAlpaFpFMTq7onUsRkx0WEd5RD42ucP3YWJeMJQI4YTJj7H2tz3TzkBlPR0oQEgkUK6AEzz2mPnxhx6LPsW0RA2XHYc8ebKwXPcP2UPgPTynAk9KV2pTHtcRZkxZ8Ma9dkZvqJmO3KLjnPFTimP1EEm/PyP8tMfSp8p7/R9FWWyy9BMrTnp1lwRA53zNx70UrtTSmjiMhIdXykvdADQpDCT2katoiTWiC1Xli0OmbJyZkQ8m4XR7/XYASQ7JNLwSglxfh4qUl+bETUNsIqyM66YAyfPdaz5hpiYuzzDWLHeKyUcYhEH9Zcy/wzZRApebDUOa2873dWZ2iJG54oQENHVuGRrE+Ju30q5g28qoRTK7SWUQrm9hFIot5dQCuX2kvUrWQrcXrKqKgvUkiRJkiRJkiS9328dj4CaN1dagAAAAABJRU5ErkJggg==", + "description": "Simple form to input new JSON value for pre-defined attribute/timeseries key.", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "\n", + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.jsonInputWidget.onDataUpdated();\n}\n\nself.onResize = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AdvancedSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"widgetMode\": {\n \"title\": \"Widget mode\",\n \"type\": \"string\",\n \"default\": \"ATTRIBUTE\"\n },\n \"attributeScope\": {\n \"title\": \"Attribute scope\",\n \"type\": \"string\",\n \"default\": \"SERVER_SCOPE\"\n },\n \"showLabel\":{\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"attributeRequired\": {\n \"title\": \"Value required\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"widgetMode\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"ATTRIBUTE\",\n \"label\": \"Update attribute\"\n },\n {\n \"value\": \"TIME_SERIES\",\n \"label\": \"Update timeseries\"\n }\n ]\n },\n {\n \"key\": \"attributeScope\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"condition\": \"model.widgetMode === 'ATTRIBUTE'\",\n \"items\": [\n {\n \"value\": \"SERVER_SCOPE\",\n \"label\": \"Server attribute\"\n },\n {\n \"value\": \"SHARED_SCOPE\",\n \"label\": \"Shared attribute\"\n }\n ]\n },\n \"showLabel\",\n {\n \"key\": \"labelValue\",\n \"condition\": \"model.showLabel\"\n },\n \"attributeRequired\",\n \"showResultMessage\"\n ]\n}", + "dataKeySettingsSchema": "{}", + "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\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" + } } ] } diff --git a/application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json b/application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json new file mode 100644 index 0000000000..740d64627e --- /dev/null +++ b/application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json @@ -0,0 +1,162 @@ +{ + "ruleChain": { + "additionalInfo": null, + "name": "Edge Root Rule Chain", + "type": "EDGE", + "firstRuleNodeId": null, + "root": true, + "debugMode": false, + "configuration": null + }, + "metadata": { + "firstNodeIndex": 0, + "nodes": [ + { + "additionalInfo": { + "description": "Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \"Success\" relation type.", + "layoutX": 203, + "layoutY": 259 + }, + "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", + "name": "Device Profile Node", + "debugMode": false, + "configuration": { + "persistAlarmRulesState": false, + "fetchAlarmRulesStateOnStart": false + } + }, + { + "additionalInfo": { + "layoutX": 823, + "layoutY": 157 + }, + "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", + "name": "Save Timeseries", + "debugMode": false, + "configuration": { + "defaultTTL": 0 + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 52 + }, + "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", + "name": "Save Client Attributes", + "debugMode": false, + "configuration": { + "scope": "CLIENT_SCOPE" + } + }, + { + "additionalInfo": { + "layoutX": 347, + "layoutY": 149 + }, + "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", + "name": "Message Type Switch", + "debugMode": false, + "configuration": { + "version": 0 + } + }, + { + "additionalInfo": { + "layoutX": 825, + "layoutY": 266 + }, + "type": "org.thingsboard.rule.engine.action.TbLogNode", + "name": "Log RPC from Device", + "debugMode": false, + "configuration": { + "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 378 + }, + "type": "org.thingsboard.rule.engine.action.TbLogNode", + "name": "Log Other", + "debugMode": false, + "configuration": { + "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" + } + }, + { + "additionalInfo": { + "layoutX": 824, + "layoutY": 466 + }, + "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", + "name": "RPC Call Request", + "debugMode": false, + "configuration": { + "timeoutInSeconds": 60 + } + }, + { + "additionalInfo": { + "layoutX": 1134, + "layoutY": 132 + }, + "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", + "name": "Push to cloud", + "debugMode": false, + "configuration": { + "version": 0 + } + } + ], + "connections": [ + { + "fromIndex": 0, + "toIndex": 3, + "type": "Success" + }, + { + "fromIndex": 1, + "toIndex": 7, + "type": "Success" + }, + { + "fromIndex": 2, + "toIndex": 7, + "type": "Success" + }, + { + "fromIndex": 3, + "toIndex": 6, + "type": "RPC Request to Device" + }, + { + "fromIndex": 3, + "toIndex": 5, + "type": "Other" + }, + { + "fromIndex": 3, + "toIndex": 2, + "type": "Post attributes" + }, + { + "fromIndex": 3, + "toIndex": 1, + "type": "Post telemetry" + }, + { + "fromIndex": 3, + "toIndex": 4, + "type": "RPC Request from Device" + }, + { + "fromIndex": 4, + "toIndex": 7, + "type": "Success" + } + ], + "ruleChainConnections": null + } +} \ No newline at end of file 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 a37b1fc865..8231ca81ff 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 @@ -2,6 +2,7 @@ "ruleChain": { "additionalInfo": null, "name": "Root Rule Chain", + "type": "CORE", "firstRuleNodeId": null, "root": true, "debugMode": false, diff --git a/application/src/main/data/upgrade/3.2.2/schema_update.sql b/application/src/main/data/upgrade/3.2.2/schema_update.sql index e68392230a..b138b5c254 100644 --- a/application/src/main/data/upgrade/3.2.2/schema_update.sql +++ b/application/src/main/data/upgrade/3.2.2/schema_update.sql @@ -14,6 +14,38 @@ -- limitations under the License. -- +CREATE TABLE IF NOT EXISTS edge ( + id uuid NOT NULL CONSTRAINT edge_pkey PRIMARY KEY, + created_time bigint NOT NULL, + additional_info varchar, + customer_id uuid, + root_rule_chain_id uuid, + type varchar(255), + name varchar(255), + label varchar(255), + routing_key varchar(255), + secret varchar(255), + edge_license_key varchar(30), + cloud_endpoint varchar(255), + search_text varchar(255), + tenant_id uuid, + CONSTRAINT edge_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT edge_routing_key_unq_key UNIQUE (routing_key) + ); + +CREATE TABLE IF NOT EXISTS edge_event ( + id uuid NOT NULL CONSTRAINT edge_event_pkey PRIMARY KEY, + created_time bigint NOT NULL, + edge_id uuid, + edge_event_type varchar(255), + edge_event_uid varchar(255), + entity_id uuid, + edge_event_action varchar(255), + body varchar(10000000), + tenant_id uuid, + ts bigint NOT NULL + ); + CREATE TABLE IF NOT EXISTS resource ( id uuid NOT NULL CONSTRAINT resource_pkey PRIMARY KEY, created_time bigint NOT NULL, @@ -64,3 +96,4 @@ DO $$ END IF; END; $$; + diff --git a/application/src/main/data/upgrade/3.2.2/schema_update_ttl.sql b/application/src/main/data/upgrade/3.2.2/schema_update_ttl.sql new file mode 100644 index 0000000000..f70c65cbdc --- /dev/null +++ b/application/src/main/data/upgrade/3.2.2/schema_update_ttl.sql @@ -0,0 +1,32 @@ +-- +-- Copyright © 2016-2021 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. +-- + +CREATE OR REPLACE PROCEDURE cleanup_edge_events_by_ttl(IN ttl bigint, INOUT deleted bigint) + LANGUAGE plpgsql AS +$$ +DECLARE + ttl_ts bigint; + ttl_deleted_count bigint DEFAULT 0; +BEGIN + IF ttl > 0 THEN + ttl_ts := (EXTRACT(EPOCH FROM current_timestamp) * 1000 - ttl::bigint * 1000)::bigint; + EXECUTE format( + 'WITH deleted AS (DELETE FROM edge_event WHERE ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', ttl_ts) into ttl_deleted_count; + END IF; + RAISE NOTICE 'Edge events removed by ttl: %', ttl_deleted_count; + deleted := ttl_deleted_count; +END +$$; diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 430819391a..c54f940c38 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -46,6 +46,7 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.tools.TbRateLimits; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.audit.AuditLogService; @@ -54,6 +55,8 @@ import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.ClaimDevicesService; 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.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.nosql.CassandraBufferedRateExecutor; @@ -69,7 +72,7 @@ import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.usagestats.TbApiUsageClient; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.component.ComponentDiscoveryService; -import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; +import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.executors.ExternalCallExecutorService; import org.thingsboard.server.service.executors.SharedEventLoopGroupService; @@ -296,6 +299,18 @@ public class ActorSystemContext { @Getter private TbCoreDeviceRpcService tbCoreDeviceRpcService; + @Lazy + @Autowired(required = false) + @Getter private EdgeService edgeService; + + @Lazy + @Autowired(required = false) + @Getter private EdgeEventService edgeEventService; + + @Lazy + @Autowired(required = false) + @Getter private EdgeRpcService edgeRpcService; + @Value("${actors.session.max_concurrent_sessions_per_device:1}") @Getter private long maxConcurrentSessionsPerDevice; @@ -320,6 +335,9 @@ public class ActorSystemContext { @Getter private long statisticsPersistFrequency; + @Value("${edges.enabled}") + @Getter + private boolean edgesEnabled; @Scheduled(fixedDelayString = "${actors.statistics.js_print_interval_ms}") public void printStats() { diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index f1248a3aad..81e9c8917a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -35,10 +35,12 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.aware.TenantAwareMsg; +import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; @@ -89,10 +91,15 @@ public class AppActor extends ContextAwareActor { case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG: case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG: case DEVICE_NAME_OR_TYPE_UPDATE_TO_DEVICE_ACTOR_MSG: + case DEVICE_EDGE_UPDATE_TO_DEVICE_ACTOR_MSG: case DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG: + case DEVICE_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG: case SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG: onToDeviceActorMsg((TenantAwareMsg) msg, true); break; + case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG: + onToTenantActorMsg((EdgeEventUpdateMsg) msg); + break; default: return false; } @@ -192,6 +199,20 @@ public class AppActor extends ContextAwareActor { () -> new TenantActor.ActorCreator(systemContext, tenantId)); } + private void onToTenantActorMsg(EdgeEventUpdateMsg msg) { + TbActorRef target = null; + if (ModelConstants.SYSTEM_TENANT.equals(msg.getTenantId())) { + log.warn("Message has system tenant id: {}", msg); + } else { + target = getOrCreateTenantActor(msg.getTenantId()); + } + if (target != null) { + target.tellWithHighPriority(msg); + } else { + log.debug("[{}] Invalid edge event update msg: {}", msg.getTenantId(), msg); + } + } + public static class ActorCreator extends ContextBasedCreator { public ActorCreator(ActorSystemContext context) { diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java index 9233f4a317..6e2761c9c5 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java @@ -17,6 +17,7 @@ package org.thingsboard.server.actors.device; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; +import org.thingsboard.rule.engine.api.msg.DeviceEdgeUpdateMsg; import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; @@ -26,6 +27,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.timeout.DeviceActorServerSideRpcTimeoutMsg; +import org.thingsboard.server.service.rpc.FromDeviceRpcResponseActorMsg; import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; @@ -70,12 +72,18 @@ public class DeviceActor extends ContextAwareActor { case DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG: processor.processRpcRequest(ctx, (ToDeviceRpcRequestActorMsg) msg); break; + case DEVICE_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG: + processor.processRpcResponsesFromEdge(ctx, (FromDeviceRpcResponseActorMsg) msg); + break; case DEVICE_ACTOR_SERVER_SIDE_RPC_TIMEOUT_MSG: processor.processServerSideRpcTimeout(ctx, (DeviceActorServerSideRpcTimeoutMsg) msg); break; case SESSION_TIMEOUT_MSG: processor.checkSessionsTimeout(); break; + case DEVICE_EDGE_UPDATE_TO_DEVICE_ACTOR_MSG: + processor.processEdgeUpdate((DeviceEdgeUpdateMsg) msg); + break; default: return false; } diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index 41073939f2..c90c47fb1c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.actors.device; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -24,6 +25,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.thingsboard.rule.engine.api.RpcError; import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; +import org.thingsboard.rule.engine.api.msg.DeviceEdgeUpdateMsg; import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg; import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.actors.ActorSystemContext; @@ -31,11 +33,17 @@ import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKey; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; @@ -68,6 +76,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceAct import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; import org.thingsboard.server.service.rpc.FromDeviceRpcResponse; +import org.thingsboard.server.service.rpc.FromDeviceRpcResponseActorMsg; import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; @@ -103,6 +112,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { private String deviceName; private String deviceType; private TbMsgMetaData defaultMetaData; + private EdgeId edgeId; DeviceActorMessageProcessor(ActorSystemContext systemContext, TenantId tenantId, DeviceId deviceId) { super(systemContext); @@ -125,12 +135,32 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { this.defaultMetaData = new TbMsgMetaData(); this.defaultMetaData.putValue("deviceName", deviceName); this.defaultMetaData.putValue("deviceType", deviceType); + if (systemContext.isEdgesEnabled()) { + this.edgeId = findRelatedEdgeId(); + } return true; } else { return false; } } + private EdgeId findRelatedEdgeId() { + List result = + systemContext.getRelationService().findByToAndType(tenantId, deviceId, EntityRelation.EDGE_TYPE, RelationTypeGroup.COMMON); + if (result != null && result.size() > 0) { + EntityRelation relationToEdge = result.get(0); + if (relationToEdge.getFrom() != null && relationToEdge.getFrom().getId() != null) { + log.trace("[{}][{}] found edge [{}] for device", tenantId, deviceId, relationToEdge.getFrom().getId()); + return new EdgeId(relationToEdge.getFrom().getId()); + } else { + log.trace("[{}][{}] edge relation is empty {}", tenantId, deviceId, relationToEdge); + } + } else { + log.trace("[{}][{}] device doesn't have any related edge", tenantId, deviceId); + } + return null; + } + void processRpcRequest(TbActorCtx context, ToDeviceRpcRequestActorMsg msg) { ToDeviceRpcRequest request = msg.getMsg(); ToDeviceRpcRequestBody body = request.getBody(); @@ -143,15 +173,22 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { return; } - boolean sent = rpcSubscriptions.size() > 0; - Set syncSessionSet = new HashSet<>(); - rpcSubscriptions.forEach((key, value) -> { - sendToTransport(rpcRequest, key, value.getNodeId()); - if (SessionType.SYNC == value.getType()) { - syncSessionSet.add(key); - } - }); - syncSessionSet.forEach(rpcSubscriptions::remove); + boolean sent; + if (systemContext.isEdgesEnabled() && edgeId != null) { + log.debug("[{}][{}] device is related to edge [{}]. Saving RPC request to edge queue", tenantId, deviceId, edgeId.getId()); + saveRpcRequestToEdgeQueue(request, rpcRequest.getRequestId()); + sent = true; + } else { + sent = rpcSubscriptions.size() > 0; + Set syncSessionSet = new HashSet<>(); + rpcSubscriptions.forEach((key, value) -> { + sendToTransport(rpcRequest, key, value.getNodeId()); + if (SessionType.SYNC == value.getType()) { + syncSessionSet.add(key); + } + }); + syncSessionSet.forEach(rpcSubscriptions::remove); + } if (request.isOneway() && sent) { log.debug("[{}] Rpc command response sent [{}]!", deviceId, request.getId()); @@ -166,6 +203,17 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { } } + void processRpcResponsesFromEdge(TbActorCtx context, FromDeviceRpcResponseActorMsg responseMsg) { + log.debug("[{}] Processing rpc command response from edge session", deviceId); + ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(responseMsg.getRequestId()); + boolean success = requestMd != null; + if (success) { + systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(responseMsg.getMsg()); + } else { + log.debug("[{}] Rpc command response [{}] is stale!", deviceId, responseMsg.getRequestId()); + } + } + private void registerPendingRpcRequest(TbActorCtx context, ToDeviceRpcRequestActorMsg msg, boolean sent, ToDeviceRpcRequestMsg rpcRequest, long timeout) { toDeviceRpcPendingMap.put(rpcRequest.getRequestId(), new ToDeviceRpcRequestMetadata(msg, sent)); DeviceActorServerSideRpcTimeoutMsg timeoutMsg = new DeviceActorServerSideRpcTimeoutMsg(rpcRequest.getRequestId(), timeout); @@ -498,6 +546,11 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { this.defaultMetaData.putValue("deviceType", deviceType); } + void processEdgeUpdate(DeviceEdgeUpdateMsg msg) { + log.trace("[{}] Processing edge update {}", deviceId, msg); + this.edgeId = msg.getEdgeId(); + } + private void sendToTransport(GetAttributeResponseMsg responseMsg, SessionInfoProto sessionInfo) { ToTransportMsg msg = ToTransportMsg.newBuilder() .setSessionIdMSB(sessionInfo.getSessionIdMSB()) @@ -530,6 +583,36 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { systemContext.getTbCoreToTransportService().process(nodeId, msg); } + private void saveRpcRequestToEdgeQueue(ToDeviceRpcRequest msg, Integer requestId) { + EdgeEvent edgeEvent = new EdgeEvent(); + edgeEvent.setTenantId(tenantId); + edgeEvent.setAction(EdgeEventActionType.RPC_CALL); + edgeEvent.setEntityId(deviceId.getId()); + edgeEvent.setType(EdgeEventType.DEVICE); + + ObjectNode body = mapper.createObjectNode(); + body.put("requestId", requestId); + body.put("requestUUID", msg.getId().toString()); + body.put("oneway", msg.isOneway()); + body.put("expirationTime", msg.getExpirationTime()); + body.put("method", msg.getBody().getMethod()); + body.put("params", msg.getBody().getParams()); + edgeEvent.setBody(body); + + edgeEvent.setEdgeId(edgeId); + ListenableFuture future = systemContext.getEdgeEventService().saveAsync(edgeEvent); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess( EdgeEvent result) { + systemContext.getClusterService().onEdgeEventUpdate(tenantId, edgeId); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Can't save edge event [{}] for edge [{}]", tenantId.getId(), edgeEvent, edgeId.getId(), t); + } + }, systemContext.getDbCallbackExecutor()); + } private List toTsKvProtos(@Nullable List result) { List clientAttributes; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index e1717e3b9b..ae7d8c492e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; @@ -62,6 +63,8 @@ import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; 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.entityview.EntityViewService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; import org.thingsboard.server.dao.nosql.TbResultSetFuture; @@ -319,6 +322,11 @@ class DefaultTbContext implements TbContext { return entityActionMsg(alarm, alarm.getId(), ruleNodeId, action, queueName, ruleChainId); } + @Override + public void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId) { + mainCtx.getClusterService().onEdgeEventUpdate(tenantId, edgeId); + } + public TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action) { return entityActionMsg(entity, id, ruleNodeId, action, ServiceQueue.MAIN, null); } @@ -477,6 +485,16 @@ class DefaultTbContext implements TbContext { return mainCtx.getDeviceProfileCache(); } + @Override + public EdgeService getEdgeService() { + return mainCtx.getEdgeService(); + } + + @Override + public EdgeEventService getEdgeEventService() { + return mainCtx.getEdgeEventService(); + } + @Override public EventLoopGroup getSharedEventLoop() { return mainCtx.getSharedEventLoopGroupService().getSharedEventLoopGroup(); diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java index 830714b431..10d6272beb 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleState; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; @@ -99,7 +100,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor ruleNodeList = service.getRuleChainNodes(tenantId, entityId); log.trace("[{}][{}] Starting rule chain with {} nodes", tenantId, entityId, ruleNodeList.size()); // Creating and starting the actors; @@ -119,7 +120,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor ruleNodeList = service.getRuleChainNodes(tenantId, entityId); log.trace("[{}][{}] Updating rule chain with {} nodes", tenantId, entityId, ruleNodeList.size()); diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java index b1c6c2f0ed..76f5ec1809 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java @@ -23,13 +23,13 @@ import org.thingsboard.server.actors.TbEntityActorId; import org.thingsboard.server.actors.TbEntityTypeActorIdPredicate; import org.thingsboard.server.actors.service.ContextAwareActor; import org.thingsboard.server.actors.service.DefaultActorService; -import org.thingsboard.server.actors.tenant.TenantActor; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.dao.rule.RuleChainService; @@ -55,7 +55,7 @@ public abstract class RuleChainManagerActor extends ContextAwareActor { } protected void initRuleChains() { - for (RuleChain ruleChain : new PageDataIterable<>(link -> ruleChainService.findTenantRuleChains(tenantId, link), ContextAwareActor.ENTITY_PACK_LIMIT)) { + for (RuleChain ruleChain : new PageDataIterable<>(link -> ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, link), ContextAwareActor.ENTITY_PACK_LIMIT)) { RuleChainId ruleChainId = ruleChain.getId(); log.debug("[{}|{}] Creating rule chain actor", ruleChainId.getEntityType(), ruleChain.getId()); TbActorRef actorRef = getOrCreateActor(ruleChainId, id -> ruleChain); @@ -65,13 +65,13 @@ public abstract class RuleChainManagerActor extends ContextAwareActor { } protected void destroyRuleChains() { - for (RuleChain ruleChain : new PageDataIterable<>(link -> ruleChainService.findTenantRuleChains(tenantId, link), ContextAwareActor.ENTITY_PACK_LIMIT)) { + for (RuleChain ruleChain : new PageDataIterable<>(link -> ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, link), ContextAwareActor.ENTITY_PACK_LIMIT)) { ctx.stop(new TbEntityActorId(ruleChain.getId())); } } protected void visit(RuleChain entity, TbActorRef actorRef) { - if (entity != null && entity.isRoot()) { + if (entity != null && entity.isRoot() && entity.getType().equals(RuleChainType.CORE)) { rootChain = entity; rootChainActor = actorRef; } diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index d6d1d6aa9a..68afbdaf99 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -33,21 +33,27 @@ import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.aware.DeviceAwareMsg; import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg; +import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; import java.util.List; @@ -155,13 +161,18 @@ public class TenantActor extends RuleChainManagerActor { case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG: case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG: case DEVICE_NAME_OR_TYPE_UPDATE_TO_DEVICE_ACTOR_MSG: + case DEVICE_EDGE_UPDATE_TO_DEVICE_ACTOR_MSG: case DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG: + case DEVICE_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG: case SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG: onToDeviceActorMsg((DeviceAwareMsg) msg, true); break; case RULE_CHAIN_TO_RULE_CHAIN_MSG: onRuleChainMsg((RuleChainAwareMsg) msg); break; + case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG: + onToEdgeSessionMsg((EdgeEventUpdateMsg) msg); + break; default: return false; } @@ -230,14 +241,26 @@ public class TenantActor extends RuleChainManagerActor { log.info("[{}] Received API state update. Going to ENABLE Rule Engine execution.", tenantId); initRuleChains(); } - } - if (isRuleEngineForCurrentTenant) { + } else if (msg.getEntityId().getEntityType() == EntityType.EDGE) { + EdgeId edgeId = new EdgeId(msg.getEntityId().getId()); + EdgeRpcService edgeRpcService = systemContext.getEdgeRpcService(); + if (msg.getEvent() == ComponentLifecycleEvent.DELETED) { + edgeRpcService.deleteEdge(edgeId); + } else { + Edge edge = systemContext.getEdgeService().findEdgeById(tenantId, edgeId); + if (msg.getEvent() == ComponentLifecycleEvent.UPDATED) { + edgeRpcService.updateEdge(edge); + } + } + } else if (isRuleEngineForCurrentTenant) { TbActorRef target = getEntityActorRef(msg.getEntityId()); if (target != null) { if (msg.getEntityId().getEntityType() == EntityType.RULE_CHAIN) { RuleChain ruleChain = systemContext.getRuleChainService(). findRuleChainById(tenantId, new RuleChainId(msg.getEntityId().getId())); - visit(ruleChain, target); + if (ruleChain != null && RuleChainType.CORE.equals(ruleChain.getType())) { + visit(ruleChain, target); + } } target.tellWithHighPriority(msg); } else { @@ -252,6 +275,11 @@ public class TenantActor extends RuleChainManagerActor { () -> new DeviceActorCreator(systemContext, tenantId, deviceId)); } + private void onToEdgeSessionMsg(EdgeEventUpdateMsg msg) { + log.trace("[{}] onToEdgeSessionMsg [{}]", msg.getTenantId(), msg); + systemContext.getEdgeRpcService().onEdgeEvent(msg.getEdgeId()); + } + public static class ActorCreator extends ContextBasedCreator { private final TenantId tenantId; diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 8b595bd987..2718b455b9 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -71,7 +71,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public"; public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; - protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**"}; + protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**"}; public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**"; diff --git a/application/src/main/java/org/thingsboard/server/config/WebConfig.java b/application/src/main/java/org/thingsboard/server/config/WebConfig.java index 81d3390dd2..500d1bd06e 100644 --- a/application/src/main/java/org/thingsboard/server/config/WebConfig.java +++ b/application/src/main/java/org/thingsboard/server/config/WebConfig.java @@ -21,7 +21,7 @@ import org.springframework.web.bind.annotation.RequestMapping; @Controller public class WebConfig { - @RequestMapping(value = "/{path:^(?!api$)(?!assets$)(?!static$)(?!webjars$)[^\\.]*}/**") + @RequestMapping(value = {"/assets", "/assets/", "/{path:^(?!api$)(?!assets$)(?!static$)(?!webjars$)[^\\.]*}/**"}) public String redirect() { return "forward:/index.html"; } diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index 454bb12213..9453bd46b7 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AlarmId; @@ -45,8 +46,6 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; -import java.util.UUID; - @RestController @TbCoreComponent @RequestMapping("/api") @@ -93,6 +92,9 @@ public class AlarmController extends BaseController { logEntityAction(savedAlarm.getOriginator(), savedAlarm, getCurrentUser().getCustomerId(), alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + + sendEntityNotificationMsg(getTenantId(), savedAlarm.getId(), alarm.getId() == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED); + return savedAlarm; } catch (Exception e) { logEntityAction(emptyId(EntityType.ALARM), alarm, @@ -109,8 +111,11 @@ public class AlarmController extends BaseController { try { AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); checkAlarmId(alarmId, Operation.WRITE); + + sendEntityNotificationMsg(getTenantId(), alarmId, EdgeEventActionType.DELETED); + return alarmService.deleteAlarm(getTenantId(), alarmId); - } catch (Exception e) { + } catch (Exception e) { throw handleException(e); } } @@ -128,6 +133,8 @@ public class AlarmController extends BaseController { alarm.setAckTs(ackTs); alarm.setStatus(alarm.getStatus().isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK); logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_ACK, null); + + sendEntityNotificationMsg(getTenantId(), alarmId, EdgeEventActionType.ALARM_ACK); } catch (Exception e) { throw handleException(e); } @@ -146,6 +153,8 @@ public class AlarmController extends BaseController { alarm.setClearTs(clearTs); alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK); logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_CLEAR, null); + + sendEntityNotificationMsg(getTenantId(), alarmId, EdgeEventActionType.ALARM_CLEAR); } catch (Exception e) { throw handleException(e); } @@ -166,7 +175,6 @@ public class AlarmController extends BaseController { @RequestParam(required = false) String sortOrder, @RequestParam(required = false) Long startTime, @RequestParam(required = false) Long endTime, - @RequestParam(required = false) String offset, @RequestParam(required = false) Boolean fetchOriginator ) throws ThingsboardException { checkParameter("EntityId", strEntityId); @@ -180,12 +188,9 @@ public class AlarmController extends BaseController { } checkEntityId(entityId, Operation.READ); TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); - UUID idOffsetUuid = null; - if (StringUtils.isNotEmpty(offset)) { - idOffsetUuid = toUUID(offset); - } + try { - return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator, idOffsetUuid)).get()); + return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java index fb674f894c..19280716b3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -33,14 +33,18 @@ import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; 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.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -54,6 +58,8 @@ import java.util.stream.Collectors; import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE; +import static org.thingsboard.server.controller.EdgeController.EDGE_ID; + @RestController @TbCoreComponent @RequestMapping("/api") @@ -98,7 +104,7 @@ public class AssetController extends BaseController { asset.setTenantId(getCurrentUser().getTenantId()); - checkEntity(asset.getId(), asset, Resource.ASSET); + checkEntity(asset.getId(), asset, Resource.ASSET); Asset savedAsset = checkNotNull(assetService.saveAsset(asset)); @@ -106,6 +112,10 @@ public class AssetController extends BaseController { savedAsset.getCustomerId(), asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + if (asset.getId() != null) { + sendEntityNotificationMsg(savedAsset.getTenantId(), savedAsset.getId(), EdgeEventActionType.UPDATED); + } + return savedAsset; } catch (Exception e) { logEntityAction(emptyId(EntityType.ASSET), asset, @@ -122,12 +132,16 @@ public class AssetController extends BaseController { try { AssetId assetId = new AssetId(toUUID(strAssetId)); Asset asset = checkAssetId(assetId, Operation.DELETE); + + List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), assetId); + assetService.deleteAsset(getTenantId(), assetId); logEntityAction(assetId, asset, asset.getCustomerId(), ActionType.DELETED, null, strAssetId); + sendDeleteNotificationMsg(getTenantId(), assetId, relatedEdgeIds); } catch (Exception e) { logEntityAction(emptyId(EntityType.ASSET), null, @@ -157,6 +171,9 @@ public class AssetController extends BaseController { savedAsset.getCustomerId(), ActionType.ASSIGNED_TO_CUSTOMER, null, strAssetId, strCustomerId, customer.getName()); + sendEntityAssignToCustomerNotificationMsg(savedAsset.getTenantId(), savedAsset.getId(), + customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); + return savedAsset; } catch (Exception e) { @@ -188,6 +205,9 @@ public class AssetController extends BaseController { asset.getCustomerId(), ActionType.UNASSIGNED_FROM_CUSTOMER, null, strAssetId, customer.getId().toString(), customer.getName()); + sendEntityAssignToCustomerNotificationMsg(savedAsset.getTenantId(), savedAsset.getId(), + customer.getId(), EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); + return savedAsset; } catch (Exception e) { @@ -401,4 +421,113 @@ public class AssetController extends BaseController { throw handleException(e); } } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/asset/{assetId}", method = RequestMethod.POST) + @ResponseBody + public Asset assignAssetToEdge(@PathVariable(EDGE_ID) String strEdgeId, + @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + checkParameter(ASSET_ID, strAssetId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + + AssetId assetId = new AssetId(toUUID(strAssetId)); + checkAssetId(assetId, Operation.ASSIGN_TO_EDGE); + + Asset savedAsset = checkNotNull(assetService.assignAssetToEdge(getTenantId(), assetId, edgeId)); + + logEntityAction(assetId, savedAsset, + savedAsset.getCustomerId(), + ActionType.ASSIGNED_TO_EDGE, null, strAssetId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedAsset.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); + + return savedAsset; + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.ASSET), null, + null, + ActionType.ASSIGNED_TO_EDGE, e, strAssetId, strEdgeId); + + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/asset/{assetId}", method = RequestMethod.DELETE) + @ResponseBody + public Asset unassignAssetFromEdge(@PathVariable(EDGE_ID) String strEdgeId, + @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + checkParameter(ASSET_ID, strAssetId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + + AssetId assetId = new AssetId(toUUID(strAssetId)); + Asset asset = checkAssetId(assetId, Operation.UNASSIGN_FROM_EDGE); + + Asset savedAsset = checkNotNull(assetService.unassignAssetFromEdge(getTenantId(), assetId, edgeId)); + + logEntityAction(assetId, asset, + asset.getCustomerId(), + ActionType.UNASSIGNED_FROM_EDGE, null, strAssetId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedAsset.getId(), EdgeEventActionType.UNASSIGNED_FROM_EDGE); + + return savedAsset; + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.ASSET), null, + null, + ActionType.UNASSIGNED_FROM_EDGE, e, strAssetId, strEdgeId); + + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edge/{edgeId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getEdgeAssets( + @PathVariable(EDGE_ID) String strEdgeId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + PageData nonFilteredResult; + if (type != null && type.trim().length() > 0) { + nonFilteredResult = assetService.findAssetsByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink); + } else { + nonFilteredResult = assetService.findAssetsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); + } + List filteredAssets = nonFilteredResult.getData().stream().filter(asset -> { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.ASSET, Operation.READ, asset.getId(), asset); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + PageData filteredResult = new PageData<>(filteredAssets, + nonFilteredResult.getTotalPages(), + nonFilteredResult.getTotalElements(), + nonFilteredResult.hasNext()); + return checkNotNull(filteredResult); + } catch (Exception e) { + throw handleException(e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index d281de9009..f41e40ef89 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -37,6 +37,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; @@ -110,6 +111,8 @@ public class AuthController extends BaseController { userCredentials.setPassword(passwordEncoder.encode(newPassword)); userService.replaceUserCredentials(securityUser.getTenantId(), userCredentials); + sendEntityNotificationMsg(getTenantId(), userCredentials.getUserId(), EdgeEventActionType.CREDENTIALS_UPDATED); + eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId())); ObjectNode response = JacksonUtil.newObjectNode(); response.put("token", tokenFactory.createAccessJwtToken(securityUser).getToken()); @@ -224,6 +227,8 @@ public class AuthController extends BaseController { } } + sendEntityNotificationMsg(user.getTenantId(), user.getId(), EdgeEventActionType.CREDENTIALS_UPDATED); + JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index d9d9a232a4..89d9017fb3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; @@ -53,6 +54,10 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.edge.EdgeInfo; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AlarmId; @@ -61,6 +66,7 @@ import org.thingsboard.server.common.data.id.CustomerId; 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.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; @@ -82,7 +88,9 @@ import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; @@ -98,6 +106,7 @@ import org.thingsboard.server.dao.device.ClaimDevicesService; 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.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; @@ -106,7 +115,6 @@ import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.dao.resource.TbResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; @@ -115,14 +123,19 @@ 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.exception.ThingsboardErrorResponseHandler; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.firmware.FirmwareStateService; +import org.thingsboard.server.service.edge.EdgeNotificationService; +import org.thingsboard.server.service.edge.rpc.EdgeGrpcService; +import org.thingsboard.server.service.edge.rpc.init.SyncEdgeService; import org.thingsboard.server.service.lwm2m.LwM2MModelsRepository; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.queue.TbClusterService; +import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.AccessControlService; import org.thingsboard.server.service.security.permission.Operation; @@ -256,10 +269,25 @@ public abstract class BaseController { @Autowired protected LwM2MModelsRepository lwM2MModelsRepository; + @Autowired(required = false) + protected EdgeService edgeService; + + @Autowired(required = false) + protected EdgeNotificationService edgeNotificationService; + + @Autowired(required = false) + protected SyncEdgeService syncEdgeService; + + @Autowired(required = false) + protected EdgeGrpcService edgeGrpcService; + @Value("${server.log_controller_error_stack_trace}") @Getter private boolean logControllerErrorStackTrace; + @Value("${edges.enabled}") + @Getter + protected boolean edgesEnabled; @ExceptionHandler(ThingsboardException.class) public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) { @@ -473,6 +501,9 @@ public abstract class BaseController { case ENTITY_VIEW: checkEntityViewId(new EntityViewId(entityId.getId()), operation); return; + case EDGE: + checkEdgeId(new EdgeId(entityId.getId()), operation); + return; case WIDGETS_BUNDLE: checkWidgetsBundleId(new WidgetsBundleId(entityId.getId()), operation); return; @@ -637,6 +668,30 @@ public abstract class BaseController { } } + Edge checkEdgeId(EdgeId edgeId, Operation operation) throws ThingsboardException { + try { + validateId(edgeId, "Incorrect edgeId " + edgeId); + Edge edge = edgeService.findEdgeById(getTenantId(), edgeId); + checkNotNull(edge); + accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, edgeId, edge); + return edge; + } catch (Exception e) { + throw handleException(e, false); + } + } + + EdgeInfo checkEdgeInfoId(EdgeId edgeId, Operation operation) throws ThingsboardException { + try { + validateId(edgeId, "Incorrect edgeId " + edgeId); + EdgeInfo edge = edgeService.findEdgeInfoById(getCurrentUser().getTenantId(), edgeId); + checkNotNull(edge); + accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, edgeId, edge); + return edge; + } catch (Exception e) { + throw handleException(e, false); + } + } + DashboardInfo checkDashboardInfoId(DashboardId dashboardId, Operation operation) throws ThingsboardException { try { validateId(dashboardId, "Incorrect dashboardId " + dashboardId); @@ -658,19 +713,19 @@ public abstract class BaseController { } } - List checkComponentDescriptorsByType(ComponentType type) throws ThingsboardException { + List checkComponentDescriptorsByType(ComponentType type, RuleChainType ruleChainType) throws ThingsboardException { try { log.debug("[{}] Lookup component descriptors", type); - return componentDescriptorService.getComponents(type); + return componentDescriptorService.getComponents(type, ruleChainType); } catch (Exception e) { throw handleException(e, false); } } - List checkComponentDescriptorsByTypes(Set types) throws ThingsboardException { + List checkComponentDescriptorsByTypes(Set types, RuleChainType ruleChainType) throws ThingsboardException { try { log.debug("[{}] Lookup component descriptors", types); - return componentDescriptorService.getComponents(types); + return componentDescriptorService.getComponents(types, ruleChainType); } catch (Exception e) { throw handleException(e, false); } @@ -815,6 +870,12 @@ public abstract class BaseController { case TIMESERIES_DELETED: msgType = DataConstants.TIMESERIES_DELETED; break; + case ASSIGNED_TO_EDGE: + msgType = DataConstants.ENTITY_ASSIGNED_TO_EDGE; + break; + case UNASSIGNED_FROM_EDGE: + msgType = DataConstants.ENTITY_UNASSIGNED_FROM_EDGE; + break; } if (!StringUtils.isEmpty(msgType)) { try { @@ -844,6 +905,16 @@ public abstract class BaseController { String strTenantName = extractParameter(String.class, 1, additionalInfo); metaData.putValue("assignedToTenantId", strTenantId); metaData.putValue("assignedToTenantName", strTenantName); + } else if (actionType == ActionType.ASSIGNED_TO_EDGE) { + String strEdgeId = extractParameter(String.class, 1, additionalInfo); + String strEdgeName = extractParameter(String.class, 2, additionalInfo); + metaData.putValue("assignedEdgeId", strEdgeId); + metaData.putValue("assignedEdgeName", strEdgeName); + } else if (actionType == ActionType.UNASSIGNED_FROM_EDGE) { + String strEdgeId = extractParameter(String.class, 1, additionalInfo); + String strEdgeName = extractParameter(String.class, 2, additionalInfo); + metaData.putValue("unassignedEdgeId", strEdgeId); + metaData.putValue("unassignedEdgeName", strEdgeName); } ObjectNode entityNode; if (entity != null) { @@ -937,6 +1008,93 @@ public abstract class BaseController { return null; } + protected void sendRelationNotificationMsg(TenantId tenantId, EntityRelation relation, EdgeEventActionType action) { + try { + if (!relation.getFrom().getEntityType().equals(EntityType.EDGE) && + !relation.getTo().getEntityType().equals(EntityType.EDGE)) { + sendNotificationMsgToEdgeService(tenantId, null, null, json.writeValueAsString(relation), EdgeEventType.RELATION, action); + } + } catch (Exception e) { + log.warn("Failed to push relation to core: {}", relation, e); + } + } + + protected void sendDeleteNotificationMsg(TenantId tenantId, EntityId entityId, List edgeIds) { + if (edgeIds != null && !edgeIds.isEmpty()) { + for (EdgeId edgeId : edgeIds) { + sendNotificationMsgToEdgeService(tenantId, edgeId, entityId, null, null, EdgeEventActionType.DELETED); + } + } + } + + protected void sendEntityAssignToCustomerNotificationMsg(TenantId tenantId, EntityId entityId, CustomerId customerId, EdgeEventActionType action) { + try { + sendNotificationMsgToEdgeService(tenantId, null, entityId, json.writeValueAsString(customerId), null, action); + } catch (Exception e) { + log.warn("Failed to push assign/unassign to/from customer to core: {}", customerId, e); + } + } + + protected void sendEntityNotificationMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType action) { + sendNotificationMsgToEdgeService(tenantId, null, entityId, null, null, action); + } + + protected void sendEntityAssignToEdgeNotificationMsg(TenantId tenantId, EdgeId edgeId, EntityId entityId, EdgeEventActionType action) { + sendNotificationMsgToEdgeService(tenantId, edgeId, entityId, null, null, action); + } + + private void sendNotificationMsgToEdgeService(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action) { + if (!edgesEnabled) { + return; + } + if (type == null) { + if (entityId != null) { + type = EdgeUtils.getEdgeEventTypeByEntityType(entityId.getEntityType()); + } else { + log.trace("[{}] entity id and type are null. Ignoring this notification", tenantId); + return; + } + if (type == null) { + log.trace("[{}] edge event type is null. Ignoring this notification [{}]", tenantId, entityId); + return; + } + } + TransportProtos.EdgeNotificationMsgProto.Builder builder = TransportProtos.EdgeNotificationMsgProto.newBuilder(); + builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); + builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); + builder.setType(type.name()); + builder.setAction(action.name()); + if (entityId != null) { + builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); + builder.setEntityIdLSB(entityId.getId().getLeastSignificantBits()); + builder.setEntityType(entityId.getEntityType().name()); + } + if (edgeId != null) { + builder.setEdgeIdMSB(edgeId.getId().getMostSignificantBits()); + builder.setEdgeIdLSB(edgeId.getId().getLeastSignificantBits()); + } + if (body != null) { + builder.setBody(body); + } + TransportProtos.EdgeNotificationMsgProto msg = builder.build(); + log.trace("[{}] sending notification to edge service {}", tenantId.getId(), msg); + tbClusterService.pushMsgToCore(tenantId, entityId != null ? entityId : tenantId, + TransportProtos.ToCoreMsg.newBuilder().setEdgeNotificationMsg(msg).build(), null); + } + + protected List findRelatedEdgeIds(TenantId tenantId, EntityId entityId) { + if (!edgesEnabled) { + return null; + } + List result = null; + try { + result = edgeService.findRelatedEdgeIdsByEntityId(tenantId, entityId).get(); + } catch (Exception e) { + log.error("[{}] can't find related edge ids for entity [{}]", tenantId, entityId, e); + } + return result; + } + private void addTimeseries(ObjectNode entityNode, List timeseries) throws Exception { if (timeseries != null && !timeseries.isEmpty()) { ArrayNode result = entityNode.putArray("timeseries"); diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java index 2740bf8e49..81676374f8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import org.apache.commons.lang3.StringUtils; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -25,6 +26,7 @@ import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.queue.util.TbCoreComponent; import java.util.HashSet; @@ -51,10 +53,11 @@ public class ComponentDescriptorController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") @RequestMapping(value = "/components/{componentType}", method = RequestMethod.GET) @ResponseBody - public List getComponentDescriptorsByType(@PathVariable("componentType") String strComponentType) throws ThingsboardException { + public List getComponentDescriptorsByType(@PathVariable("componentType") String strComponentType, + @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException { checkParameter("componentType", strComponentType); try { - return checkComponentDescriptorsByType(ComponentType.valueOf(strComponentType)); + return checkComponentDescriptorsByType(ComponentType.valueOf(strComponentType), getRuleChainType(strRuleChainType)); } catch (Exception e) { throw handleException(e); } @@ -63,17 +66,28 @@ public class ComponentDescriptorController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") @RequestMapping(value = "/components", params = {"componentTypes"}, method = RequestMethod.GET) @ResponseBody - public List getComponentDescriptorsByTypes(@RequestParam("componentTypes") String[] strComponentTypes) throws ThingsboardException { + public List getComponentDescriptorsByTypes(@RequestParam("componentTypes") String[] strComponentTypes, + @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException { checkArrayParameter("componentTypes", strComponentTypes); try { Set componentTypes = new HashSet<>(); for (String strComponentType : strComponentTypes) { componentTypes.add(ComponentType.valueOf(strComponentType)); } - return checkComponentDescriptorsByTypes(componentTypes); + return checkComponentDescriptorsByTypes(componentTypes, getRuleChainType(strRuleChainType)); } catch (Exception e) { throw handleException(e); } } + private RuleChainType getRuleChainType(String strRuleChainType) { + RuleChainType ruleChainType; + if (StringUtils.isEmpty(strRuleChainType)) { + ruleChainType = RuleChainType.CORE; + } else { + ruleChainType = RuleChainType.valueOf(strRuleChainType); + } + return ruleChainType; + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index dd0312169b..484b15f076 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -31,8 +31,10 @@ import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; 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.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -40,6 +42,8 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.List; + @RestController @TbCoreComponent @RequestMapping("/api") @@ -112,6 +116,10 @@ public class CustomerController extends BaseController { savedCustomer.getId(), customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + if (customer.getId() != null) { + sendEntityNotificationMsg(savedCustomer.getTenantId(), savedCustomer.getId(), EdgeEventActionType.UPDATED); + } + return savedCustomer; } catch (Exception e) { @@ -130,12 +138,16 @@ public class CustomerController extends BaseController { try { CustomerId customerId = new CustomerId(toUUID(strCustomerId)); Customer customer = checkCustomerId(customerId, Operation.DELETE); + + List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), customerId); + customerService.deleteCustomer(getTenantId(), customerId); logEntityAction(customerId, customer, customer.getId(), ActionType.DELETED, null, strCustomerId); + sendDeleteNotificationMsg(getTenantId(), customerId, relatedEdgeIds); } catch (Exception e) { logEntityAction(emptyId(EntityType.CUSTOMER), diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 2e6efc4246..8616da8714 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -38,20 +38,26 @@ import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EdgeId; 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.common.util.JacksonUtil; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; @RestController @TbCoreComponent @@ -122,6 +128,10 @@ public class DashboardController extends BaseController { null, dashboard.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + if (dashboard.getId() != null) { + sendEntityNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), EdgeEventActionType.UPDATED); + } + return savedDashboard; } catch (Exception e) { logEntityAction(emptyId(EntityType.DASHBOARD), dashboard, @@ -139,12 +149,16 @@ public class DashboardController extends BaseController { try { DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); Dashboard dashboard = checkDashboardId(dashboardId, Operation.DELETE); + + List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), dashboardId); + dashboardService.deleteDashboard(getCurrentUser().getTenantId(), dashboardId); logEntityAction(dashboardId, dashboard, null, ActionType.DELETED, null, strDashboardId); + sendDeleteNotificationMsg(getTenantId(), dashboardId, relatedEdgeIds); } catch (Exception e) { logEntityAction(emptyId(EntityType.DASHBOARD), @@ -176,6 +190,7 @@ public class DashboardController extends BaseController { customerId, ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, strCustomerId, customer.getName()); + sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); return savedDashboard; } catch (Exception e) { @@ -207,6 +222,8 @@ public class DashboardController extends BaseController { customerId, ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, customer.getId().toString(), customer.getName()); + sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); + return savedDashboard; } catch (Exception e) { @@ -262,6 +279,7 @@ public class DashboardController extends BaseController { logEntityAction(dashboardId, savedDashboard, customerId, ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, customerId.toString(), customerInfo.getTitle()); + sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); } for (CustomerId customerId : removedCustomerIds) { ShortCustomerInfo customerInfo = dashboard.getAssignedCustomerInfo(customerId); @@ -269,7 +287,7 @@ public class DashboardController extends BaseController { logEntityAction(dashboardId, dashboard, customerId, ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, customerId.toString(), customerInfo.getTitle()); - + sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); } return savedDashboard; } @@ -313,6 +331,7 @@ public class DashboardController extends BaseController { logEntityAction(dashboardId, savedDashboard, customerId, ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, customerId.toString(), customerInfo.getTitle()); + sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); } return savedDashboard; } @@ -356,7 +375,7 @@ public class DashboardController extends BaseController { logEntityAction(dashboardId, dashboard, customerId, ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, customerId.toString(), customerInfo.getTitle()); - + sendEntityAssignToCustomerNotificationMsg(savedDashboard.getTenantId(), savedDashboard.getId(), customerId, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); } return savedDashboard; } @@ -578,4 +597,106 @@ public class DashboardController extends BaseController { } catch (Exception e) {} return null; } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}", method = RequestMethod.POST) + @ResponseBody + public Dashboard assignDashboardToEdge(@PathVariable("edgeId") String strEdgeId, + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { + checkParameter("edgeId", strEdgeId); + checkParameter(DASHBOARD_ID, strDashboardId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + checkDashboardId(dashboardId, Operation.ASSIGN_TO_EDGE); + + Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToEdge(getCurrentUser().getTenantId(), dashboardId, edgeId)); + + logEntityAction(dashboardId, savedDashboard, + null, + ActionType.ASSIGNED_TO_EDGE, null, strDashboardId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedDashboard.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); + + return savedDashboard; + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.DASHBOARD), null, + null, + ActionType.ASSIGNED_TO_EDGE, e, strDashboardId, strEdgeId); + + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}", method = RequestMethod.DELETE) + @ResponseBody + public Dashboard unassignDashboardFromEdge(@PathVariable("edgeId") String strEdgeId, + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { + checkParameter("edgeId", strEdgeId); + checkParameter(DASHBOARD_ID, strDashboardId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + Dashboard dashboard = checkDashboardId(dashboardId, Operation.UNASSIGN_FROM_EDGE); + + Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromEdge(getCurrentUser().getTenantId(), dashboardId, edgeId)); + + logEntityAction(dashboardId, dashboard, + null, + ActionType.UNASSIGNED_FROM_EDGE, null, strDashboardId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedDashboard.getId(), EdgeEventActionType.UNASSIGNED_FROM_EDGE); + + return savedDashboard; + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.DASHBOARD), null, + null, + ActionType.UNASSIGNED_FROM_EDGE, e, strDashboardId, strEdgeId); + + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edge/{edgeId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getEdgeDashboards( + @PathVariable("edgeId") String strEdgeId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime) throws ThingsboardException { + checkParameter("edgeId", strEdgeId); + try { + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + PageData nonFilteredResult = dashboardService.findDashboardsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); + List filteredDashboards = nonFilteredResult.getData().stream().filter(dashboardInfo -> { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.DASHBOARD, Operation.READ, dashboardInfo.getId(), dashboardInfo); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + PageData filteredResult = new PageData<>(filteredDashboards, + nonFilteredResult.getTotalPages(), + nonFilteredResult.getTotalElements(), + nonFilteredResult.hasNext()); + return checkNotNull(filteredResult); + } catch (Exception e) { + throw handleException(e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 0e4b758593..508ca124a3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -32,6 +32,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg; +import org.thingsboard.rule.engine.api.msg.DeviceEdgeUpdateMsg; import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.common.data.ClaimRequest; import org.thingsboard.server.common.data.Customer; @@ -43,14 +44,18 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.device.DeviceSearchQuery; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; 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.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.msg.TbMsg; @@ -73,6 +78,8 @@ import java.util.List; import java.util.Objects; import java.util.stream.Collectors; +import static org.thingsboard.server.controller.EdgeController.EDGE_ID; + @RestController @TbCoreComponent @RequestMapping("/api") @@ -138,6 +145,10 @@ public class DeviceController extends BaseController { savedDevice.getId(), savedDevice.getName(), savedDevice.getType()), null); tbClusterService.onEntityStateChange(savedDevice.getTenantId(), savedDevice.getId(), created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + if (device.getId() != null) { + sendEntityNotificationMsg(savedDevice.getTenantId(), savedDevice.getId(), EdgeEventActionType.UPDATED); + } + logEntityAction(savedDevice.getId(), savedDevice, savedDevice.getCustomerId(), device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); @@ -170,6 +181,9 @@ public class DeviceController extends BaseController { try { DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); Device device = checkDeviceId(deviceId, Operation.DELETE); + + List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), deviceId); + deviceService.deleteDevice(getCurrentUser().getTenantId(), deviceId); tbClusterService.onDeviceDeleted(device, null); @@ -179,6 +193,8 @@ public class DeviceController extends BaseController { device.getCustomerId(), ActionType.DELETED, null, strDeviceId); + sendDeleteNotificationMsg(getTenantId(), deviceId, relatedEdgeIds); + deviceStateService.onDeviceDeleted(device); } catch (Exception e) { logEntityAction(emptyId(EntityType.DEVICE), @@ -209,6 +225,9 @@ public class DeviceController extends BaseController { savedDevice.getCustomerId(), ActionType.ASSIGNED_TO_CUSTOMER, null, strDeviceId, strCustomerId, customer.getName()); + sendEntityAssignToCustomerNotificationMsg(savedDevice.getTenantId(), savedDevice.getId(), + customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); + return savedDevice; } catch (Exception e) { logEntityAction(emptyId(EntityType.DEVICE), null, @@ -237,6 +256,9 @@ public class DeviceController extends BaseController { device.getCustomerId(), ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDeviceId, customer.getId().toString(), customer.getName()); + sendEntityAssignToCustomerNotificationMsg(savedDevice.getTenantId(), savedDevice.getId(), + customer.getId(), EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); + return savedDevice; } catch (Exception e) { logEntityAction(emptyId(EntityType.DEVICE), null, @@ -300,6 +322,9 @@ public class DeviceController extends BaseController { Device device = checkDeviceId(deviceCredentials.getDeviceId(), Operation.WRITE_CREDENTIALS); DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(getCurrentUser().getTenantId(), deviceCredentials)); tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId(), result), null); + + sendEntityNotificationMsg(getTenantId(), device.getId(), EdgeEventActionType.CREDENTIALS_UPDATED); + logEntityAction(device.getId(), device, device.getCustomerId(), ActionType.CREDENTIALS_UPDATED, null, deviceCredentials); @@ -651,4 +676,115 @@ public class DeviceController extends BaseController { metaData.putValue("assignedFromTenantName", tenant.getName()); return metaData; } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/device/{deviceId}", method = RequestMethod.POST) + @ResponseBody + public Device assignDeviceToEdge(@PathVariable(EDGE_ID) String strEdgeId, + @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + checkParameter(DEVICE_ID, strDeviceId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + checkDeviceId(deviceId, Operation.ASSIGN_TO_EDGE); + + Device savedDevice = checkNotNull(deviceService.assignDeviceToEdge(getCurrentUser().getTenantId(), deviceId, edgeId)); + + tbClusterService.pushMsgToCore(new DeviceEdgeUpdateMsg(savedDevice.getTenantId(), + savedDevice.getId(), edgeId), null); + + logEntityAction(deviceId, savedDevice, + savedDevice.getCustomerId(), + ActionType.ASSIGNED_TO_EDGE, null, strDeviceId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedDevice.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); + + return savedDevice; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.DEVICE), null, + null, + ActionType.ASSIGNED_TO_EDGE, e, strDeviceId, strEdgeId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/device/{deviceId}", method = RequestMethod.DELETE) + @ResponseBody + public Device unassignDeviceFromEdge(@PathVariable(EDGE_ID) String strEdgeId, + @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + checkParameter(DEVICE_ID, strDeviceId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + Device device = checkDeviceId(deviceId, Operation.UNASSIGN_FROM_EDGE); + + Device savedDevice = checkNotNull(deviceService.unassignDeviceFromEdge(getCurrentUser().getTenantId(), deviceId, edgeId)); + + tbClusterService.pushMsgToCore(new DeviceEdgeUpdateMsg(savedDevice.getTenantId(), + savedDevice.getId(), null), null); + + logEntityAction(deviceId, device, + device.getCustomerId(), + ActionType.UNASSIGNED_FROM_EDGE, null, strDeviceId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedDevice.getId(), EdgeEventActionType.UNASSIGNED_FROM_EDGE); + + return savedDevice; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.DEVICE), null, + null, + ActionType.UNASSIGNED_FROM_EDGE, e, strDeviceId, strEdgeId); + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edge/{edgeId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getEdgeDevices( + @PathVariable(EDGE_ID) String strEdgeId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + PageData nonFilteredResult; + if (type != null && type.trim().length() > 0) { + nonFilteredResult = deviceService.findDevicesByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink); + } else { + nonFilteredResult = deviceService.findDevicesByTenantIdAndEdgeId(tenantId, edgeId, pageLink); + } + List filteredDevices = nonFilteredResult.getData().stream().filter(device -> { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, Operation.READ, device.getId(), device); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + PageData filteredResult = new PageData<>(filteredDevices, + nonFilteredResult.getTotalPages(), + nonFilteredResult.getTotalElements(), + nonFilteredResult.hasNext()); + return checkNotNull(filteredResult); + } catch (Exception e) { + throw handleException(e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java index c72ae1870d..225c79b79a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.page.PageData; @@ -166,7 +167,8 @@ public class DeviceProfileController extends BaseController { if (isFirmwareChanged) { firmwareStateService.update(savedDeviceProfile); } - + sendEntityNotificationMsg(getTenantId(), savedDeviceProfile.getId(), + deviceProfile.getId() == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED); return savedDeviceProfile; } catch (Exception e) { logEntityAction(emptyId(EntityType.DEVICE_PROFILE), deviceProfile, @@ -192,6 +194,7 @@ public class DeviceProfileController extends BaseController { null, ActionType.DELETED, null, strDeviceProfileId); + sendEntityNotificationMsg(getTenantId(), deviceProfile.getId(), EdgeEventActionType.DELETED); } catch (Exception e) { logEntityAction(emptyId(EntityType.DEVICE_PROFILE), null, diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java new file mode 100644 index 0000000000..9c196c6bd2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -0,0 +1,593 @@ +/** + * Copyright © 2016-2021 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.google.common.util.concurrent.ListenableFuture; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeInfo; +import org.thingsboard.server.common.data.edge.EdgeSearchQuery; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; +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.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.EdgeGrpcSession; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +public class EdgeController extends BaseController { + + public static final String EDGE_ID = "edgeId"; + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edges/enabled", method = RequestMethod.GET) + @ResponseBody + public boolean isEdgesSupportEnabled() { + return edgesEnabled; + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.GET) + @ResponseBody + public Edge getEdgeById(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + if (Authority.CUSTOMER_USER.equals(getCurrentUser().getAuthority())) { + cleanUpSensitiveData(edge); + } + return edge; + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edge/info/{edgeId}", method = RequestMethod.GET) + @ResponseBody + public EdgeInfo getEdgeInfoById(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + EdgeInfo edgeInfo = checkEdgeInfoId(edgeId, Operation.READ); + if (Authority.CUSTOMER_USER.equals(getCurrentUser().getAuthority())) { + cleanUpSensitiveData(edgeInfo); + } + return edgeInfo; + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge", method = RequestMethod.POST) + @ResponseBody + public Edge saveEdge(@RequestBody Edge edge) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + edge.setTenantId(tenantId); + boolean created = edge.getId() == null; + + RuleChain edgeTemplateRootRuleChain = null; + if (created) { + edgeTemplateRootRuleChain = ruleChainService.getEdgeTemplateRootRuleChain(tenantId); + if (edgeTemplateRootRuleChain == null) { + throw new DataValidationException("Root edge rule chain is not available!"); + } + } + + Operation operation = created ? Operation.CREATE : Operation.WRITE; + + accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, + edge.getId(), edge); + + Edge savedEdge = checkNotNull(edgeService.saveEdge(edge)); + + if (created) { + ruleChainService.assignRuleChainToEdge(tenantId, edgeTemplateRootRuleChain.getId(), savedEdge.getId()); + edgeNotificationService.setEdgeRootRuleChain(tenantId, savedEdge, edgeTemplateRootRuleChain.getId()); + edgeService.assignDefaultRuleChainsToEdge(tenantId, savedEdge.getId()); + } + + tbClusterService.onEntityStateChange(savedEdge.getTenantId(), savedEdge.getId(), + created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + + logEntityAction(savedEdge.getId(), savedEdge, null, created ? ActionType.ADDED : ActionType.UPDATED, null); + return savedEdge; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.EDGE), edge, + null, edge.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.DELETE) + @ResponseStatus(value = HttpStatus.OK) + public void deleteEdge(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.DELETE); + edgeService.deleteEdge(getTenantId(), edgeId); + + tbClusterService.onEntityStateChange(getTenantId(), edgeId, + ComponentLifecycleEvent.DELETED); + + logEntityAction(edgeId, edge, + null, + ActionType.DELETED, null, strEdgeId); + + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.EDGE), + null, + null, + ActionType.DELETED, e, strEdgeId); + + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edges", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getEdges(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(edgeService.findEdgesByTenantId(tenantId, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/{customerId}/edge/{edgeId}", method = RequestMethod.POST) + @ResponseBody + public Edge assignEdgeToCustomer(@PathVariable("customerId") String strCustomerId, + @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + checkParameter("customerId", strCustomerId); + checkParameter(EDGE_ID, strEdgeId); + try { + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + Customer customer = checkCustomerId(customerId, Operation.READ); + + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.ASSIGN_TO_CUSTOMER); + + Edge savedEdge = checkNotNull(edgeService.assignEdgeToCustomer(getCurrentUser().getTenantId(), edgeId, customerId)); + + tbClusterService.onEntityStateChange(getTenantId(), edgeId, + ComponentLifecycleEvent.UPDATED); + + logEntityAction(edgeId, savedEdge, + savedEdge.getCustomerId(), + ActionType.ASSIGNED_TO_CUSTOMER, null, strEdgeId, strCustomerId, customer.getName()); + + sendEntityAssignToCustomerNotificationMsg(savedEdge.getTenantId(), savedEdge.getId(), + customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); + + return savedEdge; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.EDGE), null, + null, + ActionType.ASSIGNED_TO_CUSTOMER, e, strEdgeId, strCustomerId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/edge/{edgeId}", method = RequestMethod.DELETE) + @ResponseBody + public Edge unassignEdgeFromCustomer(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.UNASSIGN_FROM_CUSTOMER); + if (edge.getCustomerId() == null || edge.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + throw new IncorrectParameterException("Edge isn't assigned to any customer!"); + } + Customer customer = checkCustomerId(edge.getCustomerId(), Operation.READ); + + Edge savedEdge = checkNotNull(edgeService.unassignEdgeFromCustomer(getCurrentUser().getTenantId(), edgeId)); + + tbClusterService.onEntityStateChange(getTenantId(), edgeId, + ComponentLifecycleEvent.UPDATED); + + logEntityAction(edgeId, edge, + edge.getCustomerId(), + ActionType.UNASSIGNED_FROM_CUSTOMER, null, strEdgeId, customer.getId().toString(), customer.getName()); + + sendEntityAssignToCustomerNotificationMsg(savedEdge.getTenantId(), savedEdge.getId(), + customer.getId(), EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); + + return savedEdge; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.EDGE), null, + null, + ActionType.UNASSIGNED_FROM_CUSTOMER, e, strEdgeId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/public/edge/{edgeId}", method = RequestMethod.POST) + @ResponseBody + public Edge assignEdgeToPublicCustomer(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.ASSIGN_TO_CUSTOMER); + Customer publicCustomer = customerService.findOrCreatePublicCustomer(edge.getTenantId()); + Edge savedEdge = checkNotNull(edgeService.assignEdgeToCustomer(getCurrentUser().getTenantId(), edgeId, publicCustomer.getId())); + + tbClusterService.onEntityStateChange(getTenantId(), edgeId, + ComponentLifecycleEvent.UPDATED); + + logEntityAction(edgeId, savedEdge, + savedEdge.getCustomerId(), + ActionType.ASSIGNED_TO_CUSTOMER, null, strEdgeId, publicCustomer.getId().toString(), publicCustomer.getName()); + + return savedEdge; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.EDGE), null, + null, + ActionType.ASSIGNED_TO_CUSTOMER, e, strEdgeId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/tenant/edges", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getTenantEdges( + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(edgeService.findEdgesByTenantIdAndType(tenantId, type, pageLink)); + } else { + return checkNotNull(edgeService.findEdgesByTenantId(tenantId, pageLink)); + } + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/tenant/edgeInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getTenantEdgeInfos( + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (type != null && type.trim().length() > 0) { + return checkNotNull(edgeService.findEdgeInfosByTenantIdAndType(tenantId, type, pageLink)); + } else { + return checkNotNull(edgeService.findEdgeInfosByTenantId(tenantId, pageLink)); + } + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/tenant/edges", params = {"edgeName"}, method = RequestMethod.GET) + @ResponseBody + public Edge getTenantEdge(@RequestParam String edgeName) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(edgeService.findEdgeByTenantIdAndName(tenantId, edgeName)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/{ruleChainId}/root", method = RequestMethod.POST) + @ResponseBody + public Edge setRootRuleChain(@PathVariable(EDGE_ID) String strEdgeId, + @PathVariable("ruleChainId") String strRuleChainId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + checkParameter("ruleChainId", strRuleChainId); + try { + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); + checkRuleChain(ruleChainId, Operation.WRITE); + + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.WRITE); + accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, Operation.WRITE, + edge.getId(), edge); + + Edge updatedEdge = edgeNotificationService.setEdgeRootRuleChain(getTenantId(), edge, ruleChainId); + + tbClusterService.onEntityStateChange(updatedEdge.getTenantId(), updatedEdge.getId(), ComponentLifecycleEvent.UPDATED); + + logEntityAction(updatedEdge.getId(), updatedEdge, null, ActionType.UPDATED, null); + + return updatedEdge; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.EDGE), + null, + null, + ActionType.UPDATED, e, strEdgeId); + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/customer/{customerId}/edges", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getCustomerEdges( + @PathVariable("customerId") String strCustomerId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + checkParameter("customerId", strCustomerId); + try { + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + PageData result; + if (type != null && type.trim().length() > 0) { + result = edgeService.findEdgesByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink); + } else { + result = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + } + if (Authority.CUSTOMER_USER.equals(user.getAuthority())) { + for (Edge edge : result.getData()) { + cleanUpSensitiveData(edge); + } + } + return checkNotNull(result); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/customer/{customerId}/edgeInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getCustomerEdgeInfos( + @PathVariable("customerId") String strCustomerId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + checkParameter("customerId", strCustomerId); + try { + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + PageData result; + if (type != null && type.trim().length() > 0) { + result = edgeService.findEdgeInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink); + } else { + result = edgeService.findEdgeInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink); + } + if (Authority.CUSTOMER_USER.equals(user.getAuthority())) { + for (Edge edge : result.getData()) { + cleanUpSensitiveData(edge); + } + } + return checkNotNull(result); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edges", params = {"edgeIds"}, method = RequestMethod.GET) + @ResponseBody + public List getEdgesByIds( + @RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException { + checkArrayParameter("edgeIds", strEdgeIds); + try { + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + CustomerId customerId = user.getCustomerId(); + List edgeIds = new ArrayList<>(); + for (String strEdgeId : strEdgeIds) { + edgeIds.add(new EdgeId(toUUID(strEdgeId))); + } + ListenableFuture> edgesFuture; + if (customerId == null || customerId.isNullUid()) { + edgesFuture = edgeService.findEdgesByTenantIdAndIdsAsync(tenantId, edgeIds); + } else { + edgesFuture = edgeService.findEdgesByTenantIdCustomerIdAndIdsAsync(tenantId, customerId, edgeIds); + } + List edges = edgesFuture.get(); + if (Authority.CUSTOMER_USER.equals(user.getAuthority())) { + for (Edge edge : edges) { + cleanUpSensitiveData(edge); + } + } + return checkNotNull(edges); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edges", method = RequestMethod.POST) + @ResponseBody + public List findByQuery(@RequestBody EdgeSearchQuery query) throws ThingsboardException { + checkNotNull(query); + checkNotNull(query.getParameters()); + checkNotNull(query.getEdgeTypes()); + checkEntityId(query.getParameters().getEntityId(), Operation.READ); + try { + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + List edges = checkNotNull(edgeService.findEdgesByQuery(tenantId, query).get()); + edges = edges.stream().filter(edge -> { + try { + accessControlService.checkPermission(user, Resource.EDGE, Operation.READ, edge.getId(), edge); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + if (Authority.CUSTOMER_USER.equals(user.getAuthority())) { + for (Edge edge : edges) { + cleanUpSensitiveData(edge); + } + } + return edges; + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edge/types", method = RequestMethod.GET) + @ResponseBody + public List getEdgeTypes() throws ThingsboardException { + try { + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + ListenableFuture> edgeTypes = edgeService.findEdgeTypesByTenantId(tenantId); + return checkNotNull(edgeTypes.get()); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/sync/{edgeId}", method = RequestMethod.POST) + public void syncEdge(@PathVariable("edgeId") String strEdgeId) throws ThingsboardException { + checkParameter("edgeId", strEdgeId); + try { + if (isEdgesEnabled()) { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + edgeId = checkNotNull(edgeId); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + EdgeGrpcSession session = edgeGrpcService.getEdgeGrpcSessionById(tenantId, edgeId); + Edge edge = session.getEdge(); + syncEdgeService.sync(tenantId, edge); + } else { + throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); + } + } catch (Exception e) { + throw handleException(e); + } + } + + @RequestMapping(value = "/license/checkInstance", method = RequestMethod.POST) + @ResponseBody + public Object checkInstance(@RequestBody Object request) throws ThingsboardException { + try { + return edgeService.checkInstance(request); + } catch (Exception e) { + throw new ThingsboardException(e, ThingsboardErrorCode.SUBSCRIPTION_VIOLATION); + } + } + + @RequestMapping(value = "/license/activateInstance", params = {"licenseSecret", "releaseDate"}, method = RequestMethod.POST) + @ResponseBody + public Object activateInstance(@RequestParam String licenseSecret, + @RequestParam String releaseDate) throws ThingsboardException { + try { + return edgeService.activateInstance(licenseSecret, releaseDate); + } catch (Exception e) { + throw new ThingsboardException(e, ThingsboardErrorCode.SUBSCRIPTION_VIOLATION); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/missingToRelatedRuleChains/{edgeId}", method = RequestMethod.GET) + @ResponseBody + public String findMissingToRelatedRuleChains(@PathVariable("edgeId") String strEdgeId) throws ThingsboardException { + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + edgeId = checkNotNull(edgeId); + SecurityUser user = getCurrentUser(); + TenantId tenantId = user.getTenantId(); + return edgeService.findMissingToRelatedRuleChains(tenantId, edgeId); + } catch (Exception e) { + throw handleException(e); + } + } + + private void cleanUpSensitiveData(Edge edge) { + edge.setEdgeLicenseKey(null); + edge.setRoutingKey(null); + edge.setSecret(null); + edge.setCloudEndpoint(null); + edge.setRootRuleChainId(null); + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java new file mode 100644 index 0000000000..22191efddf --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java @@ -0,0 +1,71 @@ +/** + * Copyright © 2016-2021 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 lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.dao.edge.EdgeEventService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.permission.Operation; + +@Slf4j +@RestController +@TbCoreComponent +@RequestMapping("/api") +public class EdgeEventController extends BaseController { + + @Autowired + private EdgeEventService edgeEventService; + + public static final String EDGE_ID = "edgeId"; + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/events", method = RequestMethod.GET) + @ResponseBody + public PageData getEdgeEvents( + @PathVariable(EDGE_ID) String strEdgeId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + return checkNotNull(edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, false)); + } catch (Exception e) { + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java index 357036e4f4..3f072b77f0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java @@ -25,6 +25,7 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; @@ -63,10 +64,13 @@ public class EntityRelationController extends BaseController { relation.setTypeGroup(RelationTypeGroup.COMMON); } relationService.saveRelation(getTenantId(), relation); + logEntityAction(relation.getFrom(), null, getCurrentUser().getCustomerId(), ActionType.RELATION_ADD_OR_UPDATE, null, relation); logEntityAction(relation.getTo(), null, getCurrentUser().getCustomerId(), ActionType.RELATION_ADD_OR_UPDATE, null, relation); + + sendRelationNotificationMsg(getTenantId(), relation, EdgeEventActionType.RELATION_ADD_OR_UPDATE); } catch (Exception e) { logEntityAction(relation.getFrom(), null, getCurrentUser().getCustomerId(), ActionType.RELATION_ADD_OR_UPDATE, e, relation); @@ -104,6 +108,8 @@ public class EntityRelationController extends BaseController { ActionType.RELATION_DELETED, null, relation); logEntityAction(relation.getTo(), null, getCurrentUser().getCustomerId(), ActionType.RELATION_DELETED, null, relation); + + sendRelationNotificationMsg(getTenantId(), relation, EdgeEventActionType.RELATION_DELETED); } catch (Exception e) { logEntityAction(relation.getFrom(), null, getCurrentUser().getCustomerId(), ActionType.RELATION_DELETED, e, relation); diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java index 6250975b01..5e9103a66a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java @@ -32,11 +32,20 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.server.common.data.*; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.EntityViewInfo; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; @@ -47,6 +56,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -65,6 +75,7 @@ import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.thingsboard.server.controller.CustomerController.CUSTOMER_ID; +import static org.thingsboard.server.controller.EdgeController.EDGE_ID; /** * Created by Victor Basanets on 8/28/2017. @@ -150,6 +161,11 @@ public class EntityViewController extends BaseController { logEntityAction(savedEntityView.getId(), savedEntityView, null, entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + + if (entityView.getId() != null) { + sendEntityNotificationMsg(savedEntityView.getTenantId(), savedEntityView.getId(), EdgeEventActionType.UPDATED); + } + return savedEntityView; } catch (Exception e) { logEntityAction(emptyId(EntityType.ENTITY_VIEW), entityView, null, @@ -243,7 +259,7 @@ public class EntityViewController extends BaseController { private ListenableFuture> copyLatestFromEntityToEntityView(EntityView entityView, SecurityUser user) { EntityViewId entityId = entityView.getId(); List keys = entityView.getKeys() != null && entityView.getKeys().getTimeseries() != null ? - entityView.getKeys().getTimeseries() : Collections.emptyList(); + entityView.getKeys().getTimeseries() : Collections.emptyList(); long startTs = entityView.getStartTimeMs(); long endTs = entityView.getEndTimeMs() == 0 ? Long.MAX_VALUE : entityView.getEndTimeMs(); ListenableFuture> keysFuture; @@ -345,9 +361,14 @@ public class EntityViewController extends BaseController { try { EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); EntityView entityView = checkEntityViewId(entityViewId, Operation.DELETE); + + List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), entityViewId); + entityViewService.deleteEntityView(getTenantId(), entityViewId); logEntityAction(entityViewId, entityView, entityView.getCustomerId(), ActionType.DELETED, null, strEntityViewId); + + sendDeleteNotificationMsg(getTenantId(), entityViewId, relatedEdgeIds); } catch (Exception e) { logEntityAction(emptyId(EntityType.ENTITY_VIEW), null, @@ -388,6 +409,10 @@ public class EntityViewController extends BaseController { logEntityAction(entityViewId, savedEntityView, savedEntityView.getCustomerId(), ActionType.ASSIGNED_TO_CUSTOMER, null, strEntityViewId, strCustomerId, customer.getName()); + + sendEntityAssignToCustomerNotificationMsg(savedEntityView.getTenantId(), savedEntityView.getId(), + customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER); + return savedEntityView; } catch (Exception e) { logEntityAction(emptyId(EntityType.ENTITY_VIEW), null, @@ -414,6 +439,9 @@ public class EntityViewController extends BaseController { entityView.getCustomerId(), ActionType.UNASSIGNED_FROM_CUSTOMER, null, strEntityViewId, customer.getId().toString(), customer.getName()); + sendEntityAssignToCustomerNotificationMsg(savedEntityView.getTenantId(), savedEntityView.getId(), + customer.getId(), EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER); + return savedEntityView; } catch (Exception e) { logEntityAction(emptyId(EntityType.ENTITY_VIEW), null, @@ -585,4 +613,107 @@ public class EntityViewController extends BaseController { throw handleException(e); } } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/entityView/{entityViewId}", method = RequestMethod.POST) + @ResponseBody + public EntityView assignEntityViewToEdge(@PathVariable(EDGE_ID) String strEdgeId, + @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + checkParameter(ENTITY_VIEW_ID, strEntityViewId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + + EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); + checkEntityViewId(entityViewId, Operation.ASSIGN_TO_EDGE); + + EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToEdge(getTenantId(), entityViewId, edgeId)); + logEntityAction(entityViewId, savedEntityView, + savedEntityView.getCustomerId(), + ActionType.ASSIGNED_TO_EDGE, null, strEntityViewId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedEntityView.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); + + return savedEntityView; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.ENTITY_VIEW), null, + null, + ActionType.ASSIGNED_TO_EDGE, e, strEntityViewId, strEdgeId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/entityView/{entityViewId}", method = RequestMethod.DELETE) + @ResponseBody + public EntityView unassignEntityViewFromEdge(@PathVariable(EDGE_ID) String strEdgeId, + @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + checkParameter(ENTITY_VIEW_ID, strEntityViewId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + + EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); + EntityView entityView = checkEntityViewId(entityViewId, Operation.UNASSIGN_FROM_EDGE); + + EntityView savedEntityView = checkNotNull(entityViewService.unassignEntityViewFromEdge(getTenantId(), entityViewId, edgeId)); + logEntityAction(entityViewId, entityView, + entityView.getCustomerId(), + ActionType.UNASSIGNED_FROM_EDGE, null, strEntityViewId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedEntityView.getId(), EdgeEventActionType.UNASSIGNED_FROM_EDGE); + + return savedEntityView; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.ENTITY_VIEW), null, + null, + ActionType.UNASSIGNED_FROM_EDGE, e, strEntityViewId, strEdgeId); + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/edge/{edgeId}/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getEdgeEntityViews( + @PathVariable(EDGE_ID) String strEdgeId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime) throws ThingsboardException { + checkParameter(EDGE_ID, strEdgeId); + try { + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + PageData nonFilteredResult; + if (type != null && type.trim().length() > 0) { + nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink); + } else { + nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); + } + List filteredEntityViews = nonFilteredResult.getData().stream().filter(entityView -> { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.READ, entityView.getId(), entityView); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + PageData filteredResult = new PageData<>(filteredEntityViews, + nonFilteredResult.getTotalPages(), + nonFilteredResult.getTotalElements(), + nonFilteredResult.hasNext()); + return checkNotNull(filteredResult); + } catch (Exception e) { + throw handleException(e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java index ac9e5a3ca2..7e49ba55d4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EventController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java @@ -18,12 +18,14 @@ package org.thingsboard.server.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.event.EventFilter; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -31,6 +33,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; @@ -101,4 +104,38 @@ public class EventController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/events/{entityType}/{entityId}", method = RequestMethod.POST) + @ResponseBody + public PageData getEvents( + @PathVariable("entityType") String strEntityType, + @PathVariable("entityId") String strEntityId, + @RequestParam("tenantId") String strTenantId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestBody EventFilter eventFilter, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime) throws ThingsboardException { + checkParameter("EntityId", strEntityId); + checkParameter("EntityType", strEntityType); + try { + TenantId tenantId = new TenantId(toUUID(strTenantId)); + + EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); + checkEntityId(entityId, Operation.READ); + + if(sortProperty != null && sortProperty.equals("createdTime") && eventFilter.hasFilterForJsonBody()) { + sortProperty = ModelConstants.CREATED_TIME_PROPERTY; + } + + TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + return checkNotNull(eventService.findEventsByFilter(tenantId, entityId, eventFilter, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index c36d328dd3..d5fcc2706f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -42,7 +42,10 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Event; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -54,6 +57,7 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainData; import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; @@ -138,13 +142,21 @@ public class RuleChainController extends BaseController { RuleChain savedRuleChain = checkNotNull(ruleChainService.saveRuleChain(ruleChain)); - tbClusterService.onEntityStateChange(ruleChain.getTenantId(), savedRuleChain.getId(), - created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + if (RuleChainType.CORE.equals(savedRuleChain.getType())) { + tbClusterService.onEntityStateChange(ruleChain.getTenantId(), savedRuleChain.getId(), + created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + } logEntityAction(savedRuleChain.getId(), savedRuleChain, null, created ? ActionType.ADDED : ActionType.UPDATED, null); + if (RuleChainType.EDGE.equals(savedRuleChain.getType())) { + if (!created) { + sendEntityNotificationMsg(savedRuleChain.getTenantId(), savedRuleChain.getId(), EdgeEventActionType.UPDATED); + } + } + return savedRuleChain; } catch (Exception e) { @@ -232,14 +244,22 @@ public class RuleChainController extends BaseController { } RuleChain ruleChain = checkRuleChain(ruleChainMetaData.getRuleChainId(), Operation.WRITE); - RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData)); + checkNotNull(ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData) ? true : null); + RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.loadRuleChainMetaData(tenantId, ruleChainMetaData.getRuleChainId())); - tbClusterService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.UPDATED); + if (RuleChainType.CORE.equals(ruleChain.getType())) { + tbClusterService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.UPDATED); + } logEntityAction(ruleChain.getId(), ruleChain, null, ActionType.UPDATED, null, ruleChainMetaData); + if (RuleChainType.EDGE.equals(ruleChain.getType())) { + sendEntityNotificationMsg(ruleChain.getTenantId(), + ruleChain.getId(), EdgeEventActionType.UPDATED); + } + return savedRuleChainMetaData; } catch (Exception e) { @@ -256,13 +276,18 @@ public class RuleChainController extends BaseController { public PageData getRuleChains( @RequestParam int pageSize, @RequestParam int page, + @RequestParam(value = "type", required = false) String typeStr, @RequestParam(required = false) String textSearch, @RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return checkNotNull(ruleChainService.findTenantRuleChains(tenantId, pageLink)); + RuleChainType type = RuleChainType.CORE; + if (typeStr != null && typeStr.trim().length() > 0) { + type = RuleChainType.valueOf(typeStr); + } + return checkNotNull(ruleChainService.findTenantRuleChainsByType(tenantId, type, pageLink)); } catch (Exception e) { throw handleException(e); } @@ -281,19 +306,30 @@ public class RuleChainController extends BaseController { Set referencingRuleChainIds = referencingRuleNodes.stream().map(RuleNode::getRuleChainId).collect(Collectors.toSet()); + List relatedEdgeIds = null; + if (RuleChainType.EDGE.equals(ruleChain.getType())) { + relatedEdgeIds = findRelatedEdgeIds(getTenantId(), ruleChainId); + } + ruleChainService.deleteRuleChainById(getTenantId(), ruleChainId); referencingRuleChainIds.remove(ruleChain.getId()); - referencingRuleChainIds.forEach(referencingRuleChainId -> - tbClusterService.onEntityStateChange(ruleChain.getTenantId(), referencingRuleChainId, ComponentLifecycleEvent.UPDATED)); + if (RuleChainType.CORE.equals(ruleChain.getType())) { + referencingRuleChainIds.forEach(referencingRuleChainId -> + tbClusterService.onEntityStateChange(ruleChain.getTenantId(), referencingRuleChainId, ComponentLifecycleEvent.UPDATED)); - tbClusterService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.DELETED); + tbClusterService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.DELETED); + } logEntityAction(ruleChainId, ruleChain, null, ActionType.DELETED, null, strRuleChainId); + if (RuleChainType.EDGE.equals(ruleChain.getType())) { + sendDeleteNotificationMsg(ruleChain.getTenantId(), ruleChain.getId(), relatedEdgeIds); + } + } catch (Exception e) { logEntityAction(emptyId(EntityType.RULE_CHAIN), null, @@ -411,7 +447,7 @@ public class RuleChainController extends BaseController { public void importRuleChains(@RequestBody RuleChainData ruleChainData, @RequestParam(required = false, defaultValue = "false") boolean overwrite) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); - List importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite); + List importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, RuleChainType.CORE, overwrite); if (!CollectionUtils.isEmpty(importResults)) { for (RuleChainImportResult importResult : importResults) { tbClusterService.onEntityStateChange(importResult.getTenantId(), importResult.getRuleChainId(), importResult.getLifecycleEvent()); @@ -452,5 +488,160 @@ public class RuleChainController extends BaseController { return msgData; } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/ruleChain/{ruleChainId}", method = RequestMethod.POST) + @ResponseBody + public RuleChain assignRuleChainToEdge(@PathVariable("edgeId") String strEdgeId, + @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { + checkParameter("edgeId", strEdgeId); + checkParameter(RULE_CHAIN_ID, strRuleChainId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); + checkRuleChain(ruleChainId, Operation.ASSIGN_TO_EDGE); + + RuleChain savedRuleChain = checkNotNull(ruleChainService.assignRuleChainToEdge(getCurrentUser().getTenantId(), ruleChainId, edgeId)); + + logEntityAction(ruleChainId, savedRuleChain, + null, + ActionType.ASSIGNED_TO_EDGE, null, strRuleChainId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedRuleChain.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); + + return savedRuleChain; + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.RULE_CHAIN), null, + null, + ActionType.ASSIGNED_TO_EDGE, e, strRuleChainId, strEdgeId); + + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/ruleChain/{ruleChainId}", method = RequestMethod.DELETE) + @ResponseBody + public RuleChain unassignRuleChainFromEdge(@PathVariable("edgeId") String strEdgeId, + @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { + checkParameter("edgeId", strEdgeId); + checkParameter(RULE_CHAIN_ID, strRuleChainId); + try { + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + Edge edge = checkEdgeId(edgeId, Operation.READ); + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); + RuleChain ruleChain = checkRuleChain(ruleChainId, Operation.UNASSIGN_FROM_EDGE); + + RuleChain savedRuleChain = checkNotNull(ruleChainService.unassignRuleChainFromEdge(getCurrentUser().getTenantId(), ruleChainId, edgeId, false)); + + logEntityAction(ruleChainId, ruleChain, + null, + ActionType.UNASSIGNED_FROM_EDGE, null, strRuleChainId, strEdgeId, edge.getName()); + + sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedRuleChain.getId(), EdgeEventActionType.UNASSIGNED_FROM_EDGE); + + return savedRuleChain; + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.RULE_CHAIN), null, + null, + ActionType.UNASSIGNED_FROM_EDGE, e, strRuleChainId, strEdgeId); + + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/edge/{edgeId}/ruleChains", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getEdgeRuleChains( + @PathVariable("edgeId") String strEdgeId, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + checkParameter("edgeId", strEdgeId); + try { + TenantId tenantId = getCurrentUser().getTenantId(); + EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); + checkEdgeId(edgeId, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edgeId, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChain/{ruleChainId}/edgeTemplateRoot", method = RequestMethod.POST) + @ResponseBody + public RuleChain setEdgeTemplateRootRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { + checkParameter(RULE_CHAIN_ID, strRuleChainId); + try { + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); + RuleChain ruleChain = checkRuleChain(ruleChainId, Operation.WRITE); + ruleChainService.setEdgeTemplateRootRuleChain(getTenantId(), ruleChainId); + return ruleChain; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.RULE_CHAIN), + null, + null, + ActionType.UPDATED, e, strRuleChainId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChain/{ruleChainId}/autoAssignToEdge", method = RequestMethod.POST) + @ResponseBody + public RuleChain setAutoAssignToEdgeRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { + checkParameter(RULE_CHAIN_ID, strRuleChainId); + try { + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); + RuleChain ruleChain = checkRuleChain(ruleChainId, Operation.WRITE); + ruleChainService.setAutoAssignToEdgeRuleChain(getTenantId(), ruleChainId); + return ruleChain; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.RULE_CHAIN), + null, + null, + ActionType.UPDATED, e, strRuleChainId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChain/{ruleChainId}/autoAssignToEdge", method = RequestMethod.DELETE) + @ResponseBody + public RuleChain unsetAutoAssignToEdgeRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { + checkParameter(RULE_CHAIN_ID, strRuleChainId); + try { + RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); + RuleChain ruleChain = checkRuleChain(ruleChainId, Operation.WRITE); + ruleChainService.unsetAutoAssignToEdgeRuleChain(getTenantId(), ruleChainId); + return ruleChain; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.RULE_CHAIN), + null, + null, + ActionType.UPDATED, e, strRuleChainId); + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChain/autoAssignToEdgeRuleChains", method = RequestMethod.GET) + @ResponseBody + public List getAutoAssignToEdgeRuleChains() throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(ruleChainService.findAutoAssignToEdgeRuleChainsByTenantId(tenantId)).get(); + } catch (Exception e) { + throw handleException(e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index adba9827cd..b24cf0e76e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -30,22 +30,19 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; -import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.lwm2m.LwM2mObject; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.dao.resource.TbResourceService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; -import java.util.ArrayList; import java.util.Base64; import java.util.List; -import java.util.StringJoiner; @Slf4j @RestController @@ -105,25 +102,13 @@ public class TbResourceController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/resource", method = RequestMethod.POST) @ResponseBody - public List saveResources(@RequestBody List resources) throws ThingsboardException { + public TbResource saveResource(@RequestBody TbResource resource) throws ThingsboardException { try { - List addResources = new ArrayList<>(); - StringJoiner noSaveResources = new StringJoiner("; "); - resources.forEach(resource -> { - try { resource.setTenantId(getTenantId()); checkEntity(resource.getId(), resource, Resource.TB_RESOURCE); - addResources.add(addResource(resource)); - } catch (Exception e) { - noSaveResources.add(resource.getFileName()); - log.warn("Fail save resource: [{}]", resource.getFileName(), e); + return addResource(resource); } - }); - if (noSaveResources.length() > 0) { - throw new ThingsboardException(String.format("Fail save resource: %s", noSaveResources.toString()), ThingsboardErrorCode.INVALID_ARGUMENTS); - } - return addResources; - } catch (Exception e) { + catch (Exception e) { throw handleException(e); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java index 70b5efe40d..7621d05908 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -95,6 +95,9 @@ public class TenantController extends BaseController { tenant = checkNotNull(tenantService.saveTenant(tenant)); if (newTenant) { installScripts.createDefaultRuleChains(tenant.getId()); + if (edgesEnabled) { + installScripts.createDefaultEdgeRuleChains(tenant.getId()); + } } tenantProfileCache.evict(tenant.getId()); tbClusterService.onTenantChange(tenant, null); diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index ca3ecdc36c..f254414902 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -36,9 +36,11 @@ import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; @@ -57,6 +59,7 @@ import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.system.SystemSecurityService; import javax.servlet.http.HttpServletRequest; +import java.util.List; @RequiredArgsConstructor @RestController @@ -178,6 +181,9 @@ public class UserController extends BaseController { savedUser.getCustomerId(), user.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + sendEntityNotificationMsg(getTenantId(), savedUser.getId(), + user.getId() == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED); + return savedUser; } catch (Exception e) { @@ -247,12 +253,17 @@ public class UserController extends BaseController { try { UserId userId = new UserId(toUUID(strUserId)); User user = checkUserId(userId, Operation.DELETE); + + List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), userId); + userService.deleteUser(getCurrentUser().getTenantId(), userId); logEntityAction(userId, user, user.getCustomerId(), ActionType.DELETED, null, strUserId); + sendDeleteNotificationMsg(getTenantId(), userId, relatedEdgeIds); + } catch (Exception e) { logEntityAction(emptyId(EntityType.USER), null, diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java index 3239d143bd..ad81821a82 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -25,6 +26,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; @@ -39,6 +42,7 @@ import org.thingsboard.server.service.security.permission.Resource; import java.util.List; +@Slf4j @RestController @TbCoreComponent @RequestMapping("/api") @@ -69,8 +73,12 @@ public class WidgetTypeController extends BaseController { } checkEntity(widgetTypeDetails.getId(), widgetTypeDetails, Resource.WIDGET_TYPE); + WidgetTypeDetails savedWidgetTypeDetails = widgetTypeService.saveWidgetType(widgetTypeDetails); - return checkNotNull(widgetTypeService.saveWidgetType(widgetTypeDetails)); + sendEntityNotificationMsg(getTenantId(), savedWidgetTypeDetails.getId(), + widgetTypeDetails.getId() == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED); + + return checkNotNull(savedWidgetTypeDetails); } catch (Exception e) { throw handleException(e); } @@ -85,6 +93,9 @@ public class WidgetTypeController extends BaseController { WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId)); checkWidgetTypeId(widgetTypeId, Operation.DELETE); widgetTypeService.deleteWidgetType(getCurrentUser().getTenantId(), widgetTypeId); + + sendEntityNotificationMsg(getTenantId(), widgetTypeId, EdgeEventActionType.DELETED); + } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java index 3908543922..14c5d6a860 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -25,6 +25,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; @@ -68,7 +69,12 @@ public class WidgetsBundleController extends BaseController { } checkEntity(widgetsBundle.getId(), widgetsBundle, Resource.WIDGETS_BUNDLE); - return checkNotNull(widgetsBundleService.saveWidgetsBundle(widgetsBundle)); + WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); + + sendEntityNotificationMsg(getTenantId(), savedWidgetsBundle.getId(), + widgetsBundle.getId() == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED); + + return checkNotNull(savedWidgetsBundle); } catch (Exception e) { throw handleException(e); } @@ -83,6 +89,9 @@ public class WidgetsBundleController extends BaseController { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); checkWidgetsBundleId(widgetsBundleId, Operation.DELETE); widgetsBundleService.deleteWidgetsBundle(getTenantId(), widgetsBundleId); + + sendEntityNotificationMsg(getTenantId(), widgetsBundleId, EdgeEventActionType.DELETED); + } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java index 57f1fd2927..ed8c0a7a17 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java @@ -27,6 +27,7 @@ import org.springframework.security.authentication.LockedException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; @@ -66,7 +67,12 @@ public class ThingsboardErrorResponseHandler implements AccessDeniedHandler { response.setContentType(MediaType.APPLICATION_JSON_VALUE); if (exception instanceof ThingsboardException) { - handleThingsboardException((ThingsboardException) exception, response); + ThingsboardException thingsboardException = (ThingsboardException) exception; + if (thingsboardException.getErrorCode() == ThingsboardErrorCode.SUBSCRIPTION_VIOLATION) { + handleSubscriptionException((ThingsboardException) exception, response); + } else { + handleThingsboardException((ThingsboardException) exception, response); + } } else if (exception instanceof TbRateLimitsException) { handleRateLimitException(response, (TbRateLimitsException) exception); } else if (exception instanceof AccessDeniedException) { @@ -126,6 +132,11 @@ public class ThingsboardErrorResponseHandler implements AccessDeniedHandler { ThingsboardErrorCode.TOO_MANY_REQUESTS, HttpStatus.TOO_MANY_REQUESTS)); } + private void handleSubscriptionException(ThingsboardException subscriptionException, HttpServletResponse response) throws IOException { + response.setStatus(HttpStatus.FORBIDDEN.value()); + mapper.writeValue(response.getWriter(), + (new ObjectMapper()).readValue(((HttpClientErrorException) subscriptionException.getCause()).getResponseBodyAsByteArray(), Object.class)); + } private void handleAccessDeniedException(HttpServletResponse response) throws IOException { response.setStatus(HttpStatus.FORBIDDEN.value()); diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index a22a22d240..8ea8b85017 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -193,7 +193,9 @@ public class ThingsboardInstallService { databaseEntitiesUpgradeService.upgradeDatabase("3.2.1"); case "3.2.2": log.info("Upgrading ThingsBoard from version 3.2.2 to 3.3.0 ..."); - databaseEntitiesUpgradeService.upgradeDatabase("3.2.2"); + databaseEntitiesUpgradeService.upgradeDatabase("3.2.2"); + + dataUpdateService.updateData("3.2.2"); log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); 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 c0a9f5e7c5..00a5bca503 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 @@ -34,13 +34,13 @@ import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.component.ComponentDescriptorService; import javax.annotation.PostConstruct; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -65,7 +65,9 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe private Map components = new HashMap<>(); - private Map> componentsMap = new HashMap<>(); + private Map> coreComponentsMap = new HashMap<>(); + + private Map> edgeComponentsMap = new HashMap<>(); private ObjectMapper mapper = new ObjectMapper(); @@ -93,7 +95,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe ComponentType type = ruleNodeAnnotation.type(); ComponentDescriptor component = scanAndPersistComponent(def, type); components.put(component.getClazz(), component); - componentsMap.computeIfAbsent(type, k -> new ArrayList<>()).add(component); + putComponentIntoMaps(type, ruleNodeAnnotation, component); break; } catch (Exception e) { log.trace("Can't initialize component {}, due to {}", def.getBeanClassName(), e.getMessage(), e); @@ -113,22 +115,35 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe } } - private void registerComponents(ComponentType type, Class annotation) { - List components = persist(getBeanDefinitions(annotation), type); - componentsMap.put(type, components); - registerComponents(components); - } - - private void registerComponents(Collection comps) { - comps.forEach(c -> components.put(c.getClazz(), c)); + private void putComponentIntoMaps(ComponentType type, RuleNode ruleNodeAnnotation, ComponentDescriptor component) { + boolean ruleChainTypesMethodAvailable; + try { + ruleNodeAnnotation.getClass().getMethod("ruleChainTypes"); + ruleChainTypesMethodAvailable = true; + } catch (NoSuchMethodException exception) { + log.warn("[{}] does not have ruleChainTypes. Probably extension class compiled before 3.3 release. " + + "Please update your extensions and compile using latest 3.3 release dependency", ruleNodeAnnotation.name()); + ruleChainTypesMethodAvailable = false; + } + if (ruleChainTypesMethodAvailable) { + if (ruleChainTypeContainsArray(RuleChainType.CORE, ruleNodeAnnotation.ruleChainTypes())) { + coreComponentsMap.computeIfAbsent(type, k -> new ArrayList<>()).add(component); + } + if (ruleChainTypeContainsArray(RuleChainType.EDGE, ruleNodeAnnotation.ruleChainTypes())) { + edgeComponentsMap.computeIfAbsent(type, k -> new ArrayList<>()).add(component); + } + } else { + coreComponentsMap.computeIfAbsent(type, k -> new ArrayList<>()).add(component); + } } - private List persist(Set filterDefs, ComponentType type) { - List result = new ArrayList<>(); - for (BeanDefinition def : filterDefs) { - result.add(scanAndPersistComponent(def, type)); + private boolean ruleChainTypeContainsArray(RuleChainType ruleChainType, RuleChainType[] array) { + for (RuleChainType tmp : array) { + if (ruleChainType.equals(tmp)) { + return true; + } } - return result; + return false; } private ComponentDescriptor scanAndPersistComponent(BeanDefinition def, ComponentType type) { @@ -222,25 +237,47 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe } @Override - public List getComponents(ComponentType type) { - if (componentsMap.containsKey(type)) { - return Collections.unmodifiableList(componentsMap.get(type)); + public List getComponents(ComponentType type, RuleChainType ruleChainType) { + if (RuleChainType.CORE.equals(ruleChainType)) { + if (coreComponentsMap.containsKey(type)) { + return Collections.unmodifiableList(coreComponentsMap.get(type)); + } else { + return Collections.emptyList(); + } + } else if (RuleChainType.EDGE.equals(ruleChainType)) { + if (edgeComponentsMap.containsKey(type)) { + return Collections.unmodifiableList(edgeComponentsMap.get(type)); + } else { + return Collections.emptyList(); + } } else { - return Collections.emptyList(); + log.error("Unsupported rule chain type {}", ruleChainType); + throw new RuntimeException("Unsupported rule chain type " + ruleChainType); } } @Override - public List getComponents(Set types) { - List result = new ArrayList<>(); - types.stream().filter(type -> componentsMap.containsKey(type)).forEach(type -> { - result.addAll(componentsMap.get(type)); - }); - return Collections.unmodifiableList(result); + public List getComponents(Set types, RuleChainType ruleChainType) { + if (RuleChainType.CORE.equals(ruleChainType)) { + return getComponents(types, coreComponentsMap); + } else if (RuleChainType.EDGE.equals(ruleChainType)) { + return getComponents(types, edgeComponentsMap); + } else { + log.error("Unsupported rule chain type {}", ruleChainType); + throw new RuntimeException("Unsupported rule chain type " + ruleChainType); + } } @Override public Optional getComponent(String clazz) { return Optional.ofNullable(components.get(clazz)); } + + private List getComponents(Set types, Map> componentsMap) { + List result = new ArrayList<>(); + types.stream().filter(componentsMap::containsKey).forEach(type -> { + result.addAll(componentsMap.get(type)); + }); + return Collections.unmodifiableList(result); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java index 4d62096cdd..d5b4ed5e32 100644 --- a/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java +++ b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.component; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.rule.RuleChainType; import java.util.List; import java.util.Optional; @@ -29,10 +30,9 @@ public interface ComponentDiscoveryService { void discoverComponents(); - List getComponents(ComponentType type); + List getComponents(ComponentType type, RuleChainType ruleChainType); - List getComponents(Set types); + List getComponents(Set types, RuleChainType ruleChainType); Optional getComponent(String clazz); - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java new file mode 100644 index 0000000000..66e64c14f1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java @@ -0,0 +1,505 @@ +/** + * Copyright © 2016-2021 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; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.RuleChainId; +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.page.TimePageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.edge.EdgeEventService; +import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.queue.TbClusterService; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Service +@TbCoreComponent +@Slf4j +public class DefaultEdgeNotificationService implements EdgeNotificationService { + + private static final ObjectMapper mapper = new ObjectMapper(); + + private static final int DEFAULT_LIMIT = 100; + + @Autowired + private EdgeService edgeService; + + @Autowired + private AlarmService alarmService; + + @Autowired + private UserService userService; + + @Autowired + private RuleChainService ruleChainService; + + @Autowired + private EdgeEventService edgeEventService; + + @Autowired + private TbClusterService clusterService; + + @Autowired + private DbCallbackExecutorService dbCallbackExecutorService; + + private ExecutorService tsCallBackExecutor; + + @PostConstruct + public void initExecutor() { + tsCallBackExecutor = Executors.newSingleThreadExecutor(); + } + + @PreDestroy + public void shutdownExecutor() { + if (tsCallBackExecutor != null) { + tsCallBackExecutor.shutdownNow(); + } + } + + @Override + public Edge setEdgeRootRuleChain(TenantId tenantId, Edge edge, RuleChainId ruleChainId) throws IOException { + edge.setRootRuleChainId(ruleChainId); + Edge savedEdge = edgeService.saveEdge(edge); + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.RULE_CHAIN, EdgeEventActionType.UPDATED, ruleChainId, null); + return savedEdge; + } + + private void saveEdgeEvent(TenantId tenantId, + EdgeId edgeId, + EdgeEventType type, + EdgeEventActionType action, + EntityId entityId, + JsonNode body) { + log.debug("Pushing edge event to edge queue. tenantId [{}], edgeId [{}], type [{}], action[{}], entityId [{}], body [{}]", + tenantId, edgeId, type, action, entityId, body); + + EdgeEvent edgeEvent = new EdgeEvent(); + edgeEvent.setEdgeId(edgeId); + edgeEvent.setTenantId(tenantId); + edgeEvent.setType(type); + edgeEvent.setAction(action); + if (entityId != null) { + edgeEvent.setEntityId(entityId.getId()); + } + edgeEvent.setBody(body); + ListenableFuture future = edgeEventService.saveAsync(edgeEvent); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable EdgeEvent result) { + clusterService.onEdgeEventUpdate(tenantId, edgeId); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Can't save edge event [{}] for edge [{}]", tenantId.getId(), edgeEvent, edgeId.getId(), t); + } + }, dbCallbackExecutorService); + + } + + @Override + public void pushNotificationToEdge(TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback) { + try { + TenantId tenantId = new TenantId(new UUID(edgeNotificationMsg.getTenantIdMSB(), edgeNotificationMsg.getTenantIdLSB())); + EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); + switch (type) { + case EDGE: + processEdge(tenantId, edgeNotificationMsg); + break; + case USER: + case ASSET: + case DEVICE: + case DEVICE_PROFILE: + case ENTITY_VIEW: + case DASHBOARD: + case RULE_CHAIN: + processEntity(tenantId, edgeNotificationMsg); + break; + case CUSTOMER: + processCustomer(tenantId, edgeNotificationMsg); + break; + case WIDGETS_BUNDLE: + case WIDGET_TYPE: + processWidgetBundleOrWidgetType(tenantId, edgeNotificationMsg); + break; + case ALARM: + processAlarm(tenantId, edgeNotificationMsg); + break; + case RELATION: + processRelation(tenantId, edgeNotificationMsg); + break; + default: + log.debug("Edge event type [{}] is not designed to be pushed to edge", type); + } + } catch (Exception e) { + callback.onFailure(e); + log.error("Can't push to edge updates, edgeNotificationMsg [{}]", edgeNotificationMsg, e); + } finally { + callback.onSuccess(); + } + } + + private void processEdge(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + try { + EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); + EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); + ListenableFuture edgeFuture; + switch (actionType) { + case ASSIGNED_TO_CUSTOMER: + CustomerId customerId = mapper.readValue(edgeNotificationMsg.getBody(), CustomerId.class); + edgeFuture = edgeService.findEdgeByIdAsync(tenantId, edgeId); + Futures.addCallback(edgeFuture, new FutureCallback() { + @Override + public void onSuccess(@Nullable Edge edge) { + if (edge != null && !customerId.isNullUid()) { + saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.CUSTOMER, EdgeEventActionType.ADDED, customerId, null); + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = userService.findCustomerUsers(tenantId, customerId, pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] user(s) are going to be added to edge.", edge.getId(), pageData.getData().size()); + for (User user : pageData.getData()) { + saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.USER, EdgeEventActionType.ADDED, user.getId(), null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't find edge by id [{}]", edgeNotificationMsg, t); + } + }, dbCallbackExecutorService); + break; + case UNASSIGNED_FROM_CUSTOMER: + CustomerId customerIdToDelete = mapper.readValue(edgeNotificationMsg.getBody(), CustomerId.class); + edgeFuture = edgeService.findEdgeByIdAsync(tenantId, edgeId); + Futures.addCallback(edgeFuture, new FutureCallback() { + @Override + public void onSuccess(@Nullable Edge edge) { + if (edge != null && !customerIdToDelete.isNullUid()) { + saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.CUSTOMER, EdgeEventActionType.DELETED, customerIdToDelete, null); + } + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't find edge by id [{}]", edgeNotificationMsg, t); + } + }, dbCallbackExecutorService); + break; + } + } catch (Exception e) { + log.error("Exception during processing edge event", e); + } + } + + private void processWidgetBundleOrWidgetType(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); + EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); + EntityId entityId = EntityIdFactory.getByEdgeEventTypeAndUuid(type, new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); + switch (actionType) { + case ADDED: + case UPDATED: + case DELETED: + processActionForAllEdges(tenantId, type, actionType, entityId); + break; + } + } + + private void processCustomer(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); + EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); + UUID uuid = new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB()); + CustomerId customerId = new CustomerId(EntityIdFactory.getByEdgeEventTypeAndUuid(type, uuid).getId()); + switch (actionType) { + case UPDATED: + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + for (Edge edge : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), type, actionType, customerId, null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + break; + case DELETED: + EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB())); + saveEdgeEvent(tenantId, edgeId, type, actionType, customerId, null); + break; + } + } + + private void processEntity(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); + EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); + EntityId entityId = EntityIdFactory.getByEdgeEventTypeAndUuid(type, + new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); + EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB())); + ListenableFuture> edgeIdsFuture; + switch (actionType) { + case ADDED: // used only for USER entity + case UPDATED: + case CREDENTIALS_UPDATED: + edgeIdsFuture = edgeService.findRelatedEdgeIdsByEntityId(tenantId, entityId); + Futures.addCallback(edgeIdsFuture, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List edgeIds) { + if (edgeIds != null && !edgeIds.isEmpty()) { + for (EdgeId edgeId : edgeIds) { + saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, null); + } + } + } + @Override + public void onFailure(Throwable throwable) { + log.error("Failed to find related edge ids [{}]", edgeNotificationMsg, throwable); + } + }, dbCallbackExecutorService); + break; + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + edgeIdsFuture = edgeService.findRelatedEdgeIdsByEntityId(tenantId, entityId); + Futures.addCallback(edgeIdsFuture, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List edgeIds) { + if (edgeIds != null && !edgeIds.isEmpty()) { + for (EdgeId edgeId : edgeIds) { + try { + CustomerId customerId = mapper.readValue(edgeNotificationMsg.getBody(), CustomerId.class); + ListenableFuture future = edgeService.findEdgeByIdAsync(tenantId, edgeId); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable Edge edge) { + if (edge != null && edge.getCustomerId() != null && + !edge.getCustomerId().isNullUid() && edge.getCustomerId().equals(customerId)) { + saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, null); + } + } + @Override + public void onFailure(Throwable throwable) { + log.error("Failed to find edge by id [{}]", edgeNotificationMsg, throwable); + } + }, dbCallbackExecutorService); + } catch (Exception e) { + log.error("Can't parse customer id from entity body [{}]", edgeNotificationMsg, e); + } + } + } + } + + @Override + public void onFailure(Throwable throwable) { + log.error("Failed to find related edge ids [{}]", edgeNotificationMsg, throwable); + } + }, dbCallbackExecutorService); + break; + case DELETED: + saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, null); + break; + case ASSIGNED_TO_EDGE: + case UNASSIGNED_FROM_EDGE: + saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, null); + if (type.equals(EdgeEventType.RULE_CHAIN)) { + updateDependentRuleChains(tenantId, new RuleChainId(entityId.getId()), edgeId); + } + break; + } + } + + private void updateDependentRuleChains(TenantId tenantId, RuleChainId processingRuleChainId, EdgeId edgeId) { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + for (RuleChain ruleChain : pageData.getData()) { + if (!ruleChain.getId().equals(processingRuleChainId)) { + List connectionInfos = + ruleChainService.loadRuleChainMetaData(ruleChain.getTenantId(), ruleChain.getId()).getRuleChainConnections(); + if (connectionInfos != null && !connectionInfos.isEmpty()) { + for (RuleChainConnectionInfo connectionInfo : connectionInfos) { + if (connectionInfo.getTargetRuleChainId().equals(processingRuleChainId)) { + saveEdgeEvent(tenantId, + edgeId, + EdgeEventType.RULE_CHAIN_METADATA, + EdgeEventActionType.UPDATED, + ruleChain.getId(), + null); + } + } + } + } + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } + + private void processAlarm(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + AlarmId alarmId = new AlarmId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); + ListenableFuture alarmFuture = alarmService.findAlarmByIdAsync(tenantId, alarmId); + Futures.addCallback(alarmFuture, new FutureCallback() { + @Override + public void onSuccess(@Nullable Alarm alarm) { + if (alarm != null) { + EdgeEventType type = EdgeUtils.getEdgeEventTypeByEntityType(alarm.getOriginator().getEntityType()); + if (type != null) { + ListenableFuture> relatedEdgeIdsByEntityIdFuture = edgeService.findRelatedEdgeIdsByEntityId(tenantId, alarm.getOriginator()); + Futures.addCallback(relatedEdgeIdsByEntityIdFuture, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List relatedEdgeIdsByEntityId) { + if (relatedEdgeIdsByEntityId != null) { + for (EdgeId edgeId : relatedEdgeIdsByEntityId) { + saveEdgeEvent(tenantId, + edgeId, + EdgeEventType.ALARM, + EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()), + alarmId, + null); + } + } + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] can't find related edge ids by entity id [{}]", tenantId.getId(), alarm.getOriginator(), t); + } + }, dbCallbackExecutorService); + } + } + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] can't find alarm by id [{}]", tenantId.getId(), alarmId.getId(), t); + } + }, dbCallbackExecutorService); + } + + private void processRelation(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) throws JsonProcessingException { + EntityRelation relation = mapper.readValue(edgeNotificationMsg.getBody(), EntityRelation.class); + if (!relation.getFrom().getEntityType().equals(EntityType.EDGE) && + !relation.getTo().getEntityType().equals(EntityType.EDGE)) { + List>> futures = new ArrayList<>(); + futures.add(edgeService.findRelatedEdgeIdsByEntityId(tenantId, relation.getTo())); + futures.add(edgeService.findRelatedEdgeIdsByEntityId(tenantId, relation.getFrom())); + ListenableFuture>> combinedFuture = Futures.allAsList(futures); + Futures.addCallback(combinedFuture, new FutureCallback>>() { + @Override + public void onSuccess(@Nullable List> listOfListsEdgeIds) { + Set uniqueEdgeIds = new HashSet<>(); + if (listOfListsEdgeIds != null && !listOfListsEdgeIds.isEmpty()) { + for (List listOfListsEdgeId : listOfListsEdgeIds) { + if (listOfListsEdgeId != null) { + uniqueEdgeIds.addAll(listOfListsEdgeId); + } + } + } + if (!uniqueEdgeIds.isEmpty()) { + for (EdgeId edgeId : uniqueEdgeIds) { + saveEdgeEvent(tenantId, + edgeId, + EdgeEventType.RELATION, + EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()), + null, + mapper.valueToTree(relation)); + } + } + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] can't find related edge ids by relation to id [{}] and relation from id [{}]" , + tenantId.getId(), relation.getTo().getId(), relation.getFrom().getId(), t); + } + }, dbCallbackExecutorService); + } + } + + private void processActionForAllEdges(TenantId tenantId, EdgeEventType type, EdgeEventActionType actionType, EntityId entityId) { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = edgeService.findEdgesByTenantId(tenantId, pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + for (Edge edge : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), type, actionType, entityId, null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } +} + + diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java new file mode 100644 index 0000000000..eb3852f7ac --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -0,0 +1,233 @@ +/** + * Copyright © 2016-2021 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; + +import lombok.Data; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.thingsboard.server.actors.service.ActorService; +import org.thingsboard.server.dao.alarm.AlarmService; +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.entityview.EntityViewService; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.rule.RuleChainService; +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.queue.discovery.PartitionService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings; +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.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.EntityDataMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.EntityViewMsgConstructor; +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.UserMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.WidgetTypeMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.WidgetsBundleMsgConstructor; +import org.thingsboard.server.service.edge.rpc.init.SyncEdgeService; +import org.thingsboard.server.service.edge.rpc.processor.AlarmProcessor; +import org.thingsboard.server.service.edge.rpc.processor.DeviceProcessor; +import org.thingsboard.server.service.edge.rpc.processor.RelationProcessor; +import org.thingsboard.server.service.edge.rpc.processor.TelemetryProcessor; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.queue.TbClusterService; +import org.thingsboard.server.service.state.DeviceStateService; + +@Component +@TbCoreComponent +@Data +public class EdgeContextComponent { + + @Lazy + @Autowired + private EdgeService edgeService; + + @Autowired + private PartitionService partitionService; + + @Lazy + @Autowired + private EdgeEventService edgeEventService; + + @Lazy + @Autowired + private AssetService assetService; + + @Lazy + @Autowired + private DeviceService deviceService; + + @Lazy + @Autowired + private DeviceProfileService deviceProfileService; + + @Lazy + @Autowired + private DeviceCredentialsService deviceCredentialsService; + + @Lazy + @Autowired + private EntityViewService entityViewService; + + @Lazy + @Autowired + private AttributesService attributesService; + + @Lazy + @Autowired + private CustomerService customerService; + + @Lazy + @Autowired + private RelationService relationService; + + @Lazy + @Autowired + private AlarmService alarmService; + + @Lazy + @Autowired + private DashboardService dashboardService; + + @Lazy + @Autowired + private RuleChainService ruleChainService; + + @Lazy + @Autowired + private UserService userService; + + @Lazy + @Autowired + private ActorService actorService; + + @Lazy + @Autowired + private WidgetsBundleService widgetsBundleService; + + @Lazy + @Autowired + private WidgetTypeService widgetTypeService; + + @Lazy + @Autowired + private DeviceStateService deviceStateService; + + @Lazy + @Autowired + private TbClusterService tbClusterService; + + @Lazy + @Autowired + private SyncEdgeService syncEdgeService; + + @Lazy + @Autowired + private RuleChainMsgConstructor ruleChainMsgConstructor; + + @Lazy + @Autowired + private AlarmMsgConstructor alarmMsgConstructor; + + @Lazy + @Autowired + private DeviceMsgConstructor deviceMsgConstructor; + + @Lazy + @Autowired + private DeviceProfileMsgConstructor deviceProfileMsgConstructor; + + @Lazy + @Autowired + private AssetMsgConstructor assetMsgConstructor; + + @Lazy + @Autowired + private EntityViewMsgConstructor entityViewMsgConstructor; + + @Lazy + @Autowired + private DashboardMsgConstructor dashboardMsgConstructor; + + @Lazy + @Autowired + private CustomerMsgConstructor customerMsgConstructor; + + @Lazy + @Autowired + private UserMsgConstructor userMsgConstructor; + + @Lazy + @Autowired + private RelationMsgConstructor relationMsgConstructor; + + @Lazy + @Autowired + private WidgetsBundleMsgConstructor widgetsBundleMsgConstructor; + + @Lazy + @Autowired + private WidgetTypeMsgConstructor widgetTypeMsgConstructor; + + @Lazy + @Autowired + private AdminSettingsMsgConstructor adminSettingsMsgConstructor; + + @Lazy + @Autowired + private EntityDataMsgConstructor entityDataMsgConstructor; + + @Lazy + @Autowired + private AlarmProcessor alarmProcessor; + + @Lazy + @Autowired + private DeviceProcessor deviceProcessor; + + @Lazy + @Autowired + private RelationProcessor relationProcessor; + + @Lazy + @Autowired + private TelemetryProcessor telemetryProcessor; + + @Lazy + @Autowired + private EdgeEventStorageSettings edgeEventStorageSettings; + + @Autowired + @Getter + private DbCallbackExecutorService dbCallbackExecutor; +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java new file mode 100644 index 0000000000..48463d246c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeNotificationService.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2021 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; + +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.io.IOException; + +public interface EdgeNotificationService { + + Edge setEdgeRootRuleChain(TenantId tenantId, Edge edge, RuleChainId ruleChainId) throws IOException; + + void pushNotificationToEdge(TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java new file mode 100644 index 0000000000..621f7e60e7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2021 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; + + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@Data +public class EdgeEventStorageSettings { + @Value("${edges.storage.max_read_records_count}") + private int maxReadRecordsCount; + @Value("${edges.storage.no_read_records_sleep}") + private long noRecordsSleepInterval; + @Value("${edges.storage.sleep_between_batches}") + private long sleepIntervalBetweenBatches; +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java new file mode 100644 index 0000000000..70033f10e1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -0,0 +1,290 @@ +/** + * Copyright © 2016-2021 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; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.io.Resources; +import com.google.common.util.concurrent.FutureCallback; +import io.grpc.Server; +import io.grpc.netty.NettyServerBuilder; +import io.grpc.stub.StreamObserver; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.gen.edge.EdgeRpcServiceGrpc; +import org.thingsboard.server.gen.edge.RequestMsg; +import org.thingsboard.server.gen.edge.ResponseMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.EdgeContextComponent; +import org.thingsboard.server.service.state.DefaultDeviceStateService; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; + +import javax.annotation.Nullable; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +@Service +@Slf4j +@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") +@TbCoreComponent +public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService { + + private final ConcurrentMap sessions = new ConcurrentHashMap<>(); + private final ConcurrentMap sessionNewEvents = new ConcurrentHashMap<>(); + private final ConcurrentMap> sessionEdgeEventChecks = new ConcurrentHashMap<>(); + private static final ObjectMapper mapper = new ObjectMapper(); + + @Value("${edges.rpc.port}") + private int rpcPort; + @Value("${edges.rpc.ssl.enabled}") + private boolean sslEnabled; + @Value("${edges.rpc.ssl.cert}") + private String certFileResource; + @Value("${edges.rpc.ssl.private_key}") + private String privateKeyResource; + @Value("${edges.state.persistToTelemetry:false}") + private boolean persistToTelemetry; + @Value("${edges.rpc.client_max_keep_alive_time_sec}") + private int clientMaxKeepAliveTimeSec; + + @Value("${edges.scheduler_pool_size}") + private int schedulerPoolSize; + + @Autowired + private EdgeContextComponent ctx; + + @Autowired + private TelemetrySubscriptionService tsSubService; + + private Server server; + + private ScheduledExecutorService scheduler; + + @PostConstruct + public void init() { + log.info("Initializing Edge RPC service!"); + NettyServerBuilder builder = NettyServerBuilder.forPort(rpcPort) + .permitKeepAliveTime(clientMaxKeepAliveTimeSec, TimeUnit.SECONDS) + .addService(this); + if (sslEnabled) { + try { + File certFile = new File(Resources.getResource(certFileResource).toURI()); + File privateKeyFile = new File(Resources.getResource(privateKeyResource).toURI()); + builder.useTransportSecurity(certFile, privateKeyFile); + } catch (Exception e) { + log.error("Unable to set up SSL context. Reason: " + e.getMessage(), e); + throw new RuntimeException("Unable to set up SSL context!", e); + } + } + server = builder.build(); + log.info("Going to start Edge RPC server using port: {}", rpcPort); + try { + server.start(); + } catch (IOException e) { + log.error("Failed to start Edge RPC server!", e); + throw new RuntimeException("Failed to start Edge RPC server!"); + } + this.scheduler = Executors.newScheduledThreadPool(schedulerPoolSize, ThingsBoardThreadFactory.forName("edge-scheduler")); + log.info("Edge RPC service initialized!"); + } + + @PreDestroy + public void destroy() { + if (server != null) { + server.shutdownNow(); + } + for (Map.Entry> entry : sessionEdgeEventChecks.entrySet()) { + EdgeId edgeId = entry.getKey(); + ScheduledFuture sessionEdgeEventCheck = entry.getValue(); + if (sessionEdgeEventCheck != null && !sessionEdgeEventCheck.isCancelled() && !sessionEdgeEventCheck.isDone()) { + sessionEdgeEventCheck.cancel(true); + sessionEdgeEventChecks.remove(edgeId); + } + } + if (scheduler != null) { + scheduler.shutdownNow(); + } + } + + @Override + public StreamObserver handleMsgs(StreamObserver outputStream) { + return new EdgeGrpcSession(ctx, outputStream, this::onEdgeConnect, this::onEdgeDisconnect, mapper).getInputStream(); + } + + @Override + public void updateEdge(Edge edge) { + EdgeGrpcSession session = sessions.get(edge.getId()); + if (session != null && session.isConnected()) { + log.debug("[{}] Updating configuration for edge [{}] [{}]", edge.getTenantId(), edge.getName(), edge.getId()); + session.onConfigurationUpdate(edge); + } else { + log.debug("[{}] Session doesn't exist for edge [{}] [{}]", edge.getTenantId(), edge.getName(), edge.getId()); + } + } + + @Override + public void deleteEdge(EdgeId edgeId) { + EdgeGrpcSession session = sessions.get(edgeId); + if (session != null && session.isConnected()) { + log.info("Closing and removing session for edge [{}]", edgeId); + session.close(); + sessions.remove(edgeId); + sessionNewEvents.remove(edgeId); + cancelScheduleEdgeEventsCheck(edgeId); + } + } + + @Override + public void onEdgeEvent(EdgeId edgeId) { + log.trace("[{}] onEdgeEvent", edgeId.getId()); + if (!sessionNewEvents.get(edgeId)) { + log.trace("[{}] set session new events flag to true", edgeId.getId()); + sessionNewEvents.put(edgeId, true); + } + } + + private void onEdgeConnect(EdgeId edgeId, EdgeGrpcSession edgeGrpcSession) { + log.info("[{}] edge [{}] connected successfully.", edgeGrpcSession.getSessionId(), edgeId); + sessions.put(edgeId, edgeGrpcSession); + sessionNewEvents.put(edgeId, false); + save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, true); + save(edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, System.currentTimeMillis()); + cancelScheduleEdgeEventsCheck(edgeId); + scheduleEdgeEventsCheck(edgeGrpcSession); + } + + public EdgeGrpcSession getEdgeGrpcSessionById(TenantId tenantId, EdgeId edgeId) { + EdgeGrpcSession session = sessions.get(edgeId); + if (session != null && session.isConnected()) { + return session; + } else { + log.error("[{}] Edge is not connected [{}]", tenantId, edgeId); + throw new RuntimeException("Edge is not connected"); + } + } + + private void scheduleEdgeEventsCheck(EdgeGrpcSession session) { + EdgeId edgeId = session.getEdge().getId(); + UUID tenantId = session.getEdge().getTenantId().getId(); + if (sessions.containsKey(edgeId)) { + ScheduledFuture schedule = scheduler.schedule(() -> { + try { + if (sessionNewEvents.get(edgeId)) { + log.trace("[{}] Set session new events flag to false", edgeId.getId()); + sessionNewEvents.put(edgeId, false); + session.processEdgeEvents(); + } + } catch (Exception e) { + log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, session.getEdge().getId().getId(), e); + } + scheduleEdgeEventsCheck(session); + }, ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval(), TimeUnit.MILLISECONDS); + sessionEdgeEventChecks.put(edgeId, schedule); + log.trace("[{}] Check edge event scheduled for edge [{}]", tenantId, edgeId.getId()); + } else { + log.debug("[{}] Session was removed and edge event check schedule must not be started [{}]", + tenantId, edgeId.getId()); + } + } + + private void cancelScheduleEdgeEventsCheck(EdgeId edgeId) { + log.trace("[{}] cancelling edge event check for edge", edgeId); + if (sessionEdgeEventChecks.containsKey(edgeId)) { + ScheduledFuture sessionEdgeEventCheck = sessionEdgeEventChecks.get(edgeId); + if (sessionEdgeEventCheck != null && !sessionEdgeEventCheck.isCancelled() && !sessionEdgeEventCheck.isDone()) { + sessionEdgeEventCheck.cancel(true); + sessionEdgeEventChecks.remove(edgeId); + } + } + } + + private void onEdgeDisconnect(EdgeId edgeId) { + log.info("[{}] edge disconnected!", edgeId); + sessions.remove(edgeId); + sessionNewEvents.remove(edgeId); + save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, false); + save(edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, System.currentTimeMillis()); + cancelScheduleEdgeEventsCheck(edgeId); + } + + private void save(EdgeId edgeId, String key, long value) { + log.debug("[{}] Updating long edge telemetry [{}] [{}]", edgeId, key, value); + if (persistToTelemetry) { + tsSubService.saveAndNotify( + TenantId.SYS_TENANT_ID, edgeId, + Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry(key, value))), + new AttributeSaveCallback(edgeId, key, value)); + } else { + tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, edgeId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(edgeId, key, value)); + } + } + + private void save(EdgeId edgeId, String key, boolean value) { + log.debug("[{}] Updating boolean edge telemetry [{}] [{}]", edgeId, key, value); + if (persistToTelemetry) { + tsSubService.saveAndNotify( + TenantId.SYS_TENANT_ID, edgeId, + Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))), + new AttributeSaveCallback(edgeId, key, value)); + } else { + tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, edgeId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(edgeId, key, value)); + } + } + + private static class AttributeSaveCallback implements FutureCallback { + private final EdgeId edgeId; + private final String key; + private final Object value; + + AttributeSaveCallback(EdgeId edgeId, String key, Object value) { + this.edgeId = edgeId; + this.key = key; + this.value = value; + } + + @Override + public void onSuccess(@Nullable Void result) { + log.trace("[{}] Successfully updated attribute [{}] with value [{}]", edgeId, key, value); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Failed to update attribute [{}] with value [{}]", edgeId, key, value, t); + } + } +} 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 new file mode 100644 index 0000000000..70978df0a3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -0,0 +1,1045 @@ +/** + * Copyright © 2016-2021 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; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.gson.JsonElement; +import io.grpc.stub.StreamObserver; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DataConstants; +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.HasCustomerId; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +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.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.SortOrder; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.common.data.widget.WidgetType; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.common.transport.util.JsonUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.AdminSettingsUpdateMsg; +import org.thingsboard.server.gen.edge.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.ConnectRequestMsg; +import org.thingsboard.server.gen.edge.ConnectResponseCode; +import org.thingsboard.server.gen.edge.ConnectResponseMsg; +import org.thingsboard.server.gen.edge.CustomerUpdateMsg; +import org.thingsboard.server.gen.edge.DashboardUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceProfileUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceRpcCallMsg; +import org.thingsboard.server.gen.edge.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.DownlinkMsg; +import org.thingsboard.server.gen.edge.DownlinkResponseMsg; +import org.thingsboard.server.gen.edge.EdgeConfiguration; +import org.thingsboard.server.gen.edge.EdgeUpdateMsg; +import org.thingsboard.server.gen.edge.EntityDataProto; +import org.thingsboard.server.gen.edge.EntityViewUpdateMsg; +import org.thingsboard.server.gen.edge.RelationRequestMsg; +import org.thingsboard.server.gen.edge.RelationUpdateMsg; +import org.thingsboard.server.gen.edge.RequestMsg; +import org.thingsboard.server.gen.edge.RequestMsgType; +import org.thingsboard.server.gen.edge.ResponseMsg; +import org.thingsboard.server.gen.edge.RuleChainMetadataRequestMsg; +import org.thingsboard.server.gen.edge.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.gen.edge.UplinkMsg; +import org.thingsboard.server.gen.edge.UplinkResponseMsg; +import org.thingsboard.server.gen.edge.UserCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.WidgetTypeUpdateMsg; +import org.thingsboard.server.gen.edge.WidgetsBundleUpdateMsg; +import org.thingsboard.server.service.edge.EdgeContextComponent; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +@Slf4j +@Data +public final class EdgeGrpcSession implements Closeable { + + private static final ReentrantLock downlinkMsgLock = new ReentrantLock(); + + private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs"; + + private final UUID sessionId; + private final BiConsumer sessionOpenListener; + private final Consumer sessionCloseListener; + private final ObjectMapper mapper; + + private EdgeContextComponent ctx; + private Edge edge; + private StreamObserver inputStream; + private StreamObserver outputStream; + private boolean connected; + + private CountDownLatch latch; + + EdgeGrpcSession(EdgeContextComponent ctx, StreamObserver outputStream, BiConsumer sessionOpenListener, + Consumer sessionCloseListener, ObjectMapper mapper) { + this.sessionId = UUID.randomUUID(); + this.ctx = ctx; + this.outputStream = outputStream; + this.sessionOpenListener = sessionOpenListener; + this.sessionCloseListener = sessionCloseListener; + this.mapper = mapper; + initInputStream(); + } + + private void initInputStream() { + this.inputStream = new StreamObserver() { + @Override + public void onNext(RequestMsg requestMsg) { + if (!connected && requestMsg.getMsgType().equals(RequestMsgType.CONNECT_RPC_MESSAGE)) { + ConnectResponseMsg responseMsg = processConnect(requestMsg.getConnectRequestMsg()); + outputStream.onNext(ResponseMsg.newBuilder() + .setConnectResponseMsg(responseMsg) + .build()); + if (ConnectResponseCode.ACCEPTED != responseMsg.getResponseCode()) { + outputStream.onError(new RuntimeException(responseMsg.getErrorMsg())); + } else { + connected = true; + } + } + if (connected && requestMsg.getMsgType().equals(RequestMsgType.SYNC_REQUEST_RPC_MESSAGE)) { + ctx.getSyncEdgeService().sync(edge.getTenantId(), edge); + } + if (connected) { + if (requestMsg.getMsgType().equals(RequestMsgType.UPLINK_RPC_MESSAGE) && requestMsg.hasUplinkMsg()) { + onUplinkMsg(requestMsg.getUplinkMsg()); + } + if (requestMsg.getMsgType().equals(RequestMsgType.UPLINK_RPC_MESSAGE) && requestMsg.hasDownlinkResponseMsg()) { + onDownlinkResponse(requestMsg.getDownlinkResponseMsg()); + } + } + } + + @Override + public void onError(Throwable t) { + log.error("Failed to deliver message from client!", t); + closeSession(); + } + + @Override + public void onCompleted() { + closeSession(); + } + + private void closeSession() { + connected = false; + if (edge != null) { + try { + sessionCloseListener.accept(edge.getId()); + } catch (Exception ignored) {} + } + try { + outputStream.onCompleted(); + } catch (Exception ignored) {} + } + }; + } + + private void onUplinkMsg(UplinkMsg uplinkMsg) { + ListenableFuture> future = processUplinkMsg(uplinkMsg); + Futures.addCallback(future, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List result) { + UplinkResponseMsg uplinkResponseMsg = UplinkResponseMsg.newBuilder().setSuccess(true).build(); + sendDownlinkMsg(ResponseMsg.newBuilder() + .setUplinkResponseMsg(uplinkResponseMsg) + .build()); + } + + @Override + public void onFailure(Throwable t) { + UplinkResponseMsg uplinkResponseMsg = UplinkResponseMsg.newBuilder().setSuccess(false).setErrorMsg(t.getMessage()).build(); + sendDownlinkMsg(ResponseMsg.newBuilder() + .setUplinkResponseMsg(uplinkResponseMsg) + .build()); + } + }, MoreExecutors.directExecutor()); + } + + private void onDownlinkResponse(DownlinkResponseMsg msg) { + try { + if (msg.getSuccess()) { + log.debug("[{}] Msg has been processed successfully! {}", edge.getRoutingKey(), msg); + } else { + log.error("[{}] Msg processing failed! Error msg: {}", edge.getRoutingKey(), msg.getErrorMsg()); + } + latch.countDown(); + } catch (Exception e) { + log.error("[{}] Can't process downlink response message [{}]", this.sessionId, msg, e); + } + } + + private void sendDownlinkMsg(ResponseMsg downlinkMsg) { + log.trace("[{}] Sending downlink msg [{}]", this.sessionId, downlinkMsg); + if (isConnected()) { + try { + downlinkMsgLock.lock(); + outputStream.onNext(downlinkMsg); + } catch (Exception e) { + log.error("[{}] Failed to send downlink message [{}]", this.sessionId, downlinkMsg, e); + connected = false; + sessionCloseListener.accept(edge.getId()); + } finally { + downlinkMsgLock.unlock(); + } + log.trace("[{}] Response msg successfully sent [{}]", this.sessionId, downlinkMsg); + } + } + + void onConfigurationUpdate(Edge edge) { + log.debug("[{}] onConfigurationUpdate [{}]", this.sessionId, edge); + this.edge = edge; + EdgeUpdateMsg edgeConfig = EdgeUpdateMsg.newBuilder() + .setConfiguration(constructEdgeConfigProto(edge)).build(); + ResponseMsg edgeConfigMsg = ResponseMsg.newBuilder() + .setEdgeUpdateMsg(edgeConfig) + .build(); + sendDownlinkMsg(edgeConfigMsg); + } + + void processEdgeEvents() throws ExecutionException, InterruptedException { + log.trace("[{}] processHandleMessages started", this.sessionId); + if (isConnected()) { + Long queueStartTs = getQueueStartTs().get(); + TimePageLink pageLink = new TimePageLink( + ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), + 0, + null, + new SortOrder("createdTime", SortOrder.Direction.ASC), + queueStartTs, + null); + PageData pageData; + UUID ifOffset = null; + boolean success = true; + do { + pageData = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), pageLink, true); + if (isConnected() && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] event(s) are going to be processed.", this.sessionId, pageData.getData().size()); + List downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); + log.trace("[{}] [{}] downlink msg(s) are going to be send.", this.sessionId, downlinkMsgsPack.size()); + + latch = new CountDownLatch(downlinkMsgsPack.size()); + for (DownlinkMsg downlinkMsg : downlinkMsgsPack) { + sendDownlinkMsg(ResponseMsg.newBuilder() + .setDownlinkMsg(downlinkMsg) + .build()); + } + + ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId(); + + success = latch.await(10, TimeUnit.SECONDS); + if (!success) { + log.warn("[{}] Failed to deliver the batch: {}", this.sessionId, downlinkMsgsPack); + } + } + if (isConnected() && (!success || pageData.hasNext())) { + try { + Thread.sleep(ctx.getEdgeEventStorageSettings().getSleepIntervalBetweenBatches()); + } catch (InterruptedException e) { + log.error("[{}] Error during sleep between batches", this.sessionId, e); + } + if (success) { + pageLink = pageLink.nextPageLink(); + } + } + } while (isConnected() && (!success || pageData.hasNext())); + + if (ifOffset != null) { + Long newStartTs = Uuids.unixTimestamp(ifOffset); + updateQueueStartTs(newStartTs); + } + } + log.trace("[{}] processHandleMessages finished", this.sessionId); + } + + private List convertToDownlinkMsgsPack(List edgeEvents) { + List result = new ArrayList<>(); + for (EdgeEvent edgeEvent : edgeEvents) { + log.trace("[{}] Processing edge event [{}]", this.sessionId, edgeEvent); + try { + DownlinkMsg downlinkMsg = null; + switch (edgeEvent.getAction()) { + case UPDATED: + case ADDED: + case DELETED: + case ASSIGNED_TO_EDGE: + case UNASSIGNED_FROM_EDGE: + case ALARM_ACK: + case ALARM_CLEAR: + case CREDENTIALS_UPDATED: + case RELATION_ADD_OR_UPDATE: + case RELATION_DELETED: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + downlinkMsg = processEntityMessage(edgeEvent, edgeEvent.getAction()); + break; + case ATTRIBUTES_UPDATED: + case POST_ATTRIBUTES: + case ATTRIBUTES_DELETED: + case TIMESERIES_UPDATED: + downlinkMsg = processTelemetryMessage(edgeEvent); + break; + case CREDENTIALS_REQUEST: + downlinkMsg = processCredentialsRequestMessage(edgeEvent); + break; + case ENTITY_MERGE_REQUEST: + downlinkMsg = processEntityMergeRequestMessage(edgeEvent); + break; + case RPC_CALL: + downlinkMsg = processRpcCallMsg(edgeEvent); + break; + } + if (downlinkMsg != null) { + result.add(downlinkMsg); + } + } catch (Exception e) { + log.error("Exception during processing records from queue", e); + } + } + return result; + } + + private DownlinkMsg processEntityMergeRequestMessage(EdgeEvent edgeEvent) { + DownlinkMsg downlinkMsg = null; + if (EdgeEventType.DEVICE.equals(edgeEvent.getType())) { + DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); + Device device = ctx.getDeviceService().findDeviceById(edge.getTenantId(), deviceId); + CustomerId customerId = getCustomerIdIfEdgeAssignedToCustomer(device); + String conflictName = null; + if(edgeEvent.getBody() != null) { + conflictName = edgeEvent.getBody().get("conflictName").asText(); + } + DeviceUpdateMsg d = ctx.getDeviceMsgConstructor() + .constructDeviceUpdatedMsg(UpdateMsgType.ENTITY_MERGE_RPC_MESSAGE, device, customerId, conflictName); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllDeviceUpdateMsg(Collections.singletonList(d)) + .build(); + } + return downlinkMsg; + } + + private DownlinkMsg processRpcCallMsg(EdgeEvent edgeEvent) { + log.trace("Executing processRpcCall, edgeEvent [{}]", edgeEvent); + DeviceRpcCallMsg deviceRpcCallMsg = + ctx.getDeviceMsgConstructor().constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody()); + return DownlinkMsg.newBuilder() + .addAllDeviceRpcCallMsg(Collections.singletonList(deviceRpcCallMsg)) + .build(); + } + + private DownlinkMsg processCredentialsRequestMessage(EdgeEvent edgeEvent) { + DownlinkMsg downlinkMsg = null; + if (EdgeEventType.DEVICE.equals(edgeEvent.getType())) { + DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); + DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = DeviceCredentialsRequestMsg.newBuilder() + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) + .build(); + DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() + .addAllDeviceCredentialsRequestMsg(Collections.singletonList(deviceCredentialsRequestMsg)); + downlinkMsg = builder.build(); + } + return downlinkMsg; + } + + private ListenableFuture getQueueStartTs() { + ListenableFuture> future = + ctx.getAttributesService().find(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, QUEUE_START_TS_ATTR_KEY); + return Futures.transform(future, attributeKvEntryOpt -> { + if (attributeKvEntryOpt != null && attributeKvEntryOpt.isPresent()) { + AttributeKvEntry attributeKvEntry = attributeKvEntryOpt.get(); + return attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L; + } else { + return 0L; + } + }, ctx.getDbCallbackExecutor()); + } + + private void updateQueueStartTs(Long newStartTs) { + log.trace("[{}] updating QueueStartTs [{}][{}]", this.sessionId, edge.getId(), newStartTs); + newStartTs = ++newStartTs; // increments ts by 1 - next edge event search starts from current offset + 1 + List attributes = Collections.singletonList(new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_TS_ATTR_KEY, newStartTs), System.currentTimeMillis())); + ctx.getAttributesService().save(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, attributes); + } + + private DownlinkMsg processTelemetryMessage(EdgeEvent edgeEvent) { + log.trace("[{}] Executing processTelemetryMessage, edgeEvent [{}]", this.sessionId, edgeEvent); + EntityId entityId = null; + switch (edgeEvent.getType()) { + case DEVICE: + entityId = new DeviceId(edgeEvent.getEntityId()); + break; + case ASSET: + entityId = new AssetId(edgeEvent.getEntityId()); + break; + case ENTITY_VIEW: + entityId = new EntityViewId(edgeEvent.getEntityId()); + break; + case DASHBOARD: + entityId = new DashboardId(edgeEvent.getEntityId()); + break; + case TENANT: + entityId = new TenantId(edgeEvent.getEntityId()); + break; + case CUSTOMER: + entityId = new CustomerId(edgeEvent.getEntityId()); + break; + case EDGE: + entityId = new EdgeId(edgeEvent.getEntityId()); + break; + } + DownlinkMsg downlinkMsg = null; + if (entityId != null) { + log.debug("[{}] Sending telemetry data msg, entityId [{}], body [{}]", this.sessionId, edgeEvent.getEntityId(), edgeEvent.getBody()); + try { + downlinkMsg = constructEntityDataProtoMsg(entityId, edgeEvent.getAction(), JsonUtils.parse(mapper.writeValueAsString(edgeEvent.getBody()))); + } catch (Exception e) { + log.warn("[{}] Can't send telemetry data msg, entityId [{}], body [{}]", this.sessionId, edgeEvent.getEntityId(), edgeEvent.getBody(), e); + } + } + return downlinkMsg; + } + + private DownlinkMsg processEntityMessage(EdgeEvent edgeEvent, EdgeEventActionType action) { + UpdateMsgType msgType = getResponseMsgType(edgeEvent.getAction()); + log.trace("Executing processEntityMessage, edgeEvent [{}], action [{}], msgType [{}]", edgeEvent, action, msgType); + switch (edgeEvent.getType()) { + case DEVICE: + return processDevice(edgeEvent, msgType, action); + case DEVICE_PROFILE: + return processDeviceProfile(edgeEvent, msgType, action); + case ASSET: + return processAsset(edgeEvent, msgType, action); + case ENTITY_VIEW: + return processEntityView(edgeEvent, msgType, action); + case DASHBOARD: + return processDashboard(edgeEvent, msgType, action); + case CUSTOMER: + return processCustomer(edgeEvent, msgType, action); + case RULE_CHAIN: + return processRuleChain(edgeEvent, msgType, action); + case RULE_CHAIN_METADATA: + return processRuleChainMetadata(edgeEvent, msgType); + case ALARM: + return processAlarm(edgeEvent, msgType); + case USER: + return processUser(edgeEvent, msgType, action); + case RELATION: + return processRelation(edgeEvent, msgType); + case WIDGETS_BUNDLE: + return processWidgetsBundle(edgeEvent, msgType, action); + case WIDGET_TYPE: + return processWidgetType(edgeEvent, msgType, action); + case ADMIN_SETTINGS: + return processAdminSettings(edgeEvent); + default: + log.warn("Unsupported edge event type [{}]", edgeEvent); + return null; + } + } + + private DownlinkMsg processDevice(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType edgeEdgeEventActionType) { + DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEdgeEventActionType) { + case ADDED: + case UPDATED: + case ASSIGNED_TO_EDGE: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + Device device = ctx.getDeviceService().findDeviceById(edgeEvent.getTenantId(), deviceId); + if (device != null) { + CustomerId customerId = getCustomerIdIfEdgeAssignedToCustomer(device); + DeviceUpdateMsg deviceUpdateMsg = + ctx.getDeviceMsgConstructor().constructDeviceUpdatedMsg(msgType, device, customerId, null); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllDeviceUpdateMsg(Collections.singletonList(deviceUpdateMsg)) + .build(); + } + break; + case DELETED: + case UNASSIGNED_FROM_EDGE: + DeviceUpdateMsg deviceUpdateMsg = + ctx.getDeviceMsgConstructor().constructDeviceDeleteMsg(deviceId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllDeviceUpdateMsg(Collections.singletonList(deviceUpdateMsg)) + .build(); + break; + case CREDENTIALS_UPDATED: + DeviceCredentials deviceCredentials = ctx.getDeviceCredentialsService().findDeviceCredentialsByDeviceId(edge.getTenantId(), deviceId); + if (deviceCredentials != null) { + DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = + ctx.getDeviceMsgConstructor().constructDeviceCredentialsUpdatedMsg(deviceCredentials); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllDeviceCredentialsUpdateMsg(Collections.singletonList(deviceCredentialsUpdateMsg)) + .build(); + } + break; + } + log.trace("[{}] device processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processDeviceProfile(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType action) { + DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (action) { + case ADDED: + case UPDATED: + DeviceProfile deviceProfile = ctx.getDeviceProfileService().findDeviceProfileById(edgeEvent.getTenantId(), deviceProfileId); + if (deviceProfile != null) { + DeviceProfileUpdateMsg deviceProfileUpdateMsg = + ctx.getDeviceProfileMsgConstructor().constructDeviceProfileUpdatedMsg(msgType, deviceProfile); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllDeviceProfileUpdateMsg(Collections.singletonList(deviceProfileUpdateMsg)) + .build(); + } + break; + case DELETED: + DeviceProfileUpdateMsg deviceProfileUpdateMsg = + ctx.getDeviceProfileMsgConstructor().constructDeviceProfileDeleteMsg(deviceProfileId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllDeviceProfileUpdateMsg(Collections.singletonList(deviceProfileUpdateMsg)) + .build(); + break; + } + log.trace("[{}] device profile processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processAsset(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType action) { + AssetId assetId = new AssetId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (action) { + case ADDED: + case UPDATED: + case ASSIGNED_TO_EDGE: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + Asset asset = ctx.getAssetService().findAssetById(edgeEvent.getTenantId(), assetId); + if (asset != null) { + CustomerId customerId = getCustomerIdIfEdgeAssignedToCustomer(asset); + AssetUpdateMsg assetUpdateMsg = + ctx.getAssetMsgConstructor().constructAssetUpdatedMsg(msgType, asset, customerId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllAssetUpdateMsg(Collections.singletonList(assetUpdateMsg)) + .build(); + } + break; + case DELETED: + case UNASSIGNED_FROM_EDGE: + AssetUpdateMsg assetUpdateMsg = + ctx.getAssetMsgConstructor().constructAssetDeleteMsg(assetId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllAssetUpdateMsg(Collections.singletonList(assetUpdateMsg)) + .build(); + break; + } + log.trace("[{}] asset processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processEntityView(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType action) { + EntityViewId entityViewId = new EntityViewId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (action) { + case ADDED: + case UPDATED: + case ASSIGNED_TO_EDGE: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + EntityView entityView = ctx.getEntityViewService().findEntityViewById(edgeEvent.getTenantId(), entityViewId); + if (entityView != null) { + CustomerId customerId = getCustomerIdIfEdgeAssignedToCustomer(entityView); + EntityViewUpdateMsg entityViewUpdateMsg = + ctx.getEntityViewMsgConstructor().constructEntityViewUpdatedMsg(msgType, entityView, customerId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllEntityViewUpdateMsg(Collections.singletonList(entityViewUpdateMsg)) + .build(); + } + break; + case DELETED: + case UNASSIGNED_FROM_EDGE: + EntityViewUpdateMsg entityViewUpdateMsg = + ctx.getEntityViewMsgConstructor().constructEntityViewDeleteMsg(entityViewId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllEntityViewUpdateMsg(Collections.singletonList(entityViewUpdateMsg)) + .build(); + break; + } + log.trace("[{}] entity view processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processDashboard(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType action) { + DashboardId dashboardId = new DashboardId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (action) { + case ADDED: + case UPDATED: + case ASSIGNED_TO_EDGE: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + Dashboard dashboard = ctx.getDashboardService().findDashboardById(edgeEvent.getTenantId(), dashboardId); + if (dashboard != null) { + CustomerId customerId = null; + if (!edge.getCustomerId().isNullUid() && dashboard.isAssignedToCustomer(edge.getCustomerId())) { + customerId = edge.getCustomerId(); + } + DashboardUpdateMsg dashboardUpdateMsg = + ctx.getDashboardMsgConstructor().constructDashboardUpdatedMsg(msgType, dashboard, customerId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllDashboardUpdateMsg(Collections.singletonList(dashboardUpdateMsg)) + .build(); + } + break; + case DELETED: + case UNASSIGNED_FROM_EDGE: + DashboardUpdateMsg dashboardUpdateMsg = + ctx.getDashboardMsgConstructor().constructDashboardDeleteMsg(dashboardId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllDashboardUpdateMsg(Collections.singletonList(dashboardUpdateMsg)) + .build(); + break; + } + log.trace("[{}] dashboard processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processCustomer(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType action) { + CustomerId customerId = new CustomerId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (action) { + case ADDED: + case UPDATED: + Customer customer = ctx.getCustomerService().findCustomerById(edgeEvent.getTenantId(), customerId); + if (customer != null) { + CustomerUpdateMsg customerUpdateMsg = + ctx.getCustomerMsgConstructor().constructCustomerUpdatedMsg(msgType, customer); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllCustomerUpdateMsg(Collections.singletonList(customerUpdateMsg)) + .build(); + } + break; + case DELETED: + CustomerUpdateMsg customerUpdateMsg = + ctx.getCustomerMsgConstructor().constructCustomerDeleteMsg(customerId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllCustomerUpdateMsg(Collections.singletonList(customerUpdateMsg)) + .build(); + break; + } + log.trace("[{}] customer processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processRuleChain(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType action) { + RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (action) { + case ADDED: + case UPDATED: + case ASSIGNED_TO_EDGE: + RuleChain ruleChain = ctx.getRuleChainService().findRuleChainById(edgeEvent.getTenantId(), ruleChainId); + if (ruleChain != null) { + RuleChainUpdateMsg ruleChainUpdateMsg = + ctx.getRuleChainMsgConstructor().constructRuleChainUpdatedMsg(edge.getRootRuleChainId(), msgType, ruleChain); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllRuleChainUpdateMsg(Collections.singletonList(ruleChainUpdateMsg)) + .build(); + } + break; + case DELETED: + case UNASSIGNED_FROM_EDGE: + downlinkMsg = DownlinkMsg.newBuilder() + .addAllRuleChainUpdateMsg(Collections.singletonList(ctx.getRuleChainMsgConstructor().constructRuleChainDeleteMsg(ruleChainId))) + .build(); + break; + } + log.trace("[{}] rule chain processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processRuleChainMetadata(EdgeEvent edgeEvent, UpdateMsgType msgType) { + RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); + RuleChain ruleChain = ctx.getRuleChainService().findRuleChainById(edgeEvent.getTenantId(), ruleChainId); + DownlinkMsg downlinkMsg = null; + if (ruleChain != null) { + RuleChainMetaData ruleChainMetaData = ctx.getRuleChainService().loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = + ctx.getRuleChainMsgConstructor().constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData); + if (ruleChainMetadataUpdateMsg != null) { + downlinkMsg = DownlinkMsg.newBuilder() + .addAllRuleChainMetadataUpdateMsg(Collections.singletonList(ruleChainMetadataUpdateMsg)) + .build(); + } + } + log.trace("[{}] rule chain metadata processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processUser(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType edgeEdgeEventActionType) { + UserId userId = new UserId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEdgeEventActionType) { + case ADDED: + case UPDATED: + User user = ctx.getUserService().findUserById(edgeEvent.getTenantId(), userId); + if (user != null) { + CustomerId customerId = getCustomerIdIfEdgeAssignedToCustomer(user); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllUserUpdateMsg(Collections.singletonList(ctx.getUserMsgConstructor().constructUserUpdatedMsg(msgType, user, customerId))) + .build(); + } + break; + case DELETED: + downlinkMsg = DownlinkMsg.newBuilder() + .addAllUserUpdateMsg(Collections.singletonList(ctx.getUserMsgConstructor().constructUserDeleteMsg(userId))) + .build(); + break; + case CREDENTIALS_UPDATED: + UserCredentials userCredentialsByUserId = ctx.getUserService().findUserCredentialsByUserId(edge.getTenantId(), userId); + if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { + UserCredentialsUpdateMsg userCredentialsUpdateMsg = + ctx.getUserMsgConstructor().constructUserCredentialsUpdatedMsg(userCredentialsByUserId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllUserCredentialsUpdateMsg(Collections.singletonList(userCredentialsUpdateMsg)) + .build(); + } + } + log.trace("[{}] user processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private CustomerId getCustomerIdIfEdgeAssignedToCustomer(HasCustomerId hasCustomerIdEntity) { + if (!edge.getCustomerId().isNullUid() && edge.getCustomerId().equals(hasCustomerIdEntity.getCustomerId())) { + return edge.getCustomerId(); + } else { + return null; + } + } + + private DownlinkMsg processRelation(EdgeEvent edgeEvent, UpdateMsgType msgType) { + EntityRelation entityRelation = mapper.convertValue(edgeEvent.getBody(), EntityRelation.class); + RelationUpdateMsg r = ctx.getRelationMsgConstructor().constructRelationUpdatedMsg(msgType, entityRelation); + DownlinkMsg downlinkMsg = DownlinkMsg.newBuilder() + .addAllRelationUpdateMsg(Collections.singletonList(r)) + .build(); + log.trace("[{}] relation processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processAlarm(EdgeEvent edgeEvent, UpdateMsgType msgType) { + DownlinkMsg downlinkMsg = null; + try { + AlarmId alarmId = new AlarmId(edgeEvent.getEntityId()); + Alarm alarm = ctx.getAlarmService().findAlarmByIdAsync(edgeEvent.getTenantId(), alarmId).get(); + if (alarm != null) { + downlinkMsg = DownlinkMsg.newBuilder() + .addAllAlarmUpdateMsg(Collections.singletonList(ctx.getAlarmMsgConstructor().constructAlarmUpdatedMsg(edge.getTenantId(), msgType, alarm))) + .build(); + } + } catch (Exception e) { + log.error("Can't process alarm msg [{}] [{}]", edgeEvent, msgType, e); + } + log.trace("[{}] alarm processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processWidgetsBundle(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType edgeEdgeEventActionType) { + WidgetsBundleId widgetsBundleId = new WidgetsBundleId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEdgeEventActionType) { + case ADDED: + case UPDATED: + WidgetsBundle widgetsBundle = ctx.getWidgetsBundleService().findWidgetsBundleById(edgeEvent.getTenantId(), widgetsBundleId); + if (widgetsBundle != null) { + WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = + ctx.getWidgetsBundleMsgConstructor().constructWidgetsBundleUpdateMsg(msgType, widgetsBundle); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllWidgetsBundleUpdateMsg(Collections.singletonList(widgetsBundleUpdateMsg)) + .build(); + } + break; + case DELETED: + WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = + ctx.getWidgetsBundleMsgConstructor().constructWidgetsBundleDeleteMsg(widgetsBundleId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllWidgetsBundleUpdateMsg(Collections.singletonList(widgetsBundleUpdateMsg)) + .build(); + break; + } + log.trace("[{}] widget bundle processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processWidgetType(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType edgeEdgeEventActionType) { + WidgetTypeId widgetTypeId = new WidgetTypeId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (edgeEdgeEventActionType) { + case ADDED: + case UPDATED: + WidgetType widgetType = ctx.getWidgetTypeService().findWidgetTypeById(edgeEvent.getTenantId(), widgetTypeId); + if (widgetType != null) { + WidgetTypeUpdateMsg widgetTypeUpdateMsg = + ctx.getWidgetTypeMsgConstructor().constructWidgetTypeUpdateMsg(msgType, widgetType); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllWidgetTypeUpdateMsg(Collections.singletonList(widgetTypeUpdateMsg)) + .build(); + } + break; + case DELETED: + WidgetTypeUpdateMsg widgetTypeUpdateMsg = + ctx.getWidgetTypeMsgConstructor().constructWidgetTypeDeleteMsg(widgetTypeId); + downlinkMsg = DownlinkMsg.newBuilder() + .addAllWidgetTypeUpdateMsg(Collections.singletonList(widgetTypeUpdateMsg)) + .build(); + break; + } + log.trace("[{}] widget type processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private DownlinkMsg processAdminSettings(EdgeEvent edgeEvent) { + AdminSettings adminSettings = mapper.convertValue(edgeEvent.getBody(), AdminSettings.class); + AdminSettingsUpdateMsg t = ctx.getAdminSettingsMsgConstructor().constructAdminSettingsUpdateMsg(adminSettings); + DownlinkMsg downlinkMsg = DownlinkMsg.newBuilder() + .addAllAdminSettingsUpdateMsg(Collections.singletonList(t)) + .build(); + log.trace("[{}] admin settings processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private UpdateMsgType getResponseMsgType(EdgeEventActionType actionType) { + switch (actionType) { + case UPDATED: + case CREDENTIALS_UPDATED: + case ASSIGNED_TO_CUSTOMER: + case UNASSIGNED_FROM_CUSTOMER: + return UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE; + case ADDED: + case ASSIGNED_TO_EDGE: + case RELATION_ADD_OR_UPDATE: + return UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE; + case DELETED: + case UNASSIGNED_FROM_EDGE: + case RELATION_DELETED: + return UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE; + case ALARM_ACK: + return UpdateMsgType.ALARM_ACK_RPC_MESSAGE; + case ALARM_CLEAR: + return UpdateMsgType.ALARM_CLEAR_RPC_MESSAGE; + default: + throw new RuntimeException("Unsupported actionType [" + actionType + "]"); + } + } + + private DownlinkMsg constructEntityDataProtoMsg(EntityId entityId, EdgeEventActionType actionType, JsonElement entityData) { + EntityDataProto entityDataProto = ctx.getEntityDataMsgConstructor().constructEntityDataMsg(entityId, actionType, entityData); + DownlinkMsg downlinkMsg = DownlinkMsg.newBuilder() + .addAllEntityData(Collections.singletonList(entityDataProto)) + .build(); + log.trace("[{}] entity data proto processed [{}]", this.sessionId, downlinkMsg); + return downlinkMsg; + } + + private ListenableFuture> processUplinkMsg(UplinkMsg uplinkMsg) { + List> result = new ArrayList<>(); + try { + if (uplinkMsg.getEntityDataCount() > 0) { + for (EntityDataProto entityData : uplinkMsg.getEntityDataList()) { + result.addAll(ctx.getTelemetryProcessor().onTelemetryUpdate(edge.getTenantId(), entityData)); + } + } + if (uplinkMsg.getDeviceUpdateMsgCount() > 0) { + for (DeviceUpdateMsg deviceUpdateMsg : uplinkMsg.getDeviceUpdateMsgList()) { + result.add(ctx.getDeviceProcessor().onDeviceUpdate(edge.getTenantId(), edge, deviceUpdateMsg)); + } + } + if (uplinkMsg.getDeviceCredentialsUpdateMsgCount() > 0) { + for (DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg : uplinkMsg.getDeviceCredentialsUpdateMsgList()) { + result.add(ctx.getDeviceProcessor().onDeviceCredentialsUpdate(edge.getTenantId(), deviceCredentialsUpdateMsg)); + } + } + if (uplinkMsg.getAlarmUpdateMsgCount() > 0) { + for (AlarmUpdateMsg alarmUpdateMsg : uplinkMsg.getAlarmUpdateMsgList()) { + result.add(ctx.getAlarmProcessor().onAlarmUpdate(edge.getTenantId(), alarmUpdateMsg)); + } + } + if (uplinkMsg.getRelationUpdateMsgCount() > 0) { + for (RelationUpdateMsg relationUpdateMsg : uplinkMsg.getRelationUpdateMsgList()) { + result.add(ctx.getRelationProcessor().onRelationUpdate(edge.getTenantId(), relationUpdateMsg)); + } + } + if (uplinkMsg.getRuleChainMetadataRequestMsgCount() > 0) { + for (RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg : uplinkMsg.getRuleChainMetadataRequestMsgList()) { + result.add(ctx.getSyncEdgeService().processRuleChainMetadataRequestMsg(edge.getTenantId(), edge, ruleChainMetadataRequestMsg)); + } + } + if (uplinkMsg.getAttributesRequestMsgCount() > 0) { + for (AttributesRequestMsg attributesRequestMsg : uplinkMsg.getAttributesRequestMsgList()) { + result.add(ctx.getSyncEdgeService().processAttributesRequestMsg(edge.getTenantId(), edge, attributesRequestMsg)); + } + } + if (uplinkMsg.getRelationRequestMsgCount() > 0) { + for (RelationRequestMsg relationRequestMsg : uplinkMsg.getRelationRequestMsgList()) { + result.add(ctx.getSyncEdgeService().processRelationRequestMsg(edge.getTenantId(), edge, relationRequestMsg)); + } + } + if (uplinkMsg.getUserCredentialsRequestMsgCount() > 0) { + for (UserCredentialsRequestMsg userCredentialsRequestMsg : uplinkMsg.getUserCredentialsRequestMsgList()) { + result.add(ctx.getSyncEdgeService().processUserCredentialsRequestMsg(edge.getTenantId(), edge, userCredentialsRequestMsg)); + } + } + if (uplinkMsg.getDeviceCredentialsRequestMsgCount() > 0) { + for (DeviceCredentialsRequestMsg deviceCredentialsRequestMsg : uplinkMsg.getDeviceCredentialsRequestMsgList()) { + result.add(ctx.getSyncEdgeService().processDeviceCredentialsRequestMsg(edge.getTenantId(), edge, deviceCredentialsRequestMsg)); + } + } + if (uplinkMsg.getDeviceRpcCallMsgCount() > 0) { + for (DeviceRpcCallMsg deviceRpcCallMsg : uplinkMsg.getDeviceRpcCallMsgList()) { + result.add(ctx.getDeviceProcessor().processDeviceRpcCallResponseMsg(edge.getTenantId(), deviceRpcCallMsg)); + } + } + } catch (Exception e) { + log.error("[{}] Can't process uplink msg [{}]", this.sessionId, uplinkMsg, e); + } + return Futures.allAsList(result); + } + + private ConnectResponseMsg processConnect(ConnectRequestMsg request) { + log.trace("[{}] processConnect [{}]", this.sessionId, request); + Optional optional = ctx.getEdgeService().findEdgeByRoutingKey(TenantId.SYS_TENANT_ID, request.getEdgeRoutingKey()); + if (optional.isPresent()) { + edge = optional.get(); + try { + if (edge.getSecret().equals(request.getEdgeSecret())) { + sessionOpenListener.accept(edge.getId(), this); + return ConnectResponseMsg.newBuilder() + .setResponseCode(ConnectResponseCode.ACCEPTED) + .setErrorMsg("") + .setConfiguration(constructEdgeConfigProto(edge)).build(); + } + return ConnectResponseMsg.newBuilder() + .setResponseCode(ConnectResponseCode.BAD_CREDENTIALS) + .setErrorMsg("Failed to validate the edge!") + .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); + } catch (Exception e) { + log.error("[{}] Failed to process edge connection!", request.getEdgeRoutingKey(), e); + return ConnectResponseMsg.newBuilder() + .setResponseCode(ConnectResponseCode.SERVER_UNAVAILABLE) + .setErrorMsg("Failed to process edge connection!") + .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); + } + } + return ConnectResponseMsg.newBuilder() + .setResponseCode(ConnectResponseCode.BAD_CREDENTIALS) + .setErrorMsg("Failed to find the edge! Routing key: " + request.getEdgeRoutingKey()) + .setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); + } + + private EdgeConfiguration constructEdgeConfigProto(Edge edge) { + EdgeConfiguration.Builder builder = EdgeConfiguration.newBuilder() + .setEdgeIdMSB(edge.getId().getId().getMostSignificantBits()) + .setEdgeIdLSB(edge.getId().getId().getLeastSignificantBits()) + .setTenantIdMSB(edge.getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(edge.getTenantId().getId().getLeastSignificantBits()) + .setName(edge.getName()) + .setType(edge.getType()) + .setRoutingKey(edge.getRoutingKey()) + .setSecret(edge.getSecret()) + .setEdgeLicenseKey(edge.getEdgeLicenseKey()) + .setCloudEndpoint(edge.getCloudEndpoint()) + .setAdditionalInfo(JacksonUtil.toString(edge.getAdditionalInfo())) + .setCloudType("CE"); + if (edge.getCustomerId() != null) { + builder.setCustomerIdMSB(edge.getCustomerId().getId().getMostSignificantBits()) + .setCustomerIdLSB(edge.getCustomerId().getId().getLeastSignificantBits()); + } + return builder + .build(); + } + + @Override + public void close() { + log.debug("[{}] Closing session", sessionId); + connected = false; + try { + outputStream.onCompleted(); + } catch (Exception e) { + log.debug("[{}] Failed to close output stream: {}", sessionId, e.getMessage()); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java new file mode 100644 index 0000000000..ddea009849 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2021 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; + +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.EdgeId; + +public interface EdgeRpcService { + + void updateEdge(Edge edge); + + void deleteEdge(EdgeId edgeId); + + void onEdgeEvent(EdgeId edgeId); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AdminSettingsMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AdminSettingsMsgConstructor.java new file mode 100644 index 0000000000..891b42f815 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AdminSettingsMsgConstructor.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2021 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.constructor; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.AdminSettingsUpdateMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class AdminSettingsMsgConstructor { + + public AdminSettingsUpdateMsg constructAdminSettingsUpdateMsg(AdminSettings adminSettings) { + AdminSettingsUpdateMsg.Builder builder = AdminSettingsUpdateMsg.newBuilder() + .setKey(adminSettings.getKey()) + .setJsonValue(JacksonUtil.toString(adminSettings.getJsonValue())); + if (adminSettings.getId() != null) { + builder.setIsSystem(true); + } + return builder.build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java new file mode 100644 index 0000000000..9d471b76ef --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2021 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.constructor; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class AlarmMsgConstructor { + + @Autowired + private DeviceService deviceService; + + @Autowired + private AssetService assetService; + + @Autowired + private EntityViewService entityViewService; + + public AlarmUpdateMsg constructAlarmUpdatedMsg(TenantId tenantId, UpdateMsgType msgType, Alarm alarm) { + String entityName = null; + switch (alarm.getOriginator().getEntityType()) { + case DEVICE: + entityName = deviceService.findDeviceById(tenantId, new DeviceId(alarm.getOriginator().getId())).getName(); + break; + case ASSET: + entityName = assetService.findAssetById(tenantId, new AssetId(alarm.getOriginator().getId())).getName(); + break; + case ENTITY_VIEW: + entityName = entityViewService.findEntityViewById(tenantId, new EntityViewId(alarm.getOriginator().getId())).getName(); + break; + } + AlarmUpdateMsg.Builder builder = AlarmUpdateMsg.newBuilder() + .setMsgType(msgType) + .setName(alarm.getName()) + .setType(alarm.getType()) + .setOriginatorName(entityName) + .setOriginatorType(alarm.getOriginator().getEntityType().name()) + .setSeverity(alarm.getSeverity().name()) + .setStatus(alarm.getStatus().name()) + .setStartTs(alarm.getStartTs()) + .setEndTs(alarm.getEndTs()) + .setAckTs(alarm.getAckTs()) + .setClearTs(alarm.getClearTs()) + .setDetails(JacksonUtil.toString(alarm.getDetails())) + .setPropagate(alarm.isPropagate()); + return builder.build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java new file mode 100644 index 0000000000..f78956b4f1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2021 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.constructor; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class AssetMsgConstructor { + + public AssetUpdateMsg constructAssetUpdatedMsg(UpdateMsgType msgType, Asset asset, CustomerId customerId) { + AssetUpdateMsg.Builder builder = AssetUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(asset.getId().getId().getMostSignificantBits()) + .setIdLSB(asset.getId().getId().getLeastSignificantBits()) + .setName(asset.getName()) + .setType(asset.getType()); + if (asset.getLabel() != null) { + builder.setLabel(asset.getLabel()); + } + if (customerId != null) { + builder.setCustomerIdMSB(customerId.getId().getMostSignificantBits()); + builder.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); + } + if (asset.getAdditionalInfo() != null) { + builder.setAdditionalInfo(JacksonUtil.toString(asset.getAdditionalInfo())); + } + return builder.build(); + } + + public AssetUpdateMsg constructAssetDeleteMsg(AssetId assetId) { + return AssetUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(assetId.getId().getMostSignificantBits()) + .setIdLSB(assetId.getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/CustomerMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/CustomerMsgConstructor.java new file mode 100644 index 0000000000..5c0ec9ff4b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/CustomerMsgConstructor.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2021 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.constructor; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.CustomerUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class CustomerMsgConstructor { + + public CustomerUpdateMsg constructCustomerUpdatedMsg(UpdateMsgType msgType, Customer customer) { + CustomerUpdateMsg.Builder builder = CustomerUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(customer.getId().getId().getMostSignificantBits()) + .setIdLSB(customer.getId().getId().getLeastSignificantBits()) + .setTitle(customer.getTitle()); + if (customer.getCountry() != null) { + builder.setCountry(customer.getCountry()); + } + if (customer.getState() != null) { + builder.setState(customer.getState()); + } + if (customer.getCity() != null) { + builder.setCity(customer.getCity()); + } + if (customer.getAddress() != null) { + builder.setAddress(customer.getAddress()); + } + if (customer.getAddress2() != null) { + builder.setAddress2(customer.getAddress2()); + } + if (customer.getZip() != null) { + builder.setZip(customer.getZip()); + } + if (customer.getPhone() != null) { + builder.setPhone(customer.getPhone()); + } + if (customer.getEmail() != null) { + builder.setEmail(customer.getEmail()); + } + if (customer.getAdditionalInfo() != null) { + builder.setAdditionalInfo(JacksonUtil.toString(customer.getAdditionalInfo())); + } + return builder.build(); + } + + public CustomerUpdateMsg constructCustomerDeleteMsg(CustomerId customerId) { + return CustomerUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(customerId.getId().getMostSignificantBits()) + .setIdLSB(customerId.getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DashboardMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DashboardMsgConstructor.java new file mode 100644 index 0000000000..a550d52395 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DashboardMsgConstructor.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2021 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.constructor; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.DashboardUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class DashboardMsgConstructor { + + public DashboardUpdateMsg constructDashboardUpdatedMsg(UpdateMsgType msgType, Dashboard dashboard, CustomerId customerId) { + DashboardUpdateMsg.Builder builder = DashboardUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(dashboard.getId().getId().getMostSignificantBits()) + .setIdLSB(dashboard.getId().getId().getLeastSignificantBits()) + .setTitle(dashboard.getTitle()) + .setConfiguration(JacksonUtil.toString(dashboard.getConfiguration())); + if (customerId != null) { + builder.setCustomerIdMSB(customerId.getId().getMostSignificantBits()); + builder.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); + } + return builder.build(); + } + + public DashboardUpdateMsg constructDashboardDeleteMsg(DashboardId dashboardId) { + return DashboardUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(dashboardId.getId().getMostSignificantBits()) + .setIdLSB(dashboardId.getId().getLeastSignificantBits()).build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java new file mode 100644 index 0000000000..df7bfc9e1a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java @@ -0,0 +1,111 @@ +/** + * Copyright © 2016-2021 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.constructor; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.gen.edge.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceRpcCallMsg; +import org.thingsboard.server.gen.edge.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.RpcRequestMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.UUID; + +@Component +@TbCoreComponent +public class DeviceMsgConstructor { + + protected static final ObjectMapper mapper = new ObjectMapper(); + + public DeviceUpdateMsg constructDeviceUpdatedMsg(UpdateMsgType msgType, Device device, CustomerId customerId, String conflictName) { + DeviceUpdateMsg.Builder builder = DeviceUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(device.getId().getId().getMostSignificantBits()) + .setIdLSB(device.getId().getId().getLeastSignificantBits()) + .setName(device.getName()) + .setType(device.getType()); + if (device.getLabel() != null) { + builder.setLabel(device.getLabel()); + } + if (customerId != null) { + builder.setCustomerIdMSB(customerId.getId().getMostSignificantBits()); + builder.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); + } + if (device.getDeviceProfileId() != null) { + builder.setDeviceProfileIdMSB(device.getDeviceProfileId().getId().getMostSignificantBits()); + builder.setDeviceProfileIdLSB(device.getDeviceProfileId().getId().getLeastSignificantBits()); + } + if (device.getAdditionalInfo() != null) { + builder.setAdditionalInfo(JacksonUtil.toString(device.getAdditionalInfo())); + } + if (conflictName != null) { + builder.setConflictName(conflictName); + } + return builder.build(); + } + + public DeviceCredentialsUpdateMsg constructDeviceCredentialsUpdatedMsg(DeviceCredentials deviceCredentials) { + DeviceCredentialsUpdateMsg.Builder builder = DeviceCredentialsUpdateMsg.newBuilder() + .setDeviceIdMSB(deviceCredentials.getDeviceId().getId().getMostSignificantBits()) + .setDeviceIdLSB(deviceCredentials.getDeviceId().getId().getLeastSignificantBits()); + if (deviceCredentials.getCredentialsType() != null) { + builder.setCredentialsType(deviceCredentials.getCredentialsType().name()) + .setCredentialsId(deviceCredentials.getCredentialsId()); + } + if (deviceCredentials.getCredentialsValue() != null) { + builder.setCredentialsValue(deviceCredentials.getCredentialsValue()); + } + return builder.build(); + } + + public DeviceUpdateMsg constructDeviceDeleteMsg(DeviceId deviceId) { + return DeviceUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(deviceId.getId().getMostSignificantBits()) + .setIdLSB(deviceId.getId().getLeastSignificantBits()).build(); + } + + public DeviceRpcCallMsg constructDeviceRpcCallMsg(UUID deviceId, JsonNode body) { + int requestId = body.get("requestId").asInt(); + boolean oneway = body.get("oneway").asBoolean(); + UUID requestUUID = UUID.fromString(body.get("requestUUID").asText()); + long expirationTime = body.get("expirationTime").asLong(); + String method = body.get("method").asText(); + String params = body.get("params").asText(); + + RpcRequestMsg.Builder requestBuilder = RpcRequestMsg.newBuilder(); + requestBuilder.setMethod(method); + requestBuilder.setParams(params); + DeviceRpcCallMsg.Builder builder = DeviceRpcCallMsg.newBuilder() + .setDeviceIdMSB(deviceId.getMostSignificantBits()) + .setDeviceIdLSB(deviceId.getLeastSignificantBits()) + .setRequestUuidMSB(requestUUID.getMostSignificantBits()) + .setRequestUuidLSB(requestUUID.getLeastSignificantBits()) + .setRequestId(requestId) + .setExpirationTime(expirationTime) + .setOneway(oneway) + .setRequestMsg(requestBuilder.build()); + return builder.build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java new file mode 100644 index 0000000000..c3f5346170 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java @@ -0,0 +1,74 @@ +/** + * Copyright © 2016-2021 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.constructor; + +import com.google.protobuf.ByteString; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; +import org.thingsboard.server.gen.edge.DeviceProfileUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class DeviceProfileMsgConstructor { + + @Autowired + private DataDecodingEncodingService dataDecodingEncodingService; + + public DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile) { + DeviceProfileUpdateMsg.Builder builder = DeviceProfileUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(deviceProfile.getId().getId().getMostSignificantBits()) + .setIdLSB(deviceProfile.getId().getId().getLeastSignificantBits()) + .setName(deviceProfile.getName()) + .setDefault(deviceProfile.isDefault()) + .setType(deviceProfile.getType().name()) + .setProfileDataBytes(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile.getProfileData()))); + // TODO: voba - should this be always null at the moment?? +// if (deviceProfile.getDefaultRuleChainId() != null) { +// builder.setDefaultRuleChainIdMSB(deviceProfile.getDefaultRuleChainId().getId().getMostSignificantBits()) +// .setDefaultRuleChainIdLSB(deviceProfile.getDefaultRuleChainId().getId().getLeastSignificantBits()); +// } +// if (deviceProfile.getDefaultQueueName() != null) { +// builder.setDefaultQueueName(deviceProfile.getDefaultQueueName()); +// } + if (deviceProfile.getDescription() != null) { + builder.setDescription(deviceProfile.getDescription()); + } + if (deviceProfile.getTransportType() != null) { + builder.setTransportType(deviceProfile.getTransportType().name()); + } + if (deviceProfile.getProvisionType() != null) { + builder.setProvisionType(deviceProfile.getProvisionType().name()); + } + if (deviceProfile.getProvisionDeviceKey() != null) { + builder.setProvisionDeviceKey(deviceProfile.getProvisionDeviceKey()); + } + return builder.build(); + } + + public DeviceProfileUpdateMsg constructDeviceProfileDeleteMsg(DeviceProfileId deviceProfileId) { + return DeviceProfileUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(deviceProfileId.getId().getMostSignificantBits()) + .setIdLSB(deviceProfileId.getId().getLeastSignificantBits()).build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java new file mode 100644 index 0000000000..1546572531 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java @@ -0,0 +1,97 @@ +/** + * Copyright © 2016-2021 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.constructor; + +import com.google.gson.reflect.TypeToken; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.gen.edge.AttributeDeleteMsg; +import org.thingsboard.server.gen.edge.EntityDataProto; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.List; + +@Component +@Slf4j +@TbCoreComponent +public class EntityDataMsgConstructor { + + public EntityDataProto constructEntityDataMsg(EntityId entityId, EdgeEventActionType actionType, JsonElement entityData) { + EntityDataProto.Builder builder = EntityDataProto.newBuilder() + .setEntityIdMSB(entityId.getId().getMostSignificantBits()) + .setEntityIdLSB(entityId.getId().getLeastSignificantBits()) + .setEntityType(entityId.getEntityType().name()); + switch (actionType) { + case TIMESERIES_UPDATED: + try { + JsonObject data = entityData.getAsJsonObject(); + long ts; + if (data.get("ts") != null && !data.get("ts").isJsonNull()) { + ts = data.getAsJsonPrimitive("ts").getAsLong(); + } else { + ts = System.currentTimeMillis(); + } + builder.setPostTelemetryMsg(JsonConverter.convertToTelemetryProto(data.getAsJsonObject("data"), ts)); + } catch (Exception e) { + log.warn("[{}] Can't convert to telemetry proto, entityData [{}]", entityId, entityData, e); + } + break; + case ATTRIBUTES_UPDATED: + try { + JsonObject data = entityData.getAsJsonObject(); + TransportProtos.PostAttributeMsg attributesUpdatedMsg = JsonConverter.convertToAttributesProto(data.getAsJsonObject("kv")); + builder.setAttributesUpdatedMsg(attributesUpdatedMsg); + builder.setPostAttributeScope(data.getAsJsonPrimitive("scope").getAsString()); + } catch (Exception e) { + log.warn("[{}] Can't convert to AttributesUpdatedMsg proto, entityData [{}]", entityId, entityData, e); + } + break; + case POST_ATTRIBUTES: + try { + JsonObject data = entityData.getAsJsonObject(); + TransportProtos.PostAttributeMsg postAttributesMsg = JsonConverter.convertToAttributesProto(data.getAsJsonObject("kv")); + builder.setPostAttributesMsg(postAttributesMsg); + builder.setPostAttributeScope(data.getAsJsonPrimitive("scope").getAsString()); + } catch (Exception e) { + log.warn("[{}] Can't convert to PostAttributesMsg, entityData [{}]", entityId, entityData, e); + } + break; + case ATTRIBUTES_DELETED: + try { + AttributeDeleteMsg.Builder attributeDeleteMsg = AttributeDeleteMsg.newBuilder(); + attributeDeleteMsg.setScope(entityData.getAsJsonObject().getAsJsonPrimitive("scope").getAsString()); + JsonArray jsonArray = entityData.getAsJsonObject().getAsJsonArray("keys"); + List keys = new Gson().fromJson(jsonArray.toString(), new TypeToken<>(){}.getType()); + attributeDeleteMsg.addAllAttributeNames(keys); + attributeDeleteMsg.build(); + builder.setAttributeDeleteMsg(attributeDeleteMsg); + } catch (Exception e) { + log.warn("[{}] Can't convert to AttributeDeleteMsg proto, entityData [{}]", entityId, entityData, e); + } + break; + } + return builder.build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityViewMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityViewMsgConstructor.java new file mode 100644 index 0000000000..0f1e3d9e49 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityViewMsgConstructor.java @@ -0,0 +1,69 @@ +/** + * Copyright © 2016-2021 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.constructor; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.EdgeEntityType; +import org.thingsboard.server.gen.edge.EntityViewUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class EntityViewMsgConstructor { + + public EntityViewUpdateMsg constructEntityViewUpdatedMsg(UpdateMsgType msgType, EntityView entityView, CustomerId customerId) { + EdgeEntityType entityType; + switch (entityView.getEntityId().getEntityType()) { + case DEVICE: + entityType = EdgeEntityType.DEVICE; + break; + case ASSET: + entityType = EdgeEntityType.ASSET; + break; + default: + throw new RuntimeException("Unsupported entity type [" + entityView.getEntityId().getEntityType() + "]"); + } + EntityViewUpdateMsg.Builder builder = EntityViewUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(entityView.getId().getId().getMostSignificantBits()) + .setIdLSB(entityView.getId().getId().getLeastSignificantBits()) + .setName(entityView.getName()) + .setType(entityView.getType()) + .setEntityIdMSB(entityView.getEntityId().getId().getMostSignificantBits()) + .setEntityIdLSB(entityView.getEntityId().getId().getLeastSignificantBits()) + .setEntityType(entityType); + if (customerId != null) { + builder.setCustomerIdMSB(customerId.getId().getMostSignificantBits()); + builder.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); + } + if (entityView.getAdditionalInfo() != null) { + builder.setAdditionalInfo(JacksonUtil.toString(entityView.getAdditionalInfo())); + } + return builder.build(); + } + + public EntityViewUpdateMsg constructEntityViewDeleteMsg(EntityViewId entityViewId) { + return EntityViewUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(entityViewId.getId().getMostSignificantBits()) + .setIdLSB(entityViewId.getId().getLeastSignificantBits()).build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RelationMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RelationMsgConstructor.java new file mode 100644 index 0000000000..c805085b6b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RelationMsgConstructor.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2021 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.constructor; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.RelationUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class RelationMsgConstructor { + + public RelationUpdateMsg constructRelationUpdatedMsg(UpdateMsgType msgType, EntityRelation entityRelation) { + RelationUpdateMsg.Builder builder = RelationUpdateMsg.newBuilder() + .setMsgType(msgType) + .setFromIdMSB(entityRelation.getFrom().getId().getMostSignificantBits()) + .setFromIdLSB(entityRelation.getFrom().getId().getLeastSignificantBits()) + .setFromEntityType(entityRelation.getFrom().getEntityType().name()) + .setToIdMSB(entityRelation.getTo().getId().getMostSignificantBits()) + .setToIdLSB(entityRelation.getTo().getId().getLeastSignificantBits()) + .setToEntityType(entityRelation.getTo().getEntityType().name()) + .setType(entityRelation.getType()) + .setAdditionalInfo(JacksonUtil.toString(entityRelation.getAdditionalInfo())); + if (entityRelation.getTypeGroup() != null) { + builder.setTypeGroup(entityRelation.getTypeGroup().name()); + } + return builder.build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructor.java new file mode 100644 index 0000000000..c1bd3d816b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructor.java @@ -0,0 +1,151 @@ +/** + * Copyright © 2016-2021 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.constructor; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.rule.NodeConnectionInfo; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.NodeConnectionInfoProto; +import org.thingsboard.server.gen.edge.RuleChainConnectionInfoProto; +import org.thingsboard.server.gen.edge.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.RuleNodeProto; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.ArrayList; +import java.util.List; + +@Component +@Slf4j +@TbCoreComponent +public class RuleChainMsgConstructor { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public RuleChainUpdateMsg constructRuleChainUpdatedMsg(RuleChainId edgeRootRuleChainId, UpdateMsgType msgType, RuleChain ruleChain) { + RuleChainUpdateMsg.Builder builder = RuleChainUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(ruleChain.getId().getId().getMostSignificantBits()) + .setIdLSB(ruleChain.getId().getId().getLeastSignificantBits()) + .setName(ruleChain.getName()) + .setRoot(ruleChain.getId().equals(edgeRootRuleChainId)) + .setDebugMode(ruleChain.isDebugMode()) + .setConfiguration(JacksonUtil.toString(ruleChain.getConfiguration())); + if (ruleChain.getFirstRuleNodeId() != null) { + builder.setFirstRuleNodeIdMSB(ruleChain.getFirstRuleNodeId().getId().getMostSignificantBits()) + .setFirstRuleNodeIdLSB(ruleChain.getFirstRuleNodeId().getId().getLeastSignificantBits()); + } + return builder.build(); + } + + public RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData) { + try { + RuleChainMetadataUpdateMsg.Builder builder = RuleChainMetadataUpdateMsg.newBuilder() + .setRuleChainIdMSB(ruleChainMetaData.getRuleChainId().getId().getMostSignificantBits()) + .setRuleChainIdLSB(ruleChainMetaData.getRuleChainId().getId().getLeastSignificantBits()) + .addAllNodes(constructNodes(ruleChainMetaData.getNodes())) + .addAllConnections(constructConnections(ruleChainMetaData.getConnections())) + .addAllRuleChainConnections(constructRuleChainConnections(ruleChainMetaData.getRuleChainConnections())); + if (ruleChainMetaData.getFirstNodeIndex() != null) { + builder.setFirstNodeIndex(ruleChainMetaData.getFirstNodeIndex()); + } else { + builder.setFirstNodeIndex(-1); + } + builder.setMsgType(msgType); + return builder.build(); + } catch (JsonProcessingException ex) { + log.error("Can't construct RuleChainMetadataUpdateMsg", ex); + } + return null; + } + + private List constructConnections(List connections) { + List result = new ArrayList<>(); + if (connections != null && !connections.isEmpty()) { + for (NodeConnectionInfo connection : connections) { + result.add(constructConnection(connection)); + } + } + return result; + } + + private NodeConnectionInfoProto constructConnection(NodeConnectionInfo connection) { + return NodeConnectionInfoProto.newBuilder() + .setFromIndex(connection.getFromIndex()) + .setToIndex(connection.getToIndex()) + .setType(connection.getType()) + .build(); + } + + private List constructNodes(List nodes) throws JsonProcessingException { + List result = new ArrayList<>(); + if (nodes != null && !nodes.isEmpty()) { + for (RuleNode node : nodes) { + result.add(constructNode(node)); + } + } + return result; + } + + private List constructRuleChainConnections(List ruleChainConnections) throws JsonProcessingException { + List result = new ArrayList<>(); + if (ruleChainConnections != null && !ruleChainConnections.isEmpty()) { + for (RuleChainConnectionInfo ruleChainConnectionInfo : ruleChainConnections) { + result.add(constructRuleChainConnection(ruleChainConnectionInfo)); + } + } + return result; + } + + private RuleChainConnectionInfoProto constructRuleChainConnection(RuleChainConnectionInfo ruleChainConnectionInfo) throws JsonProcessingException { + return RuleChainConnectionInfoProto.newBuilder() + .setFromIndex(ruleChainConnectionInfo.getFromIndex()) + .setTargetRuleChainIdMSB(ruleChainConnectionInfo.getTargetRuleChainId().getId().getMostSignificantBits()) + .setTargetRuleChainIdLSB(ruleChainConnectionInfo.getTargetRuleChainId().getId().getLeastSignificantBits()) + .setType(ruleChainConnectionInfo.getType()) + .setAdditionalInfo(objectMapper.writeValueAsString(ruleChainConnectionInfo.getAdditionalInfo())) + .build(); + } + + private RuleNodeProto constructNode(RuleNode node) throws JsonProcessingException { + return RuleNodeProto.newBuilder() + .setIdMSB(node.getId().getId().getMostSignificantBits()) + .setIdLSB(node.getId().getId().getLeastSignificantBits()) + .setType(node.getType()) + .setName(node.getName()) + .setDebugMode(node.isDebugMode()) + .setConfiguration(objectMapper.writeValueAsString(node.getConfiguration())) + .setAdditionalInfo(objectMapper.writeValueAsString(node.getAdditionalInfo())) + .build(); + } + + public RuleChainUpdateMsg constructRuleChainDeleteMsg(RuleChainId ruleChainId) { + return RuleChainUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(ruleChainId.getId().getMostSignificantBits()) + .setIdLSB(ruleChainId.getId().getLeastSignificantBits()).build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/UserMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/UserMsgConstructor.java new file mode 100644 index 0000000000..f69ede9210 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/UserMsgConstructor.java @@ -0,0 +1,74 @@ +/** + * Copyright © 2016-2021 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.constructor; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.gen.edge.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.UserUpdateMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class UserMsgConstructor { + + public UserUpdateMsg constructUserUpdatedMsg(UpdateMsgType msgType, User user, CustomerId customerId) { + UserUpdateMsg.Builder builder = UserUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(user.getId().getId().getMostSignificantBits()) + .setIdLSB(user.getId().getId().getLeastSignificantBits()) + .setEmail(user.getEmail()) + .setAuthority(user.getAuthority().name()); + if (customerId != null) { + builder.setCustomerIdMSB(customerId.getId().getMostSignificantBits()); + builder.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); + } + if (user.getFirstName() != null) { + builder.setFirstName(user.getFirstName()); + } + if (user.getLastName() != null) { + builder.setLastName(user.getLastName()); + } + if (user.getAdditionalInfo() != null) { + builder.setAdditionalInfo(JacksonUtil.toString(user.getAdditionalInfo())); + } + if (user.getAdditionalInfo() != null) { + builder.setAdditionalInfo(JacksonUtil.toString(user.getAdditionalInfo())); + } + return builder.build(); + } + + public UserUpdateMsg constructUserDeleteMsg(UserId userId) { + return UserUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(userId.getId().getMostSignificantBits()) + .setIdLSB(userId.getId().getLeastSignificantBits()).build(); + } + + public UserCredentialsUpdateMsg constructUserCredentialsUpdatedMsg(UserCredentials userCredentials) { + UserCredentialsUpdateMsg.Builder builder = UserCredentialsUpdateMsg.newBuilder() + .setUserIdMSB(userCredentials.getUserId().getId().getMostSignificantBits()) + .setUserIdLSB(userCredentials.getUserId().getId().getLeastSignificantBits()) + .setEnabled(userCredentials.isEnabled()) + .setPassword(userCredentials.getPassword()); + return builder.build(); + } +} 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 new file mode 100644 index 0000000000..672beeda75 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2021 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.constructor; + +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.widget.WidgetType; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.gen.edge.WidgetTypeUpdateMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@TbCoreComponent +public class WidgetTypeMsgConstructor { + + public WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetType widgetType) { + WidgetTypeUpdateMsg.Builder builder = WidgetTypeUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(widgetType.getId().getId().getMostSignificantBits()) + .setIdLSB(widgetType.getId().getId().getLeastSignificantBits()); + if (widgetType.getBundleAlias() != null) { + builder.setBundleAlias(widgetType.getBundleAlias()); + } + if (widgetType.getAlias() != null) { + builder.setAlias(widgetType.getAlias()); + } + if (widgetType.getName() != null) { + builder.setName(widgetType.getName()); + } + if (widgetType.getDescriptor() != null) { + builder.setDescriptorJson(JacksonUtil.toString(widgetType.getDescriptor())); + } + if (widgetType.getTenantId().equals(TenantId.SYS_TENANT_ID)) { + builder.setIsSystem(true); + } + return builder.build(); + } + + public WidgetTypeUpdateMsg constructWidgetTypeDeleteMsg(WidgetTypeId widgetTypeId) { + return WidgetTypeUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(widgetTypeId.getId().getMostSignificantBits()) + .setIdLSB(widgetTypeId.getId().getLeastSignificantBits()) + .build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetsBundleMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetsBundleMsgConstructor.java new file mode 100644 index 0000000000..fba21ed1ca --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetsBundleMsgConstructor.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2021 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.constructor; + +import com.google.protobuf.ByteString; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.gen.edge.WidgetsBundleUpdateMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.nio.charset.StandardCharsets; + +@Component +@TbCoreComponent +public class WidgetsBundleMsgConstructor { + + public WidgetsBundleUpdateMsg constructWidgetsBundleUpdateMsg(UpdateMsgType msgType, WidgetsBundle widgetsBundle) { + WidgetsBundleUpdateMsg.Builder builder = WidgetsBundleUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(widgetsBundle.getId().getId().getMostSignificantBits()) + .setIdLSB(widgetsBundle.getId().getId().getLeastSignificantBits()) + .setTitle(widgetsBundle.getTitle()) + .setAlias(widgetsBundle.getAlias()); + if (widgetsBundle.getImage() != null) { + builder.setImage(ByteString.copyFrom(widgetsBundle.getImage().getBytes(StandardCharsets.UTF_8))); + } + if (widgetsBundle.getTenantId().equals(TenantId.SYS_TENANT_ID)) { + builder.setIsSystem(true); + } + return builder.build(); + } + + public WidgetsBundleUpdateMsg constructWidgetsBundleDeleteMsg(WidgetsBundleId widgetsBundleId) { + return WidgetsBundleUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(widgetsBundleId.getId().getMostSignificantBits()) + .setIdLSB(widgetsBundleId.getId().getLeastSignificantBits()) + .build(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/init/DefaultSyncEdgeService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/init/DefaultSyncEdgeService.java new file mode 100644 index 0000000000..0b9afa4ccf --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/init/DefaultSyncEdgeService.java @@ -0,0 +1,671 @@ +/** + * Copyright © 2016-2021 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.init; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.text.WordUtils; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.AdminSettingsId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.DataType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationsQuery; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationsSearchParameters; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.widget.WidgetType; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.dashboard.DashboardService; +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.entityview.EntityViewService; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.settings.AdminSettingsService; +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.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.DeviceCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.RelationRequestMsg; +import org.thingsboard.server.gen.edge.RuleChainMetadataRequestMsg; +import org.thingsboard.server.gen.edge.UserCredentialsRequestMsg; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.queue.TbClusterService; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Service +@Slf4j +public class DefaultSyncEdgeService implements SyncEdgeService { + + private static final ObjectMapper mapper = new ObjectMapper(); + + private static final int DEFAULT_LIMIT = 100; + + @Autowired + private EdgeEventService edgeEventService; + + @Autowired + private AttributesService attributesService; + + @Autowired + private RuleChainService ruleChainService; + + @Autowired + private RelationService relationService; + + @Autowired + private DeviceService deviceService; + + @Autowired + private DeviceProfileService deviceProfileService; + + @Autowired + private AssetService assetService; + + @Autowired + private EntityViewService entityViewService; + + @Autowired + private DashboardService dashboardService; + + @Autowired + private UserService userService; + + @Autowired + private WidgetsBundleService widgetsBundleService; + + @Autowired + private WidgetTypeService widgetTypeService; + + @Autowired + private AdminSettingsService adminSettingsService; + + @Autowired + private DbCallbackExecutorService dbCallbackExecutorService; + + @Autowired + private TbClusterService tbClusterService; + + @Override + public void sync(TenantId tenantId, Edge edge) { + log.trace("[{}][{}] Staring edge sync process", tenantId, edge.getId()); + try { + syncWidgetsBundles(tenantId, edge); + // TODO: voba - implement this functionality + // syncAdminSettings(edge); + syncDeviceProfiles(tenantId, edge); + syncRuleChains(tenantId, edge); + syncUsers(tenantId, edge); + syncAssets(tenantId, edge); + syncEntityViews(tenantId, edge); + syncDashboards(tenantId, edge); + syncWidgetsTypes(tenantId, edge); + syncDevices(tenantId, edge); + } catch (Exception e) { + log.error("[{}][{}] Exception during sync process", tenantId, edge.getId(), e); + } + } + + private void syncRuleChains(TenantId tenantId, Edge edge) { + log.trace("[{}] syncRuleChains [{}]", tenantId, edge.getName()); + try { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] rule chains(s) are going to be pushed to edge.", edge.getId(), pageData.getData().size()); + for (RuleChain ruleChain : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.RULE_CHAIN, EdgeEventActionType.ADDED, ruleChain.getId(), null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } catch (Exception e) { + log.error("Exception during loading edge rule chain(s) on sync!", e); + } + } + + private void syncDevices(TenantId tenantId, Edge edge) { + log.trace("[{}] syncDevices [{}]", tenantId, edge.getName()); + try { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = deviceService.findDevicesByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] device(s) are going to be pushed to edge.", edge.getId(), pageData.getData().size()); + for (Device device : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.ADDED, device.getId(), null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } catch (Exception e) { + log.error("Exception during loading edge device(s) on sync!", e); + } + } + + private void syncDeviceProfiles(TenantId tenantId, Edge edge) { + log.trace("[{}] syncDeviceProfiles [{}]", tenantId, edge.getName()); + try { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = deviceProfileService.findDeviceProfiles(tenantId, pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] user(s) are going to be pushed to edge.", edge.getId(), pageData.getData().size()); + for (DeviceProfile deviceProfile : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE_PROFILE, EdgeEventActionType.ADDED, deviceProfile.getId(), null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } catch (Exception e) { + log.error("Exception during loading device profile(s) on sync!", e); + } + } + + private void syncAssets(TenantId tenantId, Edge edge) { + log.trace("[{}] syncAssets [{}]", tenantId, edge.getName()); + try { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = assetService.findAssetsByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] asset(s) are going to be pushed to edge.", edge.getId(), pageData.getData().size()); + for (Asset asset : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ASSET, EdgeEventActionType.ADDED, asset.getId(), null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } catch (Exception e) { + log.error("Exception during loading edge asset(s) on sync!", e); + } + } + + private void syncEntityViews(TenantId tenantId, Edge edge) { + log.trace("[{}] syncEntityViews [{}]", tenantId, edge.getName()); + try { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = entityViewService.findEntityViewsByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] entity view(s) are going to be pushed to edge.", edge.getId(), pageData.getData().size()); + for (EntityView entityView : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ENTITY_VIEW, EdgeEventActionType.ADDED, entityView.getId(), null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } catch (Exception e) { + log.error("Exception during loading edge entity view(s) on sync!", e); + } + } + + private void syncDashboards(TenantId tenantId, Edge edge) { + log.trace("[{}] syncDashboards [{}]", tenantId, edge.getName()); + try { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = dashboardService.findDashboardsByTenantIdAndEdgeId(tenantId, edge.getId(), pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] dashboard(s) are going to be pushed to edge.", edge.getId(), pageData.getData().size()); + for (DashboardInfo dashboardInfo : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DASHBOARD, EdgeEventActionType.ADDED, dashboardInfo.getId(), null); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } catch (Exception e) { + log.error("Exception during loading edge dashboard(s) on sync!", e); + } + } + + private void syncUsers(TenantId tenantId, Edge edge) { + log.trace("[{}] syncUsers [{}]", tenantId, edge.getName()); + try { + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = userService.findTenantAdmins(tenantId, pageLink); + pushUsersToEdge(tenantId, pageData, edge); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + syncCustomerUsers(tenantId, edge); + } catch (Exception e) { + log.error("Exception during loading edge user(s) on sync!", e); + } + } + + private void syncCustomerUsers(TenantId tenantId, Edge edge) { + if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.CUSTOMER, EdgeEventActionType.ADDED, edge.getCustomerId(), null); + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = userService.findCustomerUsers(tenantId, edge.getCustomerId(), pageLink); + pushUsersToEdge(tenantId, pageData, edge); + if (pageData != null && pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData != null && pageData.hasNext()); + } + } + + private void pushUsersToEdge(TenantId tenantId, PageData pageData, Edge edge) { + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] user(s) are going to be pushed to edge.", edge.getId(), pageData.getData().size()); + for (User user : pageData.getData()) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.USER, EdgeEventActionType.ADDED, user.getId(), null); + } + } + } + + private void syncWidgetsBundles(TenantId tenantId, Edge edge) { + log.trace("[{}] syncWidgetsBundles [{}]", tenantId, edge.getName()); + List widgetsBundlesToPush = new ArrayList<>(); + widgetsBundlesToPush.addAll(widgetsBundleService.findAllTenantWidgetsBundlesByTenantId(tenantId)); + widgetsBundlesToPush.addAll(widgetsBundleService.findSystemWidgetsBundles(tenantId)); + try { + for (WidgetsBundle widgetsBundle : widgetsBundlesToPush) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.WIDGETS_BUNDLE, EdgeEventActionType.ADDED, widgetsBundle.getId(), null); + } + } catch (Exception e) { + log.error("Exception during loading widgets bundle(s) on sync!", e); + } + } + + private void syncWidgetsTypes(TenantId tenantId, Edge edge) { + log.trace("[{}] syncWidgetsTypes [{}]", tenantId, edge.getName()); + List widgetsBundlesToPush = new ArrayList<>(); + widgetsBundlesToPush.addAll(widgetsBundleService.findAllTenantWidgetsBundlesByTenantId(tenantId)); + widgetsBundlesToPush.addAll(widgetsBundleService.findSystemWidgetsBundles(tenantId)); + try { + for (WidgetsBundle widgetsBundle : widgetsBundlesToPush) { + List widgetTypesToPush = + widgetTypeService.findWidgetTypesByTenantIdAndBundleAlias(widgetsBundle.getTenantId(), widgetsBundle.getAlias()); + for (WidgetType widgetType : widgetTypesToPush) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.WIDGET_TYPE, EdgeEventActionType.ADDED, widgetType.getId(), null); + } + } + } catch (Exception e) { + log.error("Exception during loading widgets type(s) on sync!", e); + } + } + + private void syncAdminSettings(TenantId tenantId, Edge edge) { + log.trace("[{}] syncAdminSettings [{}]", tenantId, edge.getName()); + try { + AdminSettings systemMailSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"); + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS, EdgeEventActionType.UPDATED, null, mapper.valueToTree(systemMailSettings)); + AdminSettings tenantMailSettings = convertToTenantAdminSettings(systemMailSettings.getKey(), (ObjectNode) systemMailSettings.getJsonValue()); + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS, EdgeEventActionType.UPDATED, null, mapper.valueToTree(tenantMailSettings)); + AdminSettings systemMailTemplates = loadMailTemplates(); + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS, EdgeEventActionType.UPDATED, null, mapper.valueToTree(systemMailTemplates)); + AdminSettings tenantMailTemplates = convertToTenantAdminSettings(systemMailTemplates.getKey(), (ObjectNode) systemMailTemplates.getJsonValue()); + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS, EdgeEventActionType.UPDATED, null, mapper.valueToTree(tenantMailTemplates)); + } catch (Exception e) { + log.error("Can't load admin settings", e); + } + } + + private AdminSettings loadMailTemplates() throws Exception { + Map mailTemplates = new HashMap<>(); + Pattern startPattern = Pattern.compile("
"); + Pattern endPattern = Pattern.compile("
"); + File[] files = new DefaultResourceLoader().getResource("classpath:/templates/").getFile().listFiles(); + for (File file : files) { + Map mailTemplate = new HashMap<>(); + String name = validateName(file.getName()); + String stringTemplate = FileUtils.readFileToString(file, StandardCharsets.UTF_8); + Matcher start = startPattern.matcher(stringTemplate); + Matcher end = endPattern.matcher(stringTemplate); + if (start.find() && end.find()) { + String body = StringUtils.substringBetween(stringTemplate, start.group(), end.group()).replaceAll("\t", ""); + String subject = StringUtils.substringBetween(body, "

", "

"); + mailTemplate.put("subject", subject); + mailTemplate.put("body", body); + mailTemplates.put(name, mailTemplate); + } else { + log.error("Can't load mail template from file {}", file.getName()); + } + } + AdminSettings adminSettings = new AdminSettings(); + adminSettings.setId(new AdminSettingsId(Uuids.timeBased())); + adminSettings.setKey("mailTemplates"); + adminSettings.setJsonValue(mapper.convertValue(mailTemplates, JsonNode.class)); + return adminSettings; + } + + private String validateName(String name) throws Exception { + StringBuilder nameBuilder = new StringBuilder(); + name = name.replace(".vm", ""); + String[] nameParts = name.split("\\."); + if (nameParts.length >= 1) { + nameBuilder.append(nameParts[0]); + for (int i = 1; i < nameParts.length; i++) { + String word = WordUtils.capitalize(nameParts[i]); + nameBuilder.append(word); + } + return nameBuilder.toString(); + } else { + throw new Exception("Error during filename validation"); + } + } + + private AdminSettings convertToTenantAdminSettings(String key, ObjectNode jsonValue) { + AdminSettings tenantMailSettings = new AdminSettings(); + jsonValue.put("useSystemMailSettings", true); + tenantMailSettings.setJsonValue(jsonValue); + tenantMailSettings.setKey(key); + return tenantMailSettings; + } + + @Override + public ListenableFuture processRuleChainMetadataRequestMsg(TenantId tenantId, Edge edge, RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg) { + log.trace("[{}] processRuleChainMetadataRequestMsg [{}][{}]", tenantId, edge.getName(), ruleChainMetadataRequestMsg); + SettableFuture futureToSet = SettableFuture.create(); + if (ruleChainMetadataRequestMsg.getRuleChainIdMSB() != 0 && ruleChainMetadataRequestMsg.getRuleChainIdLSB() != 0) { + RuleChainId ruleChainId = + new RuleChainId(new UUID(ruleChainMetadataRequestMsg.getRuleChainIdMSB(), ruleChainMetadataRequestMsg.getRuleChainIdLSB())); + ListenableFuture future = saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.RULE_CHAIN_METADATA, EdgeEventActionType.ADDED, ruleChainId, null); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable EdgeEvent result) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't save edge event [{}]", ruleChainMetadataRequestMsg, t); + futureToSet.setException(t); + } + }, dbCallbackExecutorService); + } + return futureToSet; + } + + @Override + public ListenableFuture processAttributesRequestMsg(TenantId tenantId, Edge edge, AttributesRequestMsg attributesRequestMsg) { + log.trace("[{}] processAttributesRequestMsg [{}][{}]", tenantId, edge.getName(), attributesRequestMsg); + EntityId entityId = EntityIdFactory.getByTypeAndUuid( + EntityType.valueOf(attributesRequestMsg.getEntityType()), + new UUID(attributesRequestMsg.getEntityIdMSB(), attributesRequestMsg.getEntityIdLSB())); + final EdgeEventType type = EdgeUtils.getEdgeEventTypeByEntityType(entityId.getEntityType()); + if (type != null) { + SettableFuture futureToSet = SettableFuture.create(); + String scope = attributesRequestMsg.getScope(); + ListenableFuture> ssAttrFuture = attributesService.findAll(tenantId, entityId, scope); + Futures.addCallback(ssAttrFuture, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List ssAttributes) { + if (ssAttributes != null && !ssAttributes.isEmpty()) { + try { + Map entityData = new HashMap<>(); + ObjectNode attributes = mapper.createObjectNode(); + for (AttributeKvEntry attr : ssAttributes) { + if (attr.getDataType() == DataType.BOOLEAN && attr.getBooleanValue().isPresent()) { + attributes.put(attr.getKey(), attr.getBooleanValue().get()); + } else if (attr.getDataType() == DataType.DOUBLE && attr.getDoubleValue().isPresent()) { + attributes.put(attr.getKey(), attr.getDoubleValue().get()); + } else if (attr.getDataType() == DataType.LONG && attr.getLongValue().isPresent()) { + attributes.put(attr.getKey(), attr.getLongValue().get()); + } else { + attributes.put(attr.getKey(), attr.getValueAsString()); + } + } + entityData.put("kv", attributes); + entityData.put("scope", scope); + JsonNode body = mapper.valueToTree(entityData); + log.debug("Sending attributes data msg, entityId [{}], attributes [{}]", entityId, body); + saveEdgeEvent(tenantId, + edge.getId(), + type, + EdgeEventActionType.ATTRIBUTES_UPDATED, + entityId, + body); + } catch (Exception e) { + log.error("[{}] Failed to send attribute updates to the edge", edge.getName(), e); + throw new RuntimeException("[" + edge.getName() + "] Failed to send attribute updates to the edge", e); + } + } else { + log.trace("[{}][{}] No attributes found for entity {} [{}]", tenantId, + edge.getName(), + entityId.getEntityType(), + entityId.getId()); + } + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't save attributes [{}]", attributesRequestMsg, t); + futureToSet.setException(t); + } + }, dbCallbackExecutorService); + return futureToSet; + } else { + log.warn("[{}] Type doesn't supported {}", tenantId, entityId.getEntityType()); + return Futures.immediateFuture(null); + } + } + + @Override + public ListenableFuture processRelationRequestMsg(TenantId tenantId, Edge edge, RelationRequestMsg relationRequestMsg) { + log.trace("[{}] processRelationRequestMsg [{}][{}]", tenantId, edge.getName(), relationRequestMsg); + EntityId entityId = EntityIdFactory.getByTypeAndUuid( + EntityType.valueOf(relationRequestMsg.getEntityType()), + new UUID(relationRequestMsg.getEntityIdMSB(), relationRequestMsg.getEntityIdLSB())); + + List>> futures = new ArrayList<>(); + futures.add(findRelationByQuery(tenantId, edge, entityId, EntitySearchDirection.FROM)); + futures.add(findRelationByQuery(tenantId, edge, entityId, EntitySearchDirection.TO)); + ListenableFuture>> relationsListFuture = Futures.allAsList(futures); + SettableFuture futureToSet = SettableFuture.create(); + Futures.addCallback(relationsListFuture, new FutureCallback>>() { + @Override + public void onSuccess(@Nullable List> relationsList) { + try { + if (relationsList != null && !relationsList.isEmpty()) { + for (List entityRelations : relationsList) { + log.trace("[{}] [{}] [{}] relation(s) are going to be pushed to edge.", edge.getId(), entityId, entityRelations.size()); + for (EntityRelation relation : entityRelations) { + try { + if (!relation.getFrom().getEntityType().equals(EntityType.EDGE) && + !relation.getTo().getEntityType().equals(EntityType.EDGE)) { + saveEdgeEvent(tenantId, + edge.getId(), + EdgeEventType.RELATION, + EdgeEventActionType.ADDED, + null, + mapper.valueToTree(relation)); + } + } catch (Exception e) { + log.error("Exception during loading relation [{}] to edge on sync!", relation, e); + futureToSet.setException(e); + return; + } + } + } + } + futureToSet.set(null); + } catch (Exception e) { + log.error("Exception during loading relation(s) to edge on sync!", e); + futureToSet.setException(e); + } + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}] Can't find relation by query. Entity id [{}]", tenantId, entityId, t); + futureToSet.setException(t); + } + }, dbCallbackExecutorService); + return futureToSet; + } + + private ListenableFuture> findRelationByQuery(TenantId tenantId, Edge edge, EntityId entityId, EntitySearchDirection direction) { + EntityRelationsQuery query = new EntityRelationsQuery(); + query.setParameters(new RelationsSearchParameters(entityId, direction, -1, false)); + return relationService.findByQuery(tenantId, query); + } + + @Override + public ListenableFuture processDeviceCredentialsRequestMsg(TenantId tenantId, Edge edge, DeviceCredentialsRequestMsg deviceCredentialsRequestMsg) { + log.trace("[{}] processDeviceCredentialsRequestMsg [{}][{}]", tenantId, edge.getName(), deviceCredentialsRequestMsg); + SettableFuture futureToSet = SettableFuture.create(); + if (deviceCredentialsRequestMsg.getDeviceIdMSB() != 0 && deviceCredentialsRequestMsg.getDeviceIdLSB() != 0) { + DeviceId deviceId = new DeviceId(new UUID(deviceCredentialsRequestMsg.getDeviceIdMSB(), deviceCredentialsRequestMsg.getDeviceIdLSB())); + ListenableFuture future = saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.CREDENTIALS_UPDATED, deviceId, null); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable EdgeEvent result) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't save edge event [{}]", deviceCredentialsRequestMsg, t); + futureToSet.setException(t); + } + }, dbCallbackExecutorService); + } + return futureToSet; + } + + @Override + public ListenableFuture processUserCredentialsRequestMsg(TenantId tenantId, Edge edge, UserCredentialsRequestMsg userCredentialsRequestMsg) { + log.trace("[{}] processUserCredentialsRequestMsg [{}][{}]", tenantId, edge.getName(), userCredentialsRequestMsg); + SettableFuture futureToSet = SettableFuture.create(); + if (userCredentialsRequestMsg.getUserIdMSB() != 0 && userCredentialsRequestMsg.getUserIdLSB() != 0) { + UserId userId = new UserId(new UUID(userCredentialsRequestMsg.getUserIdMSB(), userCredentialsRequestMsg.getUserIdLSB())); + ListenableFuture future = saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.USER, EdgeEventActionType.CREDENTIALS_UPDATED, userId, null); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable EdgeEvent result) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't save edge event [{}]", userCredentialsRequestMsg, t); + futureToSet.setException(t); + } + }, dbCallbackExecutorService); + } + return futureToSet; + } + + private ListenableFuture saveEdgeEvent(TenantId tenantId, + EdgeId edgeId, + EdgeEventType type, + EdgeEventActionType action, + EntityId entityId, + JsonNode body) { + log.trace("Pushing edge event to edge queue. tenantId [{}], edgeId [{}], type [{}], action[{}], entityId [{}], body [{}]", + tenantId, edgeId, type, action, entityId, body); + + EdgeEvent edgeEvent = new EdgeEvent(); + edgeEvent.setTenantId(tenantId); + edgeEvent.setEdgeId(edgeId); + edgeEvent.setType(type); + edgeEvent.setAction(action); + if (entityId != null) { + edgeEvent.setEntityId(entityId.getId()); + } + edgeEvent.setBody(body); + ListenableFuture future = edgeEventService.saveAsync(edgeEvent); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable EdgeEvent result) { + tbClusterService.onEdgeEventUpdate(tenantId, edgeId); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Can't save edge event [{}] for edge [{}]", tenantId.getId(), edgeEvent, edgeId.getId(), t); + } + }, dbCallbackExecutorService); + return future; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/init/SyncEdgeService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/init/SyncEdgeService.java new file mode 100644 index 0000000000..e95809d019 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/init/SyncEdgeService.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2021 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.init; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.DeviceCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.RelationRequestMsg; +import org.thingsboard.server.gen.edge.RuleChainMetadataRequestMsg; +import org.thingsboard.server.gen.edge.UserCredentialsRequestMsg; + +public interface SyncEdgeService { + + void sync(TenantId tenantId, Edge edge); + + ListenableFuture processRuleChainMetadataRequestMsg(TenantId tenantId, Edge edge, RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg); + + ListenableFuture processAttributesRequestMsg(TenantId tenantId, Edge edge, AttributesRequestMsg attributesRequestMsg); + + ListenableFuture processRelationRequestMsg(TenantId tenantId, Edge edge, RelationRequestMsg relationRequestMsg); + + ListenableFuture processDeviceCredentialsRequestMsg(TenantId tenantId, Edge edge, DeviceCredentialsRequestMsg deviceCredentialsRequestMsg); + + ListenableFuture processUserCredentialsRequestMsg(TenantId tenantId, Edge edge, UserCredentialsRequestMsg userCredentialsRequestMsg); +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AlarmProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AlarmProcessor.java new file mode 100644 index 0000000000..5e7e1e5844 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AlarmProcessor.java @@ -0,0 +1,99 @@ +/** + * Copyright © 2016-2021 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 com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.AlarmUpdateMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@Slf4j +@TbCoreComponent +public class AlarmProcessor extends BaseProcessor { + + public ListenableFuture onAlarmUpdate(TenantId tenantId, AlarmUpdateMsg alarmUpdateMsg) { + log.trace("[{}] onAlarmUpdate [{}]", tenantId, alarmUpdateMsg); + EntityId originatorId = getAlarmOriginator(tenantId, alarmUpdateMsg.getOriginatorName(), + EntityType.valueOf(alarmUpdateMsg.getOriginatorType())); + if (originatorId == null) { + return Futures.immediateFuture(null); + } + try { + Alarm existentAlarm = alarmService.findLatestByOriginatorAndType(tenantId, originatorId, alarmUpdateMsg.getType()).get(); + switch (alarmUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + if (existentAlarm == null || existentAlarm.getStatus().isCleared()) { + existentAlarm = new Alarm(); + existentAlarm.setTenantId(tenantId); + existentAlarm.setType(alarmUpdateMsg.getName()); + existentAlarm.setOriginator(originatorId); + existentAlarm.setSeverity(AlarmSeverity.valueOf(alarmUpdateMsg.getSeverity())); + existentAlarm.setStartTs(alarmUpdateMsg.getStartTs()); + existentAlarm.setClearTs(alarmUpdateMsg.getClearTs()); + existentAlarm.setPropagate(alarmUpdateMsg.getPropagate()); + } + existentAlarm.setStatus(AlarmStatus.valueOf(alarmUpdateMsg.getStatus())); + existentAlarm.setAckTs(alarmUpdateMsg.getAckTs()); + existentAlarm.setEndTs(alarmUpdateMsg.getEndTs()); + existentAlarm.setDetails(mapper.readTree(alarmUpdateMsg.getDetails())); + alarmService.createOrUpdateAlarm(existentAlarm); + break; + case ALARM_ACK_RPC_MESSAGE: + if (existentAlarm != null) { + alarmService.ackAlarm(tenantId, existentAlarm.getId(), alarmUpdateMsg.getAckTs()); + } + break; + case ALARM_CLEAR_RPC_MESSAGE: + if (existentAlarm != null) { + alarmService.clearAlarm(tenantId, existentAlarm.getId(), mapper.readTree(alarmUpdateMsg.getDetails()), alarmUpdateMsg.getAckTs()); + } + break; + case ENTITY_DELETED_RPC_MESSAGE: + if (existentAlarm != null) { + alarmService.deleteAlarm(tenantId, existentAlarm.getId()); + } + break; + } + return Futures.immediateFuture(null); + } catch (Exception e) { + log.error("Failed to process alarm update msg [{}]", alarmUpdateMsg, e); + return Futures.immediateFailedFuture(new RuntimeException("Failed to process alarm update msg", e)); + } + } + + private EntityId getAlarmOriginator(TenantId tenantId, String entityName, EntityType entityType) { + switch (entityType) { + case DEVICE: + return deviceService.findDeviceByTenantIdAndName(tenantId, entityName).getId(); + case ASSET: + return assetService.findAssetByTenantIdAndName(tenantId, entityName).getId(); + case ENTITY_VIEW: + return entityViewService.findEntityViewByTenantIdAndName(tenantId, entityName).getId(); + default: + return null; + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseProcessor.java new file mode 100644 index 0000000000..cf7f9e21fb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseProcessor.java @@ -0,0 +1,136 @@ +/** + * Copyright © 2016-2021 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 com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.alarm.AlarmService; +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.DeviceService; +import org.thingsboard.server.dao.edge.EdgeEventService; +import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.profile.DefaultTbDeviceProfileCache; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; +import org.thingsboard.server.service.queue.TbClusterService; +import org.thingsboard.server.service.state.DeviceStateService; + +@Slf4j +public abstract class BaseProcessor { + + protected static final ObjectMapper mapper = new ObjectMapper(); + + @Autowired + protected AlarmService alarmService; + + @Autowired + protected DeviceService deviceService; + + @Autowired + protected TbDeviceProfileCache deviceProfileCache; + + @Autowired + protected DashboardService dashboardService; + + @Autowired + protected AssetService assetService; + + @Autowired + protected EntityViewService entityViewService; + + @Autowired + protected EdgeService edgeService; + + @Autowired + protected CustomerService customerService; + + @Autowired + protected UserService userService; + + @Autowired + protected RelationService relationService; + + @Autowired + protected DeviceCredentialsService deviceCredentialsService; + + @Autowired + protected AttributesService attributesService; + + @Autowired + protected TbClusterService tbClusterService; + + @Autowired + protected DeviceStateService deviceStateService; + + @Autowired + protected EdgeEventService edgeEventService; + + @Autowired + protected DbCallbackExecutorService dbCallbackExecutorService; + + protected ListenableFuture saveEdgeEvent(TenantId tenantId, + EdgeId edgeId, + EdgeEventType type, + EdgeEventActionType action, + EntityId entityId, + JsonNode body) { + log.debug("Pushing event to edge queue. tenantId [{}], edgeId [{}], type[{}], " + + "action [{}], entityId [{}], body [{}]", + tenantId, edgeId, type, action, entityId, body); + + EdgeEvent edgeEvent = new EdgeEvent(); + edgeEvent.setTenantId(tenantId); + edgeEvent.setEdgeId(edgeId); + edgeEvent.setType(type); + edgeEvent.setAction(action); + if (entityId != null) { + edgeEvent.setEntityId(entityId.getId()); + } + edgeEvent.setBody(body); + ListenableFuture future = edgeEventService.saveAsync(edgeEvent); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable EdgeEvent result) { + tbClusterService.onEdgeEventUpdate(tenantId, edgeId); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Can't save edge event [{}] for edge [{}]", tenantId.getId(), edgeEvent, edgeId.getId(), t); + } + }, dbCallbackExecutorService); + return future; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceProcessor.java new file mode 100644 index 0000000000..35eea3f962 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceProcessor.java @@ -0,0 +1,299 @@ +/** + * Copyright © 2016-2021 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 com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.stereotype.Component; +import org.thingsboard.rule.engine.api.RpcError; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.edge.Edge; +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.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.gen.edge.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceRpcCallMsg; +import org.thingsboard.server.gen.edge.DeviceUpdateMsg; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.rpc.FromDeviceRpcResponse; +import org.thingsboard.server.service.rpc.FromDeviceRpcResponseActorMsg; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.locks.ReentrantLock; + +@Component +@Slf4j +@TbCoreComponent +public class DeviceProcessor extends BaseProcessor { + + private static final ReentrantLock deviceCreationLock = new ReentrantLock(); + + public ListenableFuture onDeviceUpdate(TenantId tenantId, Edge edge, DeviceUpdateMsg deviceUpdateMsg) { + log.trace("[{}] onDeviceUpdate [{}] from edge [{}]", tenantId, deviceUpdateMsg, edge.getName()); + switch (deviceUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + String deviceName = deviceUpdateMsg.getName(); + Device device = deviceService.findDeviceByTenantIdAndName(tenantId, deviceName); + if (device != null) { + ListenableFuture> future = edgeService.findRelatedEdgeIdsByEntityId(tenantId, device.getId()); + SettableFuture futureToSet = SettableFuture.create(); + Futures.addCallback(future, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List edgeIds) { + boolean update = false; + if (edgeIds != null && !edgeIds.isEmpty()) { + if (edgeIds.contains(edge.getId())) { + update = true; + } + } + Device device; + if (update) { + log.info("[{}] Device with name '{}' already exists on the cloud, and related to this edge [{}]. " + + "deviceUpdateMsg [{}], Updating device", tenantId, deviceName, edge.getId(), deviceUpdateMsg); + updateDevice(tenantId, edge, deviceUpdateMsg); + } else { + log.info("[{}] Device with name '{}' already exists on the cloud, but not related to this edge [{}]. deviceUpdateMsg [{}]." + + "Creating a new device with random prefix and relate to this edge", tenantId, deviceName, edge.getId(), deviceUpdateMsg); + String newDeviceName = deviceUpdateMsg.getName() + "_" + RandomStringUtils.randomAlphabetic(15); + device = createDevice(tenantId, edge, deviceUpdateMsg, newDeviceName); + ObjectNode body = mapper.createObjectNode(); + body.put("conflictName", deviceName); + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.ENTITY_MERGE_REQUEST, device.getId(), body); + } + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}] Failed to get related edge ids by device id [{}], edge [{}]", tenantId, deviceUpdateMsg, edge.getId(), t); + futureToSet.setException(t); + } + }, dbCallbackExecutorService); + return futureToSet; + } else { + log.info("[{}] Creating new device and replacing device entity on the edge [{}]", tenantId, deviceUpdateMsg); + device = createDevice(tenantId, edge, deviceUpdateMsg, deviceUpdateMsg.getName()); + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.ENTITY_MERGE_REQUEST, device.getId(), null); + } + break; + case ENTITY_UPDATED_RPC_MESSAGE: + updateDevice(tenantId, edge, deviceUpdateMsg); + break; + case ENTITY_DELETED_RPC_MESSAGE: + DeviceId deviceId = new DeviceId(new UUID(deviceUpdateMsg.getIdMSB(), deviceUpdateMsg.getIdLSB())); + Device deviceToDelete = deviceService.findDeviceById(tenantId, deviceId); + if (deviceToDelete != null) { + deviceService.unassignDeviceFromEdge(tenantId, deviceId, edge.getId()); + } + break; + case UNRECOGNIZED: + log.error("Unsupported msg type {}", deviceUpdateMsg.getMsgType()); + return Futures.immediateFailedFuture(new RuntimeException("Unsupported msg type " + deviceUpdateMsg.getMsgType())); + } + return Futures.immediateFuture(null); + } + + public ListenableFuture onDeviceCredentialsUpdate(TenantId tenantId, DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg) { + log.debug("Executing onDeviceCredentialsUpdate, deviceCredentialsUpdateMsg [{}]", deviceCredentialsUpdateMsg); + DeviceId deviceId = new DeviceId(new UUID(deviceCredentialsUpdateMsg.getDeviceIdMSB(), deviceCredentialsUpdateMsg.getDeviceIdLSB())); + ListenableFuture deviceFuture = deviceService.findDeviceByIdAsync(tenantId, deviceId); + return Futures.transform(deviceFuture, device -> { + if (device != null) { + log.debug("Updating device credentials for device [{}]. New device credentials Id [{}], value [{}]", + device.getName(), deviceCredentialsUpdateMsg.getCredentialsId(), deviceCredentialsUpdateMsg.getCredentialsValue()); + try { + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); + deviceCredentials.setCredentialsType(DeviceCredentialsType.valueOf(deviceCredentialsUpdateMsg.getCredentialsType())); + deviceCredentials.setCredentialsId(deviceCredentialsUpdateMsg.getCredentialsId()); + deviceCredentials.setCredentialsValue(deviceCredentialsUpdateMsg.getCredentialsValue()); + deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + } catch (Exception e) { + log.error("Can't update device credentials for device [{}], deviceCredentialsUpdateMsg [{}]", device.getName(), deviceCredentialsUpdateMsg, e); + throw new RuntimeException(e); + } + } + return null; + }, dbCallbackExecutorService); + } + + + private void updateDevice(TenantId tenantId, Edge edge, DeviceUpdateMsg deviceUpdateMsg) { + DeviceId deviceId = new DeviceId(new UUID(deviceUpdateMsg.getIdMSB(), deviceUpdateMsg.getIdLSB())); + Device device = deviceService.findDeviceById(tenantId, deviceId); + if (device != null) { + device.setName(deviceUpdateMsg.getName()); + device.setType(deviceUpdateMsg.getType()); + device.setLabel(deviceUpdateMsg.getLabel()); + device.setAdditionalInfo(JacksonUtil.toJsonNode(deviceUpdateMsg.getAdditionalInfo())); + if (deviceUpdateMsg.getDeviceProfileIdMSB() != 0 && deviceUpdateMsg.getDeviceProfileIdLSB() != 0) { + DeviceProfileId deviceProfileId = new DeviceProfileId( + new UUID(deviceUpdateMsg.getDeviceProfileIdMSB(), deviceUpdateMsg.getDeviceProfileIdLSB())); + device.setDeviceProfileId(deviceProfileId); + } + deviceService.saveDevice(device); + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.CREDENTIALS_REQUEST, deviceId, null); + } else { + log.warn("[{}] can't find device [{}], edge [{}]", tenantId, deviceUpdateMsg, edge.getId()); + } + } + + private Device createDevice(TenantId tenantId, Edge edge, DeviceUpdateMsg deviceUpdateMsg, String deviceName) { + Device device; + try { + deviceCreationLock.lock(); + log.debug("[{}] Creating device entity [{}] from edge [{}]", tenantId, deviceUpdateMsg, edge.getName()); + device = new Device(); + device.setTenantId(edge.getTenantId()); + // make device private, if edge is public + device.setCustomerId(getCustomerId(edge)); + device.setName(deviceName); + device.setType(deviceUpdateMsg.getType()); + device.setLabel(deviceUpdateMsg.getLabel()); + device.setAdditionalInfo(JacksonUtil.toJsonNode(deviceUpdateMsg.getAdditionalInfo())); + if (deviceUpdateMsg.getDeviceProfileIdMSB() != 0 && deviceUpdateMsg.getDeviceProfileIdLSB() != 0) { + DeviceProfileId deviceProfileId = new DeviceProfileId( + new UUID(deviceUpdateMsg.getDeviceProfileIdMSB(), deviceUpdateMsg.getDeviceProfileIdLSB())); + device.setDeviceProfileId(deviceProfileId); + } + device = deviceService.saveDevice(device); + createRelationFromEdge(tenantId, edge.getId(), device.getId()); + deviceStateService.onDeviceAdded(device); + pushDeviceCreatedEventToRuleEngine(tenantId, edge, device); + deviceService.assignDeviceToEdge(edge.getTenantId(), device.getId(), edge.getId()); + } finally { + deviceCreationLock.unlock(); + } + return device; + } + + private CustomerId getCustomerId(Edge edge) { + if (edge.getCustomerId() == null || edge.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + return edge.getCustomerId(); + } + Customer publicCustomer = customerService.findOrCreatePublicCustomer(edge.getTenantId()); + if (publicCustomer.getId().equals(edge.getCustomerId())) { + return null; + } else { + return edge.getCustomerId(); + } + } + + private void createRelationFromEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) { + EntityRelation relation = new EntityRelation(); + relation.setFrom(edgeId); + relation.setTo(entityId); + relation.setTypeGroup(RelationTypeGroup.COMMON); + relation.setType(EntityRelation.EDGE_TYPE); + relationService.saveRelation(tenantId, relation); + } + + private void pushDeviceCreatedEventToRuleEngine(TenantId tenantId, Edge edge, Device device) { + try { + DeviceId deviceId = device.getId(); + ObjectNode entityNode = mapper.valueToTree(device); + TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, + getActionTbMsgMetaData(edge, device.getCustomerId()), TbMsgDataType.JSON, mapper.writeValueAsString(entityNode)); + tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + log.debug("Successfully send ENTITY_CREATED EVENT to rule engine [{}]", device); + } + + @Override + public void onFailure(Throwable t) { + log.debug("Failed to send ENTITY_CREATED EVENT to rule engine [{}]", device, t); + } + }); + } catch (JsonProcessingException | IllegalArgumentException e) { + log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), DataConstants.ENTITY_CREATED, e); + } + } + + private TbMsgMetaData getActionTbMsgMetaData(Edge edge, CustomerId customerId) { + TbMsgMetaData metaData = getTbMsgMetaData(edge); + if (customerId != null && !customerId.isNullUid()) { + metaData.putValue("customerId", customerId.toString()); + } + return metaData; + } + + private TbMsgMetaData getTbMsgMetaData(Edge edge) { + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue("edgeId", edge.getId().toString()); + metaData.putValue("edgeName", edge.getName()); + return metaData; + } + + public ListenableFuture processDeviceRpcCallResponseMsg(TenantId tenantId, DeviceRpcCallMsg deviceRpcCallMsg) { + log.trace("[{}] processDeviceRpcCallResponseMsg [{}]", tenantId, deviceRpcCallMsg); + SettableFuture futureToSet = SettableFuture.create(); + UUID requestUuid = new UUID(deviceRpcCallMsg.getRequestUuidMSB(), deviceRpcCallMsg.getRequestUuidLSB()); + DeviceId deviceId = new DeviceId(new UUID(deviceRpcCallMsg.getDeviceIdMSB(), deviceRpcCallMsg.getDeviceIdLSB())); + + FromDeviceRpcResponse response; + if (!StringUtils.isEmpty(deviceRpcCallMsg.getResponseMsg().getError())) { + response = new FromDeviceRpcResponse(requestUuid, null, RpcError.valueOf(deviceRpcCallMsg.getResponseMsg().getError())); + } else { + response = new FromDeviceRpcResponse(requestUuid, deviceRpcCallMsg.getResponseMsg().getResponse(), null); + } + TbQueueCallback callback = new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process push notification to core [{}]", deviceRpcCallMsg, t); + futureToSet.setException(t); + } + }; + FromDeviceRpcResponseActorMsg msg = + new FromDeviceRpcResponseActorMsg(deviceRpcCallMsg.getRequestId(), + tenantId, + deviceId, response); + tbClusterService.pushMsgToCore(msg, callback); + return futureToSet; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RelationProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RelationProcessor.java new file mode 100644 index 0000000000..b21a5bd195 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RelationProcessor.java @@ -0,0 +1,104 @@ +/** + * Copyright © 2016-2021 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 com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.gen.edge.RelationUpdateMsg; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.UUID; + +@Component +@Slf4j +@TbCoreComponent +public class RelationProcessor extends BaseProcessor { + + public ListenableFuture onRelationUpdate(TenantId tenantId, RelationUpdateMsg relationUpdateMsg) { + log.trace("[{}] onRelationUpdate [{}]", tenantId, relationUpdateMsg); + try { + EntityRelation entityRelation = new EntityRelation(); + + UUID fromUUID = new UUID(relationUpdateMsg.getFromIdMSB(), relationUpdateMsg.getFromIdLSB()); + EntityId fromId = EntityIdFactory.getByTypeAndUuid(EntityType.valueOf(relationUpdateMsg.getFromEntityType()), fromUUID); + entityRelation.setFrom(fromId); + + UUID toUUID = new UUID(relationUpdateMsg.getToIdMSB(), relationUpdateMsg.getToIdLSB()); + EntityId toId = EntityIdFactory.getByTypeAndUuid(EntityType.valueOf(relationUpdateMsg.getToEntityType()), toUUID); + entityRelation.setTo(toId); + + entityRelation.setType(relationUpdateMsg.getType()); + entityRelation.setTypeGroup(RelationTypeGroup.valueOf(relationUpdateMsg.getTypeGroup())); + entityRelation.setAdditionalInfo(mapper.readTree(relationUpdateMsg.getAdditionalInfo())); + switch (relationUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + if (isEntityExists(tenantId, entityRelation.getTo()) + && isEntityExists(tenantId, entityRelation.getFrom())) { + relationService.saveRelationAsync(tenantId, entityRelation); + } + break; + case ENTITY_DELETED_RPC_MESSAGE: + relationService.deleteRelation(tenantId, entityRelation); + break; + case UNRECOGNIZED: + log.error("Unsupported msg type"); + } + return Futures.immediateFuture(null); + } catch (Exception e) { + log.error("Failed to process relation update msg [{}]", relationUpdateMsg, e); + return Futures.immediateFailedFuture(new RuntimeException("Failed to process relation update msg", e)); + } + } + + + private boolean isEntityExists(TenantId tenantId, EntityId entityId) throws ThingsboardException { + switch (entityId.getEntityType()) { + case DEVICE: + return deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId())) != null; + case ASSET: + return assetService.findAssetById(tenantId, new AssetId(entityId.getId())) != null; + case ENTITY_VIEW: + return entityViewService.findEntityViewById(tenantId, new EntityViewId(entityId.getId())) != null; + case CUSTOMER: + return customerService.findCustomerById(tenantId, new CustomerId(entityId.getId())) != null; + case USER: + return userService.findUserById(tenantId, new UserId(entityId.getId())) != null; + case DASHBOARD: + return dashboardService.findDashboardById(tenantId, new DashboardId(entityId.getId())) != null; + default: + throw new ThingsboardException("Unsupported entity type " + entityId.getEntityType(), ThingsboardErrorCode.INVALID_ARGUMENTS); + } + } + + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryProcessor.java new file mode 100644 index 0000000000..c858955bfc --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryProcessor.java @@ -0,0 +1,282 @@ +/** + * Copyright © 2016-2021 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 com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.stereotype.Component; +import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.kv.AttributeKey; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.ServiceQueue; +import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.transport.util.JsonUtils; +import org.thingsboard.server.gen.edge.AttributeDeleteMsg; +import org.thingsboard.server.gen.edge.EntityDataProto; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +@Component +@Slf4j +@TbCoreComponent +public class TelemetryProcessor extends BaseProcessor { + + private final Gson gson = new Gson(); + + public List> onTelemetryUpdate(TenantId tenantId, EntityDataProto entityData) { + log.trace("[{}] onTelemetryUpdate [{}]", tenantId, entityData); + List> result = new ArrayList<>(); + EntityId entityId = constructEntityId(entityData); + if ((entityData.hasPostAttributesMsg() || entityData.hasPostTelemetryMsg() || entityData.hasAttributesUpdatedMsg()) && entityId != null) { + // TODO: voba - in terms of performance we should not fetch device from DB by id + // TbMsgMetaData metaData = constructBaseMsgMetadata(tenantId, entityId); + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue(DataConstants.MSG_SOURCE_KEY, DataConstants.EDGE_MSG_SOURCE); + if (entityData.hasPostAttributesMsg()) { + result.add(processPostAttributes(tenantId, entityId, entityData.getPostAttributesMsg(), metaData)); + } + if (entityData.hasAttributesUpdatedMsg()) { + metaData.putValue("scope", entityData.getPostAttributeScope()); + result.add(processAttributesUpdate(tenantId, entityId, entityData.getAttributesUpdatedMsg(), metaData)); + } + if (entityData.hasPostTelemetryMsg()) { + result.add(processPostTelemetry(tenantId, entityId, entityData.getPostTelemetryMsg(), metaData)); + } + } + if (entityData.hasAttributeDeleteMsg()) { + result.add(processAttributeDeleteMsg(tenantId, entityId, entityData.getAttributeDeleteMsg(), entityData.getEntityType())); + } + return result; + } + + private TbMsgMetaData constructBaseMsgMetadata(TenantId tenantId, EntityId entityId) { + TbMsgMetaData metaData = new TbMsgMetaData(); + switch (entityId.getEntityType()) { + case DEVICE: + Device device = deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId())); + if (device != null) { + metaData.putValue("deviceName", device.getName()); + metaData.putValue("deviceType", device.getType()); + } + break; + case ASSET: + Asset asset = assetService.findAssetById(tenantId, new AssetId(entityId.getId())); + if (asset != null) { + metaData.putValue("assetName", asset.getName()); + metaData.putValue("assetType", asset.getType()); + } + break; + case ENTITY_VIEW: + EntityView entityView = entityViewService.findEntityViewById(tenantId, new EntityViewId(entityId.getId())); + if (entityView != null) { + metaData.putValue("entityViewName", entityView.getName()); + metaData.putValue("entityViewType", entityView.getType()); + } + break; + default: + log.debug("Using empty metadata for entityId [{}]", entityId); + break; + } + return metaData; + } + + private Pair getDefaultQueueNameAndRuleChainId(TenantId tenantId, EntityId entityId) { + if (EntityType.DEVICE.equals(entityId.getEntityType())) { + DeviceProfile deviceProfile = deviceProfileCache.get(tenantId, new DeviceId(entityId.getId())); + RuleChainId ruleChainId; + String queueName; + + if (deviceProfile == null) { + log.warn("[{}] Device profile is null!", entityId); + ruleChainId = null; + queueName = ServiceQueue.MAIN; + } else { + ruleChainId = deviceProfile.getDefaultRuleChainId(); + String defaultQueueName = deviceProfile.getDefaultQueueName(); + queueName = defaultQueueName != null ? defaultQueueName : ServiceQueue.MAIN; + } + return new ImmutablePair<>(queueName, ruleChainId); + } else { + return new ImmutablePair<>(ServiceQueue.MAIN, null); + } + } + + private ListenableFuture processPostTelemetry(TenantId tenantId, EntityId entityId, TransportProtos.PostTelemetryMsg msg, TbMsgMetaData metaData) { + SettableFuture futureToSet = SettableFuture.create(); + for (TransportProtos.TsKvListProto tsKv : msg.getTsKvListList()) { + JsonObject json = JsonUtils.getJsonObject(tsKv.getKvList()); + metaData.putValue("ts", tsKv.getTs() + ""); + Pair defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); + String queueName = defaultQueueAndRuleChain.getKey(); + RuleChainId ruleChainId = defaultQueueAndRuleChain.getValue(); + TbMsg tbMsg = TbMsg.newMsg(queueName, SessionMsgType.POST_TELEMETRY_REQUEST.name(), entityId, metaData, gson.toJson(json), ruleChainId, null); + tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process post telemetry [{}]", msg, t); + futureToSet.setException(t); + } + }); + } + return futureToSet; + } + + private ListenableFuture processPostAttributes(TenantId tenantId, EntityId entityId, TransportProtos.PostAttributeMsg msg, TbMsgMetaData metaData) { + SettableFuture futureToSet = SettableFuture.create(); + JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); + Pair defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); + String queueName = defaultQueueAndRuleChain.getKey(); + RuleChainId ruleChainId = defaultQueueAndRuleChain.getValue(); + TbMsg tbMsg = TbMsg.newMsg(queueName, SessionMsgType.POST_ATTRIBUTES_REQUEST.name(), entityId, metaData, gson.toJson(json), ruleChainId, null); + tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process post attributes [{}]", msg, t); + futureToSet.setException(t); + } + }); + return futureToSet; + } + + private ListenableFuture processAttributesUpdate(TenantId tenantId, EntityId entityId, TransportProtos.PostAttributeMsg msg, TbMsgMetaData metaData) { + SettableFuture futureToSet = SettableFuture.create(); + JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); + Set attributes = JsonConverter.convertToAttributes(json); + ListenableFuture> future = attributesService.save(tenantId, entityId, metaData.getValue("scope"), new ArrayList<>(attributes)); + Futures.addCallback(future, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List voids) { + Pair defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); + String queueName = defaultQueueAndRuleChain.getKey(); + RuleChainId ruleChainId = defaultQueueAndRuleChain.getValue(); + TbMsg tbMsg = TbMsg.newMsg(queueName, DataConstants.ATTRIBUTES_UPDATED, entityId, metaData, gson.toJson(json), ruleChainId, null); + tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process attributes update [{}]", msg, t); + futureToSet.setException(t); + } + }); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process attributes update [{}]", msg, t); + futureToSet.setException(t); + } + }, dbCallbackExecutorService); + return futureToSet; + } + + private ListenableFuture processAttributeDeleteMsg(TenantId tenantId, EntityId entityId, AttributeDeleteMsg attributeDeleteMsg, String entityType) { + SettableFuture futureToSet = SettableFuture.create(); + String scope = attributeDeleteMsg.getScope(); + List attributeNames = attributeDeleteMsg.getAttributeNamesList(); + attributesService.removeAll(tenantId, entityId, scope, attributeNames); + if (EntityType.DEVICE.name().equals(entityType)) { + Set attributeKeys = new HashSet<>(); + for (String attributeName : attributeNames) { + attributeKeys.add(new AttributeKey(scope, attributeName)); + } + tbClusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete( + tenantId, (DeviceId) entityId, attributeKeys), new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process attribute delete msg [{}]", attributeDeleteMsg, t); + futureToSet.setException(t); + } + }); + } + return futureToSet; + } + + private EntityId constructEntityId(EntityDataProto entityData) { + EntityType entityType = EntityType.valueOf(entityData.getEntityType()); + switch (entityType) { + case DEVICE: + return new DeviceId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); + case ASSET: + return new AssetId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); + case ENTITY_VIEW: + return new EntityViewId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); + case DASHBOARD: + return new DashboardId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); + case TENANT: + return new TenantId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); + case CUSTOMER: + return new CustomerId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); + case USER: + return new UserId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); + default: + log.warn("Unsupported entity type [{}] during construct of entity id. EntityDataProto [{}]", entityData.getEntityType(), entityData); + return null; + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java index 1c794c95b3..f9ea9afa67 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java @@ -57,6 +57,7 @@ public class DatabaseHelper { public static final String DASHBOARD = "dashboard"; public static final String ENTITY_VIEWS = "entity_views"; public static final String ENTITY_VIEW = "entity_view"; + public static final String RULE_CHAIN = "rule_chain"; public static final String ID = "id"; public static final String TITLE = "title"; public static final String TYPE = "type"; diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index b49819b7ac..51f645a2af 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -61,6 +61,7 @@ import org.thingsboard.server.common.data.query.DynamicValueSourceType; import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.NumericFilterPredicate; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.UserCredentials; @@ -271,8 +272,8 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { thermostatDeviceProfile.setTransportType(DeviceTransportType.DEFAULT); thermostatDeviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED); thermostatDeviceProfile.setDescription("Thermostat device profile"); - thermostatDeviceProfile.setDefaultRuleChainId(ruleChainService.findTenantRuleChains( - demoTenant.getId(), new PageLink(1, 0, "Thermostat")).getData().get(0).getId()); + thermostatDeviceProfile.setDefaultRuleChainId(ruleChainService.findTenantRuleChainsByType( + demoTenant.getId(), RuleChainType.CORE, new PageLink(1, 0, "Thermostat")).getData().get(0).getId()); DeviceProfileData deviceProfileData = new DeviceProfileData(); DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); @@ -439,6 +440,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { this.deleteSystemWidgetBundle("date"); this.deleteSystemWidgetBundle("entity_admin_widgets"); this.deleteSystemWidgetBundle("navigation_widgets"); + this.deleteSystemWidgetBundle("edge_widgets"); installScripts.loadSystemWidgets(); } 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 f36a8f84ce..909c2a3b03 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 @@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; -import org.thingsboard.server.dao.resource.TbResourceService; +import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; @@ -69,6 +69,8 @@ public class InstallScripts { public static final String MODELS_DIR = "models"; public static final String CREDENTIALS_DIR = "credentials"; + public static final String EDGE_MANAGEMENT = "edge_management"; + public static final String JSON_EXT = ".json"; public static final String XML_EXT = ".xml"; @@ -91,16 +93,20 @@ public class InstallScripts { private OAuth2ConfigTemplateService oAuth2TemplateService; @Autowired - private TbResourceService resourceService; + private ResourceService resourceService; - public Path getTenantRuleChainsDir() { + private Path getTenantRuleChainsDir() { return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR); } - public Path getDeviceProfileDefaultRuleChainTemplateFilePath() { + private Path getDeviceProfileDefaultRuleChainTemplateFilePath() { return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DEVICE_PROFILE_DIR, "rule_chain_template.json"); } + private Path getEdgeRuleChainsDir() { + return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, EDGE_MANAGEMENT, RULE_CHAINS_DIR); + } + public String getDataDir() { if (!StringUtils.isEmpty(dataDir)) { if (!Paths.get(this.dataDir).toFile().isDirectory()) { @@ -124,7 +130,16 @@ public class InstallScripts { public void createDefaultRuleChains(TenantId tenantId) throws IOException { Path tenantChainsDir = getTenantRuleChainsDir(); - try (DirectoryStream dirStream = Files.newDirectoryStream(tenantChainsDir, path -> path.toString().endsWith(InstallScripts.JSON_EXT))) { + loadRuleChainsFromPath(tenantId, tenantChainsDir); + } + + public void createDefaultEdgeRuleChains(TenantId tenantId) throws IOException { + Path edgeChainsDir = getEdgeRuleChainsDir(); + loadRuleChainsFromPath(tenantId, edgeChainsDir); + } + + private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) throws IOException { + try (DirectoryStream dirStream = Files.newDirectoryStream(ruleChainsPath, path -> path.toString().endsWith(InstallScripts.JSON_EXT))) { dirStream.forEach( path -> { try { @@ -159,7 +174,6 @@ public class InstallScripts { return ruleChain; } - public void loadSystemWidgets() throws Exception { Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR); try (DirectoryStream dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) { 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 922db4c2b7..9a989e38f8 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 @@ -449,8 +449,16 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService case "3.2.2": try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { log.info("Updating schema ..."); + try { + conn.createStatement().execute("ALTER TABLE rule_chain ADD COLUMN type varchar(255) DEFAULT 'CORE'"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + } catch (Exception ignored) { + } schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.2.2", SCHEMA_UPDATE_SQL); loadSql(schemaUpdateFile, conn); + log.info("Load Edge TTL functions ..."); + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.2.2", "schema_update_ttl.sql"); + loadSql(schemaUpdateFile, conn); + log.info("Edge TTL functions successfully loaded!"); conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003000;"); log.info("Schema updated."); } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java index cb987a5e65..296d9f0a8d 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java +++ b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java @@ -37,7 +37,6 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.time.Instant; import java.util.ArrayList; -import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.UUID; @@ -159,6 +158,8 @@ public class CassandraDbHelper { str = Float.valueOf(row.getFloat(index)).toString(); } else if (type.getProtocolCode() == ProtocolConstants.DataType.TIMESTAMP) { str = ""+row.getInstant(index).toEpochMilli(); + } else if (type.getProtocolCode() == ProtocolConstants.DataType.BOOLEAN) { + str = Boolean.valueOf(row.getBoolean(index)).toString(); } else { str = row.getString(index); } @@ -207,6 +208,8 @@ public class CassandraDbHelper { boundStatementBuilder.setFloat(column, Float.valueOf(value)); } else if (type.getProtocolCode() == ProtocolConstants.DataType.TIMESTAMP) { boundStatementBuilder.setInstant(column, Instant.ofEpochMilli(Long.valueOf(value))); + } else if (type.getProtocolCode() == ProtocolConstants.DataType.BOOLEAN) { + boundStatementBuilder.setBoolean(column, Boolean.valueOf(value)); } else { boundStatementBuilder.setString(column, value); } 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 f232c0a41e..3f93aef41a 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 @@ -87,6 +87,10 @@ public class DefaultDataUpdateService implements DataUpdateService { log.info("Updating data from version 3.1.1 to 3.2.0 ..."); tenantsRootRuleChainUpdater.updateEntities(null); break; + case "3.2.2": + log.info("Updating data from version 3.2.2 to 3.3.0 ..."); + tenantsDefaultEdgeRuleChainUpdater.updateEntities(null); + break; default: throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); } @@ -113,6 +117,27 @@ public class DefaultDataUpdateService implements DataUpdateService { } }; + private PaginatedUpdater tenantsDefaultEdgeRuleChainUpdater = + new PaginatedUpdater() { + + @Override + protected PageData findEntities(String region, PageLink pageLink) { + return tenantService.findTenants(pageLink); + } + + @Override + protected void updateEntity(Tenant tenant) { + try { + RuleChain defaultEdgeRuleChain = ruleChainService.getEdgeTemplateRootRuleChain(tenant.getId()); + if (defaultEdgeRuleChain == null) { + installScripts.createDefaultEdgeRuleChains(tenant.getId()); + } + } catch (Exception e) { + log.error("Unable to update Tenant", e); + } + } + }; + private PaginatedUpdater tenantsRootRuleChainUpdater = new PaginatedUpdater() { diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 6a1dc8f42f..972c99f866 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -20,7 +20,6 @@ import freemarker.template.Configuration; import freemarker.template.Template; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Lazy; @@ -374,7 +373,6 @@ public class DefaultMailService implements MailService { } } - @NotNull private String getValueAsString(long value) { if (value > _1M && value % _1M < _10K) { return value / _1M + "M"; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 1b684863de..2f5610d7ff 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -31,11 +31,13 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; 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.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -307,6 +309,21 @@ public class DefaultTbClusterService implements TbClusterService { } } + @Override + public void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId) { + log.trace("[{}] Processing edge {} event update ", tenantId, edgeId); + EdgeEventUpdateMsg msg = new EdgeEventUpdateMsg(tenantId, edgeId); + byte[] msgBytes = encodingService.encode(msg); + TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); + Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); + for (String serviceId : tbCoreServices) { + TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceId); + ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setEdgeEventUpdateMsg(ByteString.copyFrom(msgBytes)).build(); + toCoreNfProducer.send(tpi, new TbProtoQueueMsg<>(msg.getEdgeId().getId(), toCoreMsg), null); + toCoreNfs.incrementAndGet(); + } + } + private void broadcast(ComponentLifecycleMsg msg) { byte[] msgBytes = encodingService.encode(msg); TbQueueProducer> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer(); @@ -316,7 +333,8 @@ public class DefaultTbClusterService implements TbClusterService { || entityType.equals(EntityType.TENANT_PROFILE) || entityType.equals(EntityType.DEVICE_PROFILE) || entityType.equals(EntityType.API_USAGE_STATE) - || (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED)) { + || (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) + || entityType.equals(EntityType.EDGE)) { TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); for (String serviceId : tbCoreServices) { 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 02f69ce10d..71dd1a2e07 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 @@ -37,6 +37,7 @@ import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; +import org.thingsboard.server.gen.transport.TransportProtos.EdgeNotificationMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto; import org.thingsboard.server.gen.transport.TransportProtos.LocalSubscriptionServiceMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionMgrMsgProto; @@ -56,6 +57,7 @@ import org.thingsboard.server.queue.discovery.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; +import org.thingsboard.server.service.edge.EdgeNotificationService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.queue.processing.AbstractConsumerService; @@ -101,6 +103,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> usageStatsConsumer; @@ -117,7 +120,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService actorMsg = encodingService.decode(toCoreMsg.getToDeviceActorNotificationMsg().toByteArray()); if (actorMsg.isPresent()) { @@ -272,6 +280,13 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService actorMsg = encodingService.decode(toCoreNotification.getEdgeEventUpdateMsg().toByteArray()); + if (actorMsg.isPresent()) { + log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get()); + actorContext.tellWithHighPriority(actorMsg.get()); + } + callback.onSuccess(); } if (statsEnabled) { stats.log(toCoreNotification); @@ -406,6 +421,13 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService counters = new ArrayList<>(); @@ -66,6 +69,7 @@ public class TbCoreConsumerStats { this.deviceStateCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_STATES)); this.subscriptionMsgCounter = register(statsFactory.createStatsCounter(statsKey, SUBSCRIPTION_MSGS)); this.toCoreNotificationsCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NOTIFICATIONS)); + this.edgeNotificationsCounter = register(statsFactory.createStatsCounter(statsKey, EDGE_NOTIFICATIONS)); } private StatsCounter register(StatsCounter counter){ @@ -103,6 +107,11 @@ public class TbCoreConsumerStats { deviceStateCounter.increment(); } + public void log(TransportProtos.EdgeNotificationMsgProto msg) { + totalCounter.increment(); + edgeNotificationsCounter.increment(); + } + public void log(TransportProtos.SubscriptionMgrMsgProto msg) { totalCounter.increment(); subscriptionMsgCounter.increment(); diff --git a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java new file mode 100644 index 0000000000..81c59609ff --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java @@ -0,0 +1,202 @@ +/** + * Copyright © 2016-2021 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.resource; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.leshan.core.model.DDFFileParser; +import org.eclipse.leshan.core.model.DefaultDDFFileValidator; +import org.eclipse.leshan.core.model.InvalidDDFFileException; +import org.eclipse.leshan.core.model.ObjectModel; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.lwm2m.LwM2mInstance; +import org.thingsboard.server.common.data.lwm2m.LwM2mObject; +import org.thingsboard.server.common.data.lwm2m.LwM2mResourceObserve; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.resource.ResourceService; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY; +import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_SEARCH_TEXT; +import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID; +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Slf4j +@Service +public class DefaultTbResourceService implements TbResourceService { + + private final ResourceService resourceService; + private final DDFFileParser ddfFileParser; + + public DefaultTbResourceService(ResourceService resourceService) { + this.resourceService = resourceService; + this.ddfFileParser = new DDFFileParser(new DefaultDDFFileValidator()); + } + + @Override + public TbResource saveResource(TbResource resource) throws ThingsboardException { + log.trace("Executing saveResource [{}]", resource); + if (StringUtils.isEmpty(resource.getData())) { + throw new DataValidationException("Resource data should be specified!"); + } + if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType())) { + try { + List objectModels = + ddfFileParser.parseEx(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText()); + if (!objectModels.isEmpty()) { + ObjectModel objectModel = objectModels.get(0); + + String resourceKey = objectModel.id + LWM2M_SEPARATOR_KEY + objectModel.getVersion(); + String name = objectModel.name; + resource.setResourceKey(resourceKey); + if (resource.getId() == null) { + resource.setTitle(name + " id=" + objectModel.id + " v" + objectModel.getVersion()); + } + resource.setSearchText(resourceKey + LWM2M_SEPARATOR_SEARCH_TEXT + name); + } else { + throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", resource.getSearchText())); + } + } catch (InvalidDDFFileException | IOException e) { + throw new ThingsboardException(e, ThingsboardErrorCode.GENERAL); + } + if (resource.getResourceType().equals(ResourceType.LWM2M_MODEL) && toLwM2mObject(resource) == null) { + throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", resource.getSearchText())); + } + } else { + resource.setResourceKey(resource.getFileName()); + } + + return resourceService.saveResource(resource); + } + + @Override + public TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceId) { + return resourceService.getResource(tenantId, resourceType, resourceId); + } + + @Override + public TbResource findResourceById(TenantId tenantId, TbResourceId resourceId) { + return resourceService.findResourceById(tenantId, resourceId); + } + + @Override + public TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId) { + return resourceService.findResourceInfoById(tenantId, resourceId); + } + + @Override + public PageData findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) { + return resourceService.findAllTenantResourcesByTenantId(tenantId, pageLink); + } + + @Override + public PageData findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) { + return resourceService.findTenantResourcesByTenantId(tenantId, pageLink); + } + + @Override + public List findLwM2mObject(TenantId tenantId, String sortOrder, String sortProperty, String[] objectIds) { + log.trace("Executing findByTenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + List resources = resourceService.findTenantResourcesByResourceTypeAndObjectIds(tenantId, ResourceType.LWM2M_MODEL, + objectIds); + return resources.stream() + .map(this::toLwM2mObject) + .sorted(getComparator(sortProperty, sortOrder)) + .collect(Collectors.toList()); + } + + @Override + public List findLwM2mObjectPage(TenantId tenantId, String sortProperty, String sortOrder, PageLink pageLink) { + log.trace("Executing findByTenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + PageData resourcePageData = resourceService.findTenantResourcesByResourceTypeAndPageLink(tenantId, ResourceType.LWM2M_MODEL, pageLink); + return resourcePageData.getData().stream() + .map(this::toLwM2mObject) + .sorted(getComparator(sortProperty, sortOrder)) + .collect(Collectors.toList()); + } + + @Override + public void deleteResource(TenantId tenantId, TbResourceId resourceId) { + resourceService.deleteResource(tenantId, resourceId); + } + + @Override + public void deleteResourcesByTenantId(TenantId tenantId) { + resourceService.deleteResourcesByTenantId(tenantId); + } + + private Comparator getComparator(String sortProperty, String sortOrder) { + Comparator comparator; + if ("name".equals(sortProperty)) { + comparator = Comparator.comparing(LwM2mObject::getName); + } else { + comparator = Comparator.comparingLong(LwM2mObject::getId); + } + return "DESC".equals(sortOrder) ? comparator.reversed() : comparator; + } + + private LwM2mObject toLwM2mObject(TbResource resource) { + try { + DDFFileParser ddfFileParser = new DDFFileParser(new DefaultDDFFileValidator()); + List objectModels = + ddfFileParser.parseEx(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText()); + if (objectModels.size() == 0) { + return null; + } else { + ObjectModel obj = objectModels.get(0); + LwM2mObject lwM2mObject = new LwM2mObject(); + lwM2mObject.setId(obj.id); + lwM2mObject.setKeyId(resource.getResourceKey()); + lwM2mObject.setName(obj.name); + lwM2mObject.setMultiple(obj.multiple); + lwM2mObject.setMandatory(obj.mandatory); + LwM2mInstance instance = new LwM2mInstance(); + instance.setId(0); + List resources = new ArrayList<>(); + obj.resources.forEach((k, v) -> { + if (!v.operations.isExecutable()) { + LwM2mResourceObserve lwM2MResourceObserve = new LwM2mResourceObserve(k, v.name, false, false, false); + resources.add(lwM2MResourceObserve); + } + }); + instance.setResources(resources.toArray(LwM2mResourceObserve[]::new)); + lwM2mObject.setInstances(new LwM2mInstance[]{instance}); + return lwM2mObject; + } + } catch (IOException | InvalidDDFFileException e) { + log.error("Could not parse the XML of objectModel with name [{}]", resource.getSearchText(), e); + return null; + } + } +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java similarity index 90% rename from common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceService.java rename to application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java index d879827008..7ad1848138 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java @@ -13,24 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.resource; +package org.thingsboard.server.service.resource; -import org.eclipse.leshan.core.model.InvalidDDFFileException; +import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; -import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.lwm2m.LwM2mObject; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import java.io.IOException; import java.util.List; - public interface TbResourceService { - TbResource saveResource(TbResource resource) throws InvalidDDFFileException, IOException; + + TbResource saveResource(TbResource resource) throws ThingsboardException; TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceKey); @@ -55,4 +54,5 @@ public interface TbResourceService { void deleteResource(TenantId tenantId, TbResourceId resourceId); void deleteResourcesByTenantId(TenantId tenantId); + } diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java index 7e8dccb583..c3b038c35a 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java @@ -52,7 +52,6 @@ public class DefaultTbRuleEngineRpcService implements TbRuleEngineDeviceRpcServi private final TbClusterService clusterService; private final TbServiceInfoProvider serviceInfoProvider; - private final ConcurrentMap> toDeviceRpcRequests = new ConcurrentHashMap<>(); private Optional tbCoreRpcService; diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponse.java b/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponse.java index e73da4805c..b7ce551c0e 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponse.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponse.java @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import lombok.ToString; import org.thingsboard.rule.engine.api.RpcError; +import java.io.Serializable; import java.util.Optional; import java.util.UUID; @@ -28,7 +29,7 @@ import java.util.UUID; */ @RequiredArgsConstructor @ToString -public class FromDeviceRpcResponse { +public class FromDeviceRpcResponse implements Serializable { @Getter private final UUID id; private final String response; diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponseActorMsg.java b/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponseActorMsg.java new file mode 100644 index 0000000000..9efe9a3b1f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/rpc/FromDeviceRpcResponseActorMsg.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2021 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.rpc; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; + +@ToString +@RequiredArgsConstructor +public class FromDeviceRpcResponseActorMsg implements ToDeviceActorNotificationMsg { + + @Getter + private final Integer requestId; + @Getter + private final TenantId tenantId; + @Getter + private final DeviceId deviceId; + + @Getter + private final FromDeviceRpcResponse msg; + + @Override + public MsgType getMsgType() { + return MsgType.DEVICE_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/TbRuleEngineDeviceRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/TbRuleEngineDeviceRpcService.java index 25a8bd3c01..3b920ac2eb 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/TbRuleEngineDeviceRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/TbRuleEngineDeviceRpcService.java @@ -28,5 +28,4 @@ public interface TbRuleEngineDeviceRpcService extends RuleEngineRpcService { * @param response the RPC response */ void processRpcResponseFromDevice(FromDeviceRpcResponse response); - } diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java index 748cad8247..1627002fb1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java @@ -33,12 +33,14 @@ import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; @@ -54,6 +56,7 @@ import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.rule.RuleChainService; @@ -83,6 +86,7 @@ public class AccessValidator { public static final String CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "Customer user is not allowed to perform this operation!"; public static final String SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "System administrator is not allowed to perform this operation!"; public static final String DEVICE_WITH_REQUESTED_ID_NOT_FOUND = "Device with requested id wasn't found!"; + public static final String EDGE_WITH_REQUESTED_ID_NOT_FOUND = "Edge with requested id wasn't found!"; public static final String ENTITY_VIEW_WITH_REQUESTED_ID_NOT_FOUND = "Entity-view with requested id wasn't found!"; @Autowired @@ -112,6 +116,9 @@ public class AccessValidator { @Autowired protected EntityViewService entityViewService; + @Autowired(required = false) + protected EdgeService edgeService; + @Autowired protected AccessControlService accessControlService; @@ -204,6 +211,9 @@ public class AccessValidator { case ENTITY_VIEW: validateEntityView(currentUser, operation, entityId, callback); return; + case EDGE: + validateEdge(currentUser, operation, entityId, callback); + return; case API_USAGE_STATE: validateApiUsageState(currentUser, operation, entityId, callback); return; @@ -423,6 +433,26 @@ public class AccessValidator { } } + private void validateEdge(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) { + if (currentUser.isSystemAdmin()) { + callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); + } else { + ListenableFuture edgeFuture = edgeService.findEdgeByIdAsync(currentUser.getTenantId(), new EdgeId(entityId.getId())); + Futures.addCallback(edgeFuture, getCallback(callback, edge -> { + if (edge == null) { + return ValidationResult.entityNotFound(EDGE_WITH_REQUESTED_ID_NOT_FOUND); + } else { + try { + accessControlService.checkPermission(currentUser, Resource.EDGE, operation, entityId, edge); + } catch (ThingsboardException e) { + return ValidationResult.accessDenied(e.getMessage()); + } + return ValidationResult.ok(edge); + } + }), executor); + } + } + private FutureCallback getCallback(FutureCallback callback, Function> transformer) { return new FutureCallback() { @Override diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java index b3516bd364..4f38cf7a23 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java @@ -17,8 +17,10 @@ package org.thingsboard.server.service.security.auth.oauth2; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -85,6 +87,10 @@ public abstract class AbstractOAuth2ClientMapper { @Autowired protected TbClusterService tbClusterService; + @Value("${edges.enabled}") + @Getter + private boolean edgesEnabled; + private final Lock userCreationLock = new ReentrantLock(); protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, OAuth2ClientRegistrationInfo clientRegistration) { @@ -171,6 +177,9 @@ public abstract class AbstractOAuth2ClientMapper { tenant.setTitle(tenantName); tenant = tenantService.saveTenant(tenant); installScripts.createDefaultRuleChains(tenant.getId()); + if (edgesEnabled) { + installScripts.createDefaultEdgeRuleChains(tenant.getId()); + } tenantProfileCache.evict(tenant.getId()); tbClusterService.onTenantChange(tenant, null); tbClusterService.onEntityStateChange(tenant.getId(), tenant.getId(), diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java index f56d5dc17e..fc1705d688 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java @@ -40,6 +40,7 @@ public class CustomerUserPermissions extends AbstractPermissions { put(Resource.USER, userPermissionChecker); put(Resource.WIDGETS_BUNDLE, widgetsPermissionChecker); put(Resource.WIDGET_TYPE, widgetsPermissionChecker); + put(Resource.EDGE, customerEntityPermissionChecker); } private static final PermissionChecker customerEntityPermissionChecker = diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Operation.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Operation.java index ead0c52fb7..074943f1a6 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Operation.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Operation.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.security.permission; public enum Operation { ALL, CREATE, READ, WRITE, DELETE, ASSIGN_TO_CUSTOMER, UNASSIGN_FROM_CUSTOMER, RPC_CALL, - READ_CREDENTIALS, WRITE_CREDENTIALS, READ_ATTRIBUTES, WRITE_ATTRIBUTES, READ_TELEMETRY, WRITE_TELEMETRY, CLAIM_DEVICES, ASSIGN_TO_TENANT + READ_CREDENTIALS, WRITE_CREDENTIALS, READ_ATTRIBUTES, WRITE_ATTRIBUTES, READ_TELEMETRY, WRITE_TELEMETRY, CLAIM_DEVICES, + ASSIGN_TO_TENANT, ASSIGN_TO_EDGE, UNASSIGN_FROM_EDGE } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index b6ac71169e..75119c402d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -38,7 +38,8 @@ public enum Resource { DEVICE_PROFILE(EntityType.DEVICE_PROFILE), API_USAGE_STATE(EntityType.API_USAGE_STATE), TB_RESOURCE(EntityType.TB_RESOURCE), - FIRMWARE(EntityType.FIRMWARE); + FIRMWARE(EntityType.FIRMWARE), + EDGE(EntityType.EDGE); private final EntityType entityType; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index a6fce632b8..af08e38087 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -43,6 +43,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.API_USAGE_STATE, tenantEntityPermissionChecker); put(Resource.TB_RESOURCE, tbResourcePermissionChecker); put(Resource.FIRMWARE, tenantEntityPermissionChecker); + put(Resource.EDGE, tenantEntityPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 768eb592f4..6da8db7a93 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -61,7 +61,7 @@ import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponse; import org.thingsboard.server.dao.firmware.FirmwareService; import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.dao.resource.TbResourceService; +import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; @@ -83,6 +83,7 @@ import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.queue.TbClusterService; +import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.state.DeviceStateService; import java.util.UUID; @@ -123,7 +124,7 @@ public class DefaultTransportApiService implements TransportApiService { RelationService relationService, DeviceCredentialsService deviceCredentialsService, DeviceStateService deviceStateService, DbCallbackExecutorService dbCallbackExecutorService, TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService, - DeviceProvisionService deviceProvisionService, TbResourceService resourceService, FirmwareService firmwareService, CacheManager cacheManager, FirmwareCacheWriter firmwareCacheWriter) { + DeviceProvisionService deviceProvisionService, TbResourceService resourceService, FirmwareService firmwareService, FirmwareCacheWriter firmwareCacheWriter) { this.deviceProfileCache = deviceProfileCache; this.tenantProfileCache = tenantProfileCache; this.apiUsageStateService = apiUsageStateService; diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/edge/EdgeEventsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/edge/EdgeEventsCleanUpService.java new file mode 100644 index 0000000000..0c21719451 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ttl/edge/EdgeEventsCleanUpService.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2021 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.ttl.edge; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.thingsboard.server.dao.util.PsqlDao; +import org.thingsboard.server.service.ttl.AbstractCleanUpService; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +@PsqlDao +@Slf4j +@Service +public class EdgeEventsCleanUpService extends AbstractCleanUpService { + + @Value("${sql.ttl.edge_events.edge_events_ttl}") + private long ttl; + + @Value("${sql.ttl.edge_events.enabled}") + private boolean ttlTaskExecutionEnabled; + + @Scheduled(initialDelayString = "${sql.ttl.edge_events.execution_interval_ms}", fixedDelayString = "${sql.ttl.edge_events.execution_interval_ms}") + public void cleanUp() { + if (ttlTaskExecutionEnabled) { + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + doCleanUp(conn); + } catch (SQLException e) { + log.error("SQLException occurred during TTL task execution ", e); + } + } + } + + @Override + protected void doCleanUp(Connection connection) throws SQLException { + long totalEdgeEventsRemoved = executeQuery(connection, "call cleanup_edge_events_by_ttl(" + ttl + ", 0);"); + log.info("Total edge events removed by TTL: [{}]", totalEdgeEventsRemoved); + } +} diff --git a/application/src/main/resources/banner.txt b/application/src/main/resources/banner.txt index 111465dd8a..57f907c5e0 100644 --- a/application/src/main/resources/banner.txt +++ b/application/src/main/resources/banner.txt @@ -1,8 +1,8 @@ - ______ __ _ __ __ - /_ __/ / /_ (_) ____ ____ _ _____ / /_ ____ ____ _ _____ ____/ / - / / / __ \ / / / __ \ / __ `/ / ___/ / __ \ / __ \ / __ `/ / ___/ / __ / - / / / / / / / / / / / / / /_/ / (__ ) / /_/ // /_/ // /_/ / / / / /_/ / -/_/ /_/ /_/ /_/ /_/ /_/ \__, / /____/ /_.___/ \____/ \__,_/ /_/ \__,_/ + ______ __ _ ____ __ + /_ __/ / /_ (_) ____ ____ _ _____ / __ ) ____ ____ _ _____ ____/ / + / / / __ \ / / / __ \ / __ `/ / ___/ / __ | / __ \ / __ `/ / ___/ / __ / + / / / / / / / / / / / / / /_/ / (__ ) / /_/ / / /_/ // /_/ / / / / /_/ / +/_/ /_/ /_/ /_/ /_/ /_/ \__, / /____/ /_____/ \____/ \__,_/ /_/ \__,_/ /____/ =================================================== diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index f8085ec985..caf78e1537 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -37,6 +37,9 @@ + + + diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index a493e1ad48..0fd38db24e 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -243,6 +243,7 @@ sql: batch_max_delay: "${SQL_TS_LATEST_BATCH_MAX_DELAY_MS:100}" stats_print_interval_ms: "${SQL_TS_LATEST_BATCH_STATS_PRINT_MS:10000}" batch_threads: "${SQL_TS_LATEST_BATCH_THREADS:4}" + update_by_latest_ts: "${SQL_TS_UPDATE_BY_LATEST_TIMESTAMP:true}" # Specify whether to sort entities before batch update. Should be enabled for cluster mode to avoid deadlocks batch_sort: "${SQL_BATCH_SORT:false}" # Specify whether to remove null characters from strValue of attributes and timeseries before insert @@ -267,6 +268,10 @@ sql: execution_interval_ms: "${SQL_TTL_EVENTS_EXECUTION_INTERVAL:86400000}" # Number of milliseconds. The current value corresponds to one day events_ttl: "${SQL_TTL_EVENTS_EVENTS_TTL:0}" # Number of seconds debug_events_ttl: "${SQL_TTL_EVENTS_DEBUG_EVENTS_TTL:604800}" # Number of seconds. The current value corresponds to one week + edge_events: + enabled: "${SQL_TTL_EDGE_EVENTS_ENABLED:true}" + execution_interval_ms: "${SQL_TTL_EDGE_EVENTS_EXECUTION_INTERVAL:86400000}" # Number of milliseconds. The current value corresponds to one day + edge_events_ttl: "${SQL_TTL_EDGE_EVENTS_TTL:2628000}" # Number of seconds. The current value corresponds to one month # Actor system parameters actors: @@ -281,7 +286,7 @@ actors: tenant: create_components_on_init: "${ACTORS_TENANT_CREATE_COMPONENTS_ON_INIT:true}" session: - max_concurrent_sessions_per_device: "${ACTORS_MAX_CONCURRENT_SESSION_PER_DEVICE:2}" + max_concurrent_sessions_per_device: "${ACTORS_MAX_CONCURRENT_SESSION_PER_DEVICE:1}" sync: # Default timeout for processing request using synchronous session (HTTP, CoAP) in milliseconds timeout: "${ACTORS_SESSION_SYNC_TIMEOUT:10000}" @@ -368,6 +373,9 @@ caffeine: firmwares: timeToLiveInMinutes: 1440 maxSize: 100 + edges: + timeToLiveInMinutes: 1440 + maxSize: 0 redis: # standalone or cluster @@ -486,6 +494,7 @@ audit-log: "alarm": "${AUDIT_LOG_MASK_ALARM:W}" "entity_view": "${AUDIT_LOG_MASK_ENTITY_VIEW:W}" "device_profile": "${AUDIT_LOG_MASK_DEVICE_PROFILE:W}" + "edge": "${AUDIT_LOG_MASK_EDGE:W}" sink: # Type of external sink. possible options: none, elasticsearch type: "${AUDIT_LOG_SINK_TYPE:none}" @@ -514,7 +523,7 @@ js: # Built-in JVM JavaScript environment properties local: # Use Sandboxed (secured) JVM JavaScript environment - use_js_sandbox: "${USE_LOCAL_JS_SANDBOX:false}" + use_js_sandbox: "${USE_LOCAL_JS_SANDBOX:true}" # Specify thread pool size for JavaScript sandbox resource monitor monitor_thread_pool_size: "${LOCAL_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}" # Maximum CPU time in milliseconds allowed for script execution @@ -595,6 +604,10 @@ transport: dtls: # Enable/disable DTLS 1.2 support enabled: "${COAP_DTLS_ENABLED:false}" + # CoAP DTLS bind address + bind_address: "${COAP_DTLS_BIND_ADDRESS:0.0.0.0}" + # CoAP DTLS bind port + bind_port: "${COAP_DTLS_BIND_PORT:5684}" # Secure mode. Allowed values: NO_AUTH, X509 mode: "${COAP_DTLS_SECURE_MODE:NO_AUTH}" # Path to the key store that holds the certificate @@ -671,6 +684,26 @@ transport: # Use redis for Security and Registration stores redis.enabled: "${LWM2M_REDIS_ENABLED:false}" +# Edges parameters +edges: + enabled: "${EDGES_ENABLED:false}" + rpc: + port: "${EDGES_RPC_PORT:7070}" + client_max_keep_alive_time_sec: "${EDGES_RPC_CLIENT_MAX_KEEP_ALIVE_TIME_SEC:300}" + ssl: + # Enable/disable SSL support + enabled: "${EDGES_RPC_SSL_ENABLED:false}" + cert: "${EDGES_RPC_SSL_CERT:certChainFile.pem}" + private_key: "${EDGES_RPC_SSL_PRIVATE_KEY:privateKeyFile.pem}" + storage: + max_read_records_count: "${EDGES_STORAGE_MAX_READ_RECORDS_COUNT:50}" + no_read_records_sleep: "${EDGES_NO_READ_RECORDS_SLEEP:1000}" + sleep_between_batches: "${EDGES_SLEEP_BETWEEN_BATCHES:1000}" + scheduler_pool_size: "${EDGES_SCHEDULER_POOL_SIZE:4}" + edge_events_ttl: "${EDGES_EDGE_EVENTS_TTL:0}" + state: + persistToTelemetry: "${EDGES_PERSIST_STATE_TO_TELEMETRY:false}" + swagger: api_path_regex: "${SWAGGER_API_PATH_REGEX:/api.*}" security_path_regex: "${SWAGGER_SECURITY_PATH_REGEX:/api.*}" diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 3bbf29c8c8..2329e8086c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -23,6 +23,7 @@ import io.jsonwebtoken.Header; import io.jsonwebtoken.Jwt; import io.jsonwebtoken.Jwts; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.hamcrest.Matcher; import org.junit.After; @@ -57,6 +58,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.MqttTopics; @@ -562,4 +564,18 @@ public abstract class AbstractWebTest { return jsonPath("$.message", matcher); } + protected Edge constructEdge(String name, String type) { + return constructEdge(tenantId, name, type); + } + protected Edge constructEdge(TenantId tenantId, String name, String type) { + Edge edge = new Edge(); + edge.setTenantId(tenantId); + edge.setName(name); + edge.setType(type); + edge.setSecret(RandomStringUtils.randomAlphanumeric(20)); + edge.setRoutingKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setEdgeLicenseKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setCloudEndpoint("http://localhost:8080"); + return edge; + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java index 5ec0ed4fe0..244eb0e05d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -672,4 +673,30 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { Assert.assertEquals(0, pageData.getData().size()); } + @Test + public void testAssignAssetToEdge() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + Asset asset = new Asset(); + asset.setName("My asset"); + asset.setType("default"); + Asset savedAsset = doPost("/api/asset", asset, Asset.class); + + doPost("/api/edge/" + savedEdge.getId().getId().toString() + + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); + + PageData pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId().toString() + "/assets?", + new TypeReference>() {}, new PageLink(100)); + + Assert.assertEquals(1, pageData.getData().size()); + + doDelete("/api/edge/" + savedEdge.getId().getId().toString() + + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); + + pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId().toString() + "/assets?", + new TypeReference>() {}, new PageLink(100)); + + Assert.assertEquals(0, pageData.getData().size()); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseComponentDescriptorControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseComponentDescriptorControllerTest.java index 635505c1c5..977583b4c9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseComponentDescriptorControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseComponentDescriptorControllerTest.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentScope; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.Authority; import java.util.List; @@ -81,14 +82,14 @@ public abstract class BaseComponentDescriptorControllerTest extends AbstractCont @Test public void testGetByType() throws Exception { List descriptors = readResponse( - doGet("/api/components/" + ComponentType.FILTER).andExpect(status().isOk()), new TypeReference>() { + doGet("/api/components?componentTypes={componentTypes}&ruleChainType={ruleChainType}", ComponentType.FILTER, RuleChainType.CORE).andExpect(status().isOk()), new TypeReference>() { }); Assert.assertNotNull(descriptors); Assert.assertTrue(descriptors.size() >= AMOUNT_OF_DEFAULT_FILTER_NODES); for (ComponentType type : ComponentType.values()) { - doGet("/api/components/" + type).andExpect(status().isOk()); + doGet("/api/components?componentTypes={componentTypes}&ruleChainType={ruleChainType}", type, RuleChainType.CORE).andExpect(status().isOk()); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java index 6e19ff8b13..60b4128878 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java @@ -15,27 +15,30 @@ */ package org.thingsboard.server.controller; -import static org.hamcrest.Matchers.containsString; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.lang3.RandomStringUtils; -import org.thingsboard.server.common.data.*; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.page.TimePageLink; -import org.thingsboard.server.common.data.security.Authority; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.Authority; -import com.fasterxml.jackson.core.type.TypeReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public abstract class BaseDashboardControllerTest extends AbstractControllerTest { @@ -348,4 +351,30 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest Assert.assertEquals(dashboards, loadedDashboards); } + @Test + public void testAssignDashboardToEdge() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("My dashboard"); + Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class); + + doPost("/api/edge/" + savedEdge.getId().getId().toString() + + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); + + PageData pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId().toString() + "/dashboards?", + new TypeReference>() {}, new PageLink(100)); + + Assert.assertEquals(1, pageData.getData().size()); + + doDelete("/api/edge/" + savedEdge.getId().getId().toString() + + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); + + pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId().toString() + "/dashboards?", + new TypeReference>() {}, new PageLink(100)); + + Assert.assertEquals(0, pageData.getData().size()); + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java index e6cb4667ba..fa5a6b76eb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java @@ -24,10 +24,10 @@ import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceCredentialsId; import org.thingsboard.server.common.data.id.DeviceId; @@ -463,7 +463,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } pageLink = new PageLink(4, 0, title1); - pageData = doGetTypedWithPageLink("/api/tenant/devices?", + pageData = doGetTypedWithPageLink("/api/tenant/devices?", new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -473,7 +473,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } pageLink = new PageLink(4, 0, title2); - pageData = doGetTypedWithPageLink("/api/tenant/devices?", + pageData = doGetTypedWithPageLink("/api/tenant/devices?", new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -669,7 +669,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } pageLink = new PageLink(4, 0, title1); - pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -679,7 +679,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } pageLink = new PageLink(4, 0, title2); - pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", new TypeReference>(){}, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -827,4 +827,31 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { doDelete("/api/tenant/" + savedDifferentTenant.getId().getId().toString()) .andExpect(status().isOk()); } + + @Test + public void testAssignDeviceToEdge() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + Device device = new Device(); + device.setName("My device"); + device.setType("default"); + Device savedDevice = doPost("/api/device", device, Device.class); + + doPost("/api/edge/" + savedEdge.getId().getId().toString() + + "/device/" + savedDevice.getId().getId().toString(), Device.class); + + PageData pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId().toString() + "/devices?", + new TypeReference>() {}, new PageLink(100)); + + Assert.assertEquals(1, pageData.getData().size()); + + doDelete("/api/edge/" + savedEdge.getId().getId().toString() + + "/device/" + savedDevice.getId().getId().toString(), Device.class); + + pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId().toString() + "/devices?", + new TypeReference>() {}, new PageLink(100)); + + Assert.assertEquals(0, pageData.getData().size()); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java new file mode 100644 index 0000000000..246749881f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java @@ -0,0 +1,711 @@ +/** + * Copyright © 2016-2021 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.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.CustomerId; +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.security.Authority; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.edge.imitator.EdgeImitator; +import org.thingsboard.server.gen.edge.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceProfileUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.UserUpdateMsg; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.nullValue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; + +public abstract class BaseEdgeControllerTest extends AbstractControllerTest { + + private IdComparator idComparator = new IdComparator<>(); + + private Tenant savedTenant; + private TenantId tenantId; + private User tenantAdmin; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + tenantId = savedTenant.getId(); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testSaveEdge() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + Assert.assertNotNull(savedEdge); + Assert.assertNotNull(savedEdge.getId()); + Assert.assertTrue(savedEdge.getCreatedTime() > 0); + Assert.assertEquals(savedTenant.getId(), savedEdge.getTenantId()); + Assert.assertNotNull(savedEdge.getCustomerId()); + Assert.assertEquals(NULL_UUID, savedEdge.getCustomerId().getId()); + Assert.assertEquals(edge.getName(), savedEdge.getName()); + Assert.assertTrue(StringUtils.isNoneBlank(savedEdge.getEdgeLicenseKey())); + Assert.assertTrue(StringUtils.isNoneBlank(savedEdge.getCloudEndpoint())); + + savedEdge.setName("My new edge"); + doPost("/api/edge", savedEdge, Edge.class); + + Edge foundEdge = doGet("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); + Assert.assertEquals(foundEdge.getName(), savedEdge.getName()); + } + + @Test + public void testFindEdgeById() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + Edge foundEdge = doGet("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); + Assert.assertNotNull(foundEdge); + Assert.assertEquals(savedEdge, foundEdge); + } + + @Test + public void testFindEdgeTypesByTenantId() throws Exception { + List edges = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + Edge edge = constructEdge("My edge B" + i, "typeB"); + edges.add(doPost("/api/edge", edge, Edge.class)); + } + for (int i = 0; i < 7; i++) { + Edge edge = constructEdge("My edge C" + i, "typeC"); + edges.add(doPost("/api/edge", edge, Edge.class)); + } + for (int i = 0; i < 9; i++) { + Edge edge = constructEdge("My edge A" + i, "typeA"); + edges.add(doPost("/api/edge", edge, Edge.class)); + } + List edgeTypes = doGetTyped("/api/edge/types", + new TypeReference>() { + }); + + Assert.assertNotNull(edgeTypes); + Assert.assertEquals(3, edgeTypes.size()); + Assert.assertEquals("typeA", edgeTypes.get(0).getType()); + Assert.assertEquals("typeB", edgeTypes.get(1).getType()); + Assert.assertEquals("typeC", edgeTypes.get(2).getType()); + } + + @Test + public void testDeleteEdge() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + doDelete("/api/edge/" + savedEdge.getId().getId().toString()) + .andExpect(status().isOk()); + + doGet("/api/edge/" + savedEdge.getId().getId().toString()) + .andExpect(status().isNotFound()); + } + + @Test + public void testSaveEdgeWithEmptyType() throws Exception { + Edge edge = constructEdge("My edge", null); + doPost("/api/edge", edge) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Edge type should be specified"))); + } + + @Test + public void testSaveEdgeWithEmptyName() throws Exception { + Edge edge = constructEdge(null, "default"); + doPost("/api/edge", edge) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Edge name should be specified"))); + } + + @Test + public void testAssignUnassignEdgeToCustomer() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + Customer customer = new Customer(); + customer.setTitle("My customer"); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + + Edge assignedEdge = doPost("/api/customer/" + savedCustomer.getId().getId().toString() + + "/edge/" + savedEdge.getId().getId().toString(), Edge.class); + Assert.assertEquals(savedCustomer.getId(), assignedEdge.getCustomerId()); + + Edge foundEdge = doGet("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); + Assert.assertEquals(savedCustomer.getId(), foundEdge.getCustomerId()); + + Edge unassignedEdge = + doDelete("/api/customer/edge/" + savedEdge.getId().getId().toString(), Edge.class); + Assert.assertEquals(ModelConstants.NULL_UUID, unassignedEdge.getCustomerId().getId()); + + foundEdge = doGet("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); + Assert.assertEquals(ModelConstants.NULL_UUID, foundEdge.getCustomerId().getId()); + } + + @Test + public void testAssignEdgeToNonExistentCustomer() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + doPost("/api/customer/" + Uuids.timeBased().toString() + + "/edge/" + savedEdge.getId().getId().toString()) + .andExpect(status().isNotFound()); + } + + @Test + public void testAssignEdgeToCustomerFromDifferentTenant() throws Exception { + loginSysAdmin(); + + Tenant tenant2 = new Tenant(); + tenant2.setTitle("Different tenant"); + Tenant savedTenant2 = doPost("/api/tenant", tenant2, Tenant.class); + Assert.assertNotNull(savedTenant2); + + User tenantAdmin2 = new User(); + tenantAdmin2.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin2.setTenantId(savedTenant2.getId()); + tenantAdmin2.setEmail("tenant3@thingsboard.org"); + tenantAdmin2.setFirstName("Joe"); + tenantAdmin2.setLastName("Downs"); + + tenantAdmin2 = createUserAndLogin(tenantAdmin2, "testPassword1"); + + Customer customer = new Customer(); + customer.setTitle("Different customer"); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + + login(tenantAdmin.getEmail(), "testPassword1"); + + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + doPost("/api/customer/" + savedCustomer.getId().getId().toString() + + "/edge/" + savedEdge.getId().getId().toString()) + .andExpect(status().isForbidden()); + + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant2.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testFindTenantEdges() throws Exception { + List edges = new ArrayList<>(); + for (int i = 0; i < 178; i++) { + Edge edge = constructEdge("Edge" + i, "default"); + edges.add(doPost("/api/edge", edge, Edge.class)); + } + List loadedEdges = new ArrayList<>(); + PageLink pageLink = new PageLink(23); + PageData pageData = null; + do { + pageData = doGetTypedWithPageLink("/api/tenant/edges?", + new TypeReference>() { + }, pageLink); + loadedEdges.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edges, idComparator); + Collections.sort(loadedEdges, idComparator); + + Assert.assertEquals(edges, loadedEdges); + } + + @Test + public void testFindTenantEdgesByName() throws Exception { + String title1 = "Edge title 1"; + List edgesTitle1 = new ArrayList<>(); + for (int i = 0; i < 143; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title1 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, "default"); + edgesTitle1.add(doPost("/api/edge", edge, Edge.class)); + } + String title2 = "Edge title 2"; + List edgesTitle2 = new ArrayList<>(); + for (int i = 0; i < 75; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title2 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, "default"); + edgesTitle2.add(doPost("/api/edge", edge, Edge.class)); + } + + List loadedEdgesTitle1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; + do { + pageData = doGetTypedWithPageLink("/api/tenant/edges?", + new TypeReference>() { + }, pageLink); + loadedEdgesTitle1.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesTitle1, idComparator); + Collections.sort(loadedEdgesTitle1, idComparator); + + Assert.assertEquals(edgesTitle1, loadedEdgesTitle1); + + List loadedEdgesTitle2 = new ArrayList<>(); + pageLink = new PageLink(4, 0, title2); + do { + pageData = doGetTypedWithPageLink("/api/tenant/edges?", + new TypeReference>() { + }, pageLink); + loadedEdgesTitle2.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesTitle2, idComparator); + Collections.sort(loadedEdgesTitle2, idComparator); + + Assert.assertEquals(edgesTitle2, loadedEdgesTitle2); + + for (Edge edge : loadedEdgesTitle1) { + doDelete("/api/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + + pageLink = new PageLink(4, 0, title1); + pageData = doGetTypedWithPageLink("/api/tenant/edges?", + new TypeReference>() { + }, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + + for (Edge edge : loadedEdgesTitle2) { + doDelete("/api/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + + pageLink = new PageLink(4, 0, title2); + pageData = doGetTypedWithPageLink("/api/tenant/edges?", + new TypeReference>() { + }, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + } + + @Test + public void testFindTenantEdgesByType() throws Exception { + String title1 = "Edge title 1"; + String type1 = "typeA"; + List edgesType1 = new ArrayList<>(); + for (int i = 0; i < 143; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title1 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, type1); + edgesType1.add(doPost("/api/edge", edge, Edge.class)); + } + String title2 = "Edge title 2"; + String type2 = "typeB"; + List edgesType2 = new ArrayList<>(); + for (int i = 0; i < 75; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title2 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, type2); + edgesType2.add(doPost("/api/edge", edge, Edge.class)); + } + + List loadedEdgesType1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15); + PageData pageData = null; + do { + pageData = doGetTypedWithPageLink("/api/tenant/edges?type={type}&", + new TypeReference>() { + }, pageLink, type1); + loadedEdgesType1.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesType1, idComparator); + Collections.sort(loadedEdgesType1, idComparator); + + Assert.assertEquals(edgesType1, loadedEdgesType1); + + List loadedEdgesType2 = new ArrayList<>(); + pageLink = new PageLink(4); + do { + pageData = doGetTypedWithPageLink("/api/tenant/edges?type={type}&", + new TypeReference>() { + }, pageLink, type2); + loadedEdgesType2.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesType2, idComparator); + Collections.sort(loadedEdgesType2, idComparator); + + Assert.assertEquals(edgesType2, loadedEdgesType2); + + for (Edge edge : loadedEdgesType1) { + doDelete("/api/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + + pageLink = new PageLink(4); + pageData = doGetTypedWithPageLink("/api/tenant/edges?type={type}&", + new TypeReference>() { + }, pageLink, type1); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + + for (Edge edge : loadedEdgesType2) { + doDelete("/api/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + + pageLink = new PageLink(4); + pageData = doGetTypedWithPageLink("/api/tenant/edges?type={type}&", + new TypeReference>() { + }, pageLink, type2); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + } + + @Test + public void testFindCustomerEdges() throws Exception { + Customer customer = new Customer(); + customer.setTitle("Test customer"); + customer = doPost("/api/customer", customer, Customer.class); + CustomerId customerId = customer.getId(); + + List edges = new ArrayList<>(); + for (int i = 0; i < 128; i++) { + Edge edge = constructEdge("Edge" + i, "default"); + edge = doPost("/api/edge", edge, Edge.class); + edges.add(doPost("/api/customer/" + customerId.getId().toString() + + "/edge/" + edge.getId().getId().toString(), Edge.class)); + } + + List loadedEdges = new ArrayList<>(); + PageLink pageLink = new PageLink(23); + PageData pageData = null; + do { + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?", + new TypeReference>() { + }, pageLink); + loadedEdges.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edges, idComparator); + Collections.sort(loadedEdges, idComparator); + + Assert.assertEquals(edges, loadedEdges); + } + + @Test + public void testFindCustomerEdgesByName() throws Exception { + Customer customer = new Customer(); + customer.setTitle("Test customer"); + customer = doPost("/api/customer", customer, Customer.class); + CustomerId customerId = customer.getId(); + + String title1 = "Edge title 1"; + List edgesTitle1 = new ArrayList<>(); + for (int i = 0; i < 125; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title1 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, "default"); + edge = doPost("/api/edge", edge, Edge.class); + edgesTitle1.add(doPost("/api/customer/" + customerId.getId().toString() + + "/edge/" + edge.getId().getId().toString(), Edge.class)); + } + String title2 = "Edge title 2"; + List edgesTitle2 = new ArrayList<>(); + for (int i = 0; i < 143; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title2 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, "default"); + edge = doPost("/api/edge", edge, Edge.class); + edgesTitle2.add(doPost("/api/customer/" + customerId.getId().toString() + + "/edge/" + edge.getId().getId().toString(), Edge.class)); + } + + List loadedEdgesTitle1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15,0, title1); + PageData pageData = null; + do { + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?", + new TypeReference>() { + }, pageLink); + loadedEdgesTitle1.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesTitle1, idComparator); + Collections.sort(loadedEdgesTitle1, idComparator); + + Assert.assertEquals(edgesTitle1, loadedEdgesTitle1); + + List loadedEdgesTitle2 = new ArrayList<>(); + pageLink = new PageLink(4,0, title2); + do { + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?", + new TypeReference>() { + }, pageLink); + loadedEdgesTitle2.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesTitle2, idComparator); + Collections.sort(loadedEdgesTitle2, idComparator); + + Assert.assertEquals(edgesTitle2, loadedEdgesTitle2); + + for (Edge edge : loadedEdgesTitle1) { + doDelete("/api/customer/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + + pageLink = new PageLink(4, 0, title1); + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?", + new TypeReference>() { + }, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + + for (Edge edge : loadedEdgesTitle2) { + doDelete("/api/customer/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + + pageLink = new PageLink(4, 0, title2); + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?", + new TypeReference>() { + }, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + } + + @Test + public void testFindCustomerEdgesByType() throws Exception { + Customer customer = new Customer(); + customer.setTitle("Test customer"); + customer = doPost("/api/customer", customer, Customer.class); + CustomerId customerId = customer.getId(); + + String title1 = "Edge title 1"; + String type1 = "typeC"; + List edgesType1 = new ArrayList<>(); + for (int i = 0; i < 125; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title1 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, type1); + edge = doPost("/api/edge", edge, Edge.class); + edgesType1.add(doPost("/api/customer/" + customerId.getId().toString() + + "/edge/" + edge.getId().getId().toString(), Edge.class)); + } + String title2 = "Edge title 2"; + String type2 = "typeD"; + List edgesType2 = new ArrayList<>(); + for (int i = 0; i < 143; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title2 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, type2); + edge = doPost("/api/edge", edge, Edge.class); + edgesType2.add(doPost("/api/customer/" + customerId.getId().toString() + + "/edge/" + edge.getId().getId().toString(), Edge.class)); + } + + List loadedEdgesType1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; + do { + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?type={type}&", + new TypeReference>() { + }, pageLink, type1); + loadedEdgesType1.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesType1, idComparator); + Collections.sort(loadedEdgesType1, idComparator); + + Assert.assertEquals(edgesType1, loadedEdgesType1); + + List loadedEdgesType2 = new ArrayList<>(); + pageLink = new PageLink(4, 0, title2); + do { + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?type={type}&", + new TypeReference>() { + }, pageLink, type2); + loadedEdgesType2.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesType2, idComparator); + Collections.sort(loadedEdgesType2, idComparator); + + Assert.assertEquals(edgesType2, loadedEdgesType2); + + for (Edge edge : loadedEdgesType1) { + doDelete("/api/customer/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + + pageLink = new PageLink(4, 0, title1); + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?type={type}&", + new TypeReference>() { + }, pageLink, type1); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + + for (Edge edge : loadedEdgesType2) { + doDelete("/api/customer/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + + pageLink = new PageLink(4, 0, title2); + pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/edges?type={type}&", + new TypeReference>() { + }, pageLink, type2); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + } + + @Test + public void testSyncEdge() throws Exception { + Edge edge = doPost("/api/edge", constructEdge("Test Edge", "test"), Edge.class); + + Device device = new Device(); + device.setName("Edge Device 1"); + device.setType("test"); + Device savedDevice = doPost("/api/device", device, Device.class); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/device/" + savedDevice.getId().getId().toString(), Device.class); + + Asset asset = new Asset(); + asset.setName("Edge Asset 1"); + asset.setType("test"); + Asset savedAsset = doPost("/api/asset", asset, Asset.class); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); + + EdgeImitator edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); + edgeImitator.ignoreType(UserCredentialsUpdateMsg.class); + edgeImitator.expectMessageAmount(7); + edgeImitator.connect(); + Assert.assertTrue(edgeImitator.waitForMessages()); + + Assert.assertEquals(7, edgeImitator.getDownlinkMsgs().size()); + Assert.assertTrue(edgeImitator.findMessageByType(RuleChainUpdateMsg.class).isPresent()); + Assert.assertTrue(edgeImitator.findMessageByType(DeviceProfileUpdateMsg.class).isPresent()); + Assert.assertTrue(edgeImitator.findMessageByType(DeviceUpdateMsg.class).isPresent()); + Assert.assertTrue(edgeImitator.findMessageByType(AssetUpdateMsg.class).isPresent()); + Assert.assertTrue(edgeImitator.findMessageByType(UserUpdateMsg.class).isPresent()); + + edgeImitator.getDownlinkMsgs().clear(); + + edgeImitator.expectMessageAmount(7); + doPost("/api/edge/sync/" + edge.getId()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + Assert.assertEquals(7, edgeImitator.getDownlinkMsgs().size()); + Assert.assertTrue(edgeImitator.findMessageByType(RuleChainUpdateMsg.class).isPresent()); + Assert.assertTrue(edgeImitator.findMessageByType(DeviceProfileUpdateMsg.class).isPresent()); + Assert.assertTrue(edgeImitator.findMessageByType(DeviceUpdateMsg.class).isPresent()); + Assert.assertTrue(edgeImitator.findMessageByType(AssetUpdateMsg.class).isPresent()); + Assert.assertTrue(edgeImitator.findMessageByType(UserUpdateMsg.class).isPresent()); + + edgeImitator.allowIgnoredTypes(); + try { + edgeImitator.disconnect(); + } catch (Exception ignored) {} + + doDelete("/api/device/" + savedDevice.getId().getId().toString()) + .andExpect(status().isOk()); + doDelete("/api/asset/" + savedAsset.getId().getId().toString()) + .andExpect(status().isOk()); + doDelete("/api/edge/" + edge.getId().getId().toString()) + .andExpect(status().isOk()); + } + +} \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeEventControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeEventControllerTest.java new file mode 100644 index 0000000000..c07a499f83 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeEventControllerTest.java @@ -0,0 +1,128 @@ +/** + * Copyright © 2016-2021 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 lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.security.Authority; + +import java.util.List; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +public class BaseEdgeEventControllerTest extends AbstractControllerTest { + + private Tenant savedTenant; + private User tenantAdmin; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + // sleep 1 seconds to avoid CREDENTIALS updated message for the user + // user credentials is going to be stored and updated event pushed to edge notification service + // while service will be processing this event edge could be already added and additional message will be pushed + Thread.sleep(1000); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testGetEdgeEvents() throws Exception { + Edge edge = constructEdge("TestEdge", "default"); + edge = doPost("/api/edge", edge, Edge.class); + + Device device = constructDevice("TestDevice", "default"); + Device savedDevice = doPost("/api/device", device, Device.class); + + doPost("/api/edge/" + edge.getId().toString() + "/device/" + savedDevice.getId().toString(), Device.class); + + Asset asset = constructAsset("TestAsset", "default"); + Asset savedAsset = doPost("/api/asset", asset, Asset.class); + + doPost("/api/edge/" + edge.getId().toString() + "/asset/" + savedAsset.getId().toString(), Asset.class); + + EntityRelation relation = new EntityRelation(savedAsset.getId(), savedDevice.getId(), EntityRelation.CONTAINS_TYPE); + + doPost("/api/relation", relation); + + // wait while edge event for the relation entity persisted to DB + Thread.sleep(100); + List edgeEvents; + int attempt = 1; + do { + edgeEvents = doGetTypedWithTimePageLink("/api/edge/" + edge.getId().toString() + "/events?", + new TypeReference>() {}, new TimePageLink(4)).getData(); + attempt++; + Thread.sleep(100); + } while (edgeEvents.size() != 4 && attempt < 5); + Assert.assertEquals(4, edgeEvents.size()); + Assert.assertTrue(edgeEvents.stream().anyMatch(ee -> EdgeEventType.RULE_CHAIN.equals(ee.getType()))); + Assert.assertTrue(edgeEvents.stream().anyMatch(ee -> EdgeEventType.DEVICE.equals(ee.getType()))); + Assert.assertTrue(edgeEvents.stream().anyMatch(ee -> EdgeEventType.ASSET.equals(ee.getType()))); + Assert.assertTrue(edgeEvents.stream().anyMatch(ee -> EdgeEventType.RELATION.equals(ee.getType()))); + } + + private Device constructDevice(String name, String type) { + Device device = new Device(); + device.setName(name); + device.setType(type); + return device; + } + + private Asset constructAsset(String name, String type) { + Asset asset = new Asset(); + asset.setName(name); + asset.setType(type); + return asset; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java index 5d234d2691..b2fe4a563b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java @@ -16,16 +16,10 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import org.apache.http.conn.ssl.TrustStrategy; -import org.apache.http.ssl.SSLContextBuilder; -import org.apache.http.ssl.SSLContexts; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.springframework.boot.web.server.LocalServerPort; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; @@ -33,7 +27,6 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.EntityCountQuery; @@ -49,16 +42,10 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd; -import java.net.URI; -import java.net.URISyntaxException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Random; import java.util.stream.Collectors; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java index c6d0073d83..e651caa2ff 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java @@ -27,7 +27,13 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.thingsboard.server.common.data.*; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.EntityViewInfo; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.objects.AttributesEntityView; import org.thingsboard.server.common.data.objects.TelemetryEntityView; @@ -49,7 +55,6 @@ import java.util.concurrent.TimeUnit; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @@ -568,4 +573,31 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes return loadedItems; } + + @Test + public void testAssignEntityViewToEdge() throws Exception { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = doPost("/api/edge", edge, Edge.class); + + EntityView savedEntityView = getNewSavedEntityView("My entityView"); + + doPost("/api/edge/" + savedEdge.getId().getId().toString() + + "/device/" + testDevice.getId().getId().toString(), Device.class); + + doPost("/api/edge/" + savedEdge.getId().getId().toString() + + "/entityView/" + savedEntityView.getId().getId().toString(), EntityView.class); + + PageData pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId().toString() + "/entityViews?", + new TypeReference>() {}, new PageLink(100)); + + Assert.assertEquals(1, pageData.getData().size()); + + doDelete("/api/edge/" + savedEdge.getId().getId().toString() + + "/entityView/" + savedEntityView.getId().getId().toString(), EntityView.class); + + pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId().toString() + "/entityViews?", + new TypeReference>() {}, new PageLink(100)); + + Assert.assertEquals(0, pageData.getData().size()); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java index 8fae23881a..8850b70f86 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java @@ -32,7 +32,6 @@ import org.thingsboard.server.common.data.security.Authority; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -110,7 +109,7 @@ public abstract class BaseTbResourceControllerTest extends AbstractControllerTes TbResource savedResource = save(resource); loginDifferentTenant(); - doPostWithTypedResponse("/api/resource", Collections.singletonList(savedResource), new TypeReference<>(){}, status().isBadRequest()); + doPostWithTypedResponse("/api/resource", savedResource, new TypeReference<>(){}, status().isForbidden()); deleteDifferentTenant(); } @@ -148,18 +147,15 @@ public abstract class BaseTbResourceControllerTest extends AbstractControllerTes @Test public void testFindTenantTbResources() throws Exception { - List resourcesToSave = new ArrayList<>(); + List resources = new ArrayList<>(); for (int i = 0; i < 173; i++) { TbResource resource = new TbResource(); resource.setTitle("Resource" + i); resource.setResourceType(ResourceType.JKS); resource.setFileName(i + DEFAULT_FILE_NAME); resource.setData("Test Data"); - resourcesToSave.add(resource); + resources.add(new TbResourceInfo(save(resource))); } - - List resources =save(resourcesToSave).stream().map(TbResourceInfo::new).collect(Collectors.toList()); - List loadedResources = new ArrayList<>(); PageLink pageLink = new PageLink(24); PageData pageData; @@ -287,10 +283,6 @@ public abstract class BaseTbResourceControllerTest extends AbstractControllerTes } private TbResource save(TbResource tbResource) throws Exception { - return save(Collections.singletonList(tbResource)).get(0); - } - - private List save(List tbResources) throws Exception { - return doPostWithTypedResponse("/api/resource", tbResources, new TypeReference<>(){}); + return doPostWithTypedResponse("/api/resource", tbResource, new TypeReference<>(){}); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java index 0f969a848f..653f6978f6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java @@ -28,6 +28,7 @@ import java.util.Arrays; @ClasspathSuite.ClassnameFilters({ // "org.thingsboard.server.controller.sql.WebsocketApiSqlTest", // "org.thingsboard.server.controller.sql.EntityQueryControllerSqlTest", +// "org.thingsboard.server.controller.sql.TbResourceControllerSqlTest", "org.thingsboard.server.controller.sql.*Test", }) public class ControllerSqlTestSuite { diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/EdgeControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/EdgeControllerSqlTest.java new file mode 100644 index 0000000000..0d1fa48f7e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/EdgeControllerSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2021 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.sql; + +import org.thingsboard.server.controller.BaseEdgeControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class EdgeControllerSqlTest extends BaseEdgeControllerTest { +} diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/EdgeEventControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/EdgeEventControllerSqlTest.java new file mode 100644 index 0000000000..b38e00982f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/EdgeEventControllerSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2021 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.sql; + +import org.thingsboard.server.controller.BaseEdgeEventControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class EdgeEventControllerSqlTest extends BaseEdgeEventControllerTest { +} diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java new file mode 100644 index 0000000000..92548bb621 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java @@ -0,0 +1,1593 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.gson.JsonObject; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.MessageLite; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.After; +import org.junit.Assert; +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.Customer; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.device.profile.AlarmCondition; +import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; +import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; +import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; +import org.thingsboard.server.common.data.device.profile.AlarmRule; +import org.thingsboard.server.common.data.device.profile.AllowCreateNewDevicesDeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.NumericFilterPredicate; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.rule.RuleNode; +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.common.data.widget.WidgetType; +import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.edge.EdgeEventService; +import org.thingsboard.server.edge.imitator.EdgeImitator; +import org.thingsboard.server.gen.edge.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.AttributeDeleteMsg; +import org.thingsboard.server.gen.edge.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.CustomerUpdateMsg; +import org.thingsboard.server.gen.edge.DashboardUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceProfileUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceRpcCallMsg; +import org.thingsboard.server.gen.edge.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.EdgeConfiguration; +import org.thingsboard.server.gen.edge.EntityDataProto; +import org.thingsboard.server.gen.edge.EntityViewUpdateMsg; +import org.thingsboard.server.gen.edge.RelationRequestMsg; +import org.thingsboard.server.gen.edge.RelationUpdateMsg; +import org.thingsboard.server.gen.edge.RpcResponseMsg; +import org.thingsboard.server.gen.edge.RuleChainMetadataRequestMsg; +import org.thingsboard.server.gen.edge.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.UpdateMsgType; +import org.thingsboard.server.gen.edge.UplinkMsg; +import org.thingsboard.server.gen.edge.UserCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.WidgetTypeUpdateMsg; +import org.thingsboard.server.gen.edge.WidgetsBundleUpdateMsg; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.service.queue.TbClusterService; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +abstract public class BaseEdgeTest extends AbstractControllerTest { + + private static final String CUSTOM_DEVICE_PROFILE_NAME = "Thermostat"; + + private Tenant savedTenant; + private TenantId tenantId; + private User tenantAdmin; + + private EdgeImitator edgeImitator; + private Edge edge; + + @Autowired + private EdgeEventService edgeEventService; + + @Autowired + private TbClusterService clusterService; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + tenantId = savedTenant.getId(); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + // sleep 1 seconds to avoid CREDENTIALS updated message for the user + // user credentials is going to be stored and updated event pushed to edge notification service + // while service will be processing this event edge could be already added and additional message will be pushed + Thread.sleep(1000); + + installation(); + + edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); + // TODO: voba - should be less, but events from SyncEdgeService stack with events from controller. will be fixed in next releases + // so ideally sync process should check current edge queue and add only missing entities to the edge queue + edgeImitator.expectMessageAmount(10); + edgeImitator.connect(); + } + + @After + public void afterTest() throws Exception { + try { + edgeImitator.disconnect(); + } catch (Exception ignored) {} + + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void test() throws Exception { + testReceivedInitialData(); + int expectedDownlinkSize = 10; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + + testDevices(); + expectedDownlinkSize = expectedDownlinkSize + 4; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + + testAssets(); + expectedDownlinkSize = expectedDownlinkSize + 4; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + + testRuleChains(); + expectedDownlinkSize = expectedDownlinkSize + 3; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + + testDashboards(); + expectedDownlinkSize = expectedDownlinkSize + 3; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + + testRelations(); + expectedDownlinkSize = expectedDownlinkSize + 2; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + + testAlarms(); + expectedDownlinkSize = expectedDownlinkSize + 3; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + + testEntityView(); + expectedDownlinkSize = expectedDownlinkSize + 2; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + + testCustomer(); + expectedDownlinkSize = expectedDownlinkSize + 2; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + + testWidgetsBundleAndWidgetType(); + expectedDownlinkSize = expectedDownlinkSize + 4; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + + testTimeseries(); + expectedDownlinkSize = expectedDownlinkSize + 1; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + + testAttributes(); + expectedDownlinkSize = expectedDownlinkSize + 3; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + + testSendMessagesToCloud(); + expectedDownlinkSize = expectedDownlinkSize + 9; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + + testRpcCall(); + expectedDownlinkSize = expectedDownlinkSize + 1; + Assert.assertEquals(expectedDownlinkSize, edgeImitator.getDownlinkMsgs().size()); + } + + private Device findDeviceByName(String deviceName) throws Exception { + List edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getId().getId().toString() + "/devices?", + new TypeReference>() { + }, new PageLink(100)).getData(); + Optional foundDevice = edgeDevices.stream().filter(d -> d.getName().equals(deviceName)).findAny(); + Assert.assertTrue(foundDevice.isPresent()); + Device device = foundDevice.get(); + Assert.assertEquals(deviceName, device.getName()); + return device; + } + + private Asset findAssetByName(String assetName) throws Exception { + List edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getId().getId().toString() + "/assets?", + new TypeReference>() { + }, new PageLink(100)).getData(); + + Assert.assertEquals(1, edgeAssets.size()); + Asset asset = edgeAssets.get(0); + Assert.assertEquals(assetName, asset.getName()); + return asset; + } + + private Device saveDevice(String deviceName, String type) throws Exception { + Device device = new Device(); + device.setName(deviceName); + device.setType(type); + return doPost("/api/device", device, Device.class); + } + + private Asset saveAsset(String assetName) throws Exception { + Asset asset = new Asset(); + asset.setName(assetName); + asset.setType("test"); + return doPost("/api/asset", asset, Asset.class); + } + + private void testRpcCall() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + + ObjectNode body = mapper.createObjectNode(); + body.put("requestId", new Random().nextInt()); + body.put("requestUUID", Uuids.timeBased().toString()); + body.put("oneway", false); + body.put("expirationTime", System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(10)); + body.put("method", "test_method"); + body.put("params", "{\"param1\":\"value1\"}"); + + EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.RPC_CALL, device.getId().getId(), EdgeEventType.DEVICE, body); + edgeImitator.expectMessageAmount(1); + edgeEventService.saveAsync(edgeEvent); + clusterService.onEdgeEventUpdate(tenantId, edge.getId()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceRpcCallMsg); + DeviceRpcCallMsg latestDeviceRpcCallMsg = (DeviceRpcCallMsg) latestMessage; + Assert.assertEquals("test_method", latestDeviceRpcCallMsg.getRequestMsg().getMethod()); + } + + private void testReceivedInitialData() throws Exception { + log.info("Checking received data"); + Assert.assertTrue(edgeImitator.waitForMessages()); + + EdgeConfiguration configuration = edgeImitator.getConfiguration(); + Assert.assertNotNull(configuration); + + testAutoGeneratedCodeByProtobuf(configuration); + + UserId userId = edgeImitator.getUserId(); + Assert.assertNotNull(userId); + + Optional deviceUpdateMsgOpt = edgeImitator.findMessageByType(DeviceUpdateMsg.class); + Assert.assertTrue(deviceUpdateMsgOpt.isPresent()); + DeviceUpdateMsg deviceUpdateMsg = deviceUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); + UUID deviceUUID = new UUID(deviceUpdateMsg.getIdMSB(), deviceUpdateMsg.getIdLSB()); + Device device = doGet("/api/device/" + deviceUUID.toString(), Device.class); + Assert.assertNotNull(device); + List edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getId().getId().toString() + "/devices?", + new TypeReference>() {}, new PageLink(100)).getData(); + Assert.assertTrue(edgeDevices.contains(device)); + + List deviceProfileUpdateMsgList = edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class); + Assert.assertEquals(3, deviceProfileUpdateMsgList.size()); + Optional deviceProfileUpdateMsgOpt = + deviceProfileUpdateMsgList.stream().filter(dfum -> CUSTOM_DEVICE_PROFILE_NAME.equals(dfum.getName())).findAny(); + Assert.assertTrue(deviceProfileUpdateMsgOpt.isPresent()); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = deviceProfileUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); + UUID deviceProfileUUID = new UUID(deviceProfileUpdateMsg.getIdMSB(), deviceProfileUpdateMsg.getIdLSB()); + DeviceProfile deviceProfile = doGet("/api/deviceProfile/" + deviceProfileUUID.toString(), DeviceProfile.class); + Assert.assertNotNull(deviceProfile); + Assert.assertNotNull(deviceProfile.getProfileData()); + Assert.assertNotNull(deviceProfile.getProfileData().getAlarms()); + Assert.assertNotNull(deviceProfile.getProfileData().getAlarms().get(0).getClearRule()); + + testAutoGeneratedCodeByProtobuf(deviceProfileUpdateMsg); + + Optional assetUpdateMsgOpt = edgeImitator.findMessageByType(AssetUpdateMsg.class); + Assert.assertTrue(assetUpdateMsgOpt.isPresent()); + AssetUpdateMsg assetUpdateMsg = assetUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); + UUID assetUUID = new UUID(assetUpdateMsg.getIdMSB(), assetUpdateMsg.getIdLSB()); + Asset asset = doGet("/api/asset/" + assetUUID.toString(), Asset.class); + Assert.assertNotNull(asset); + List edgeAssets = doGetTypedWithPageLink("/api/edge/" + edge.getId().getId().toString() + "/assets?", + new TypeReference>() {}, new PageLink(100)).getData(); + Assert.assertTrue(edgeAssets.contains(asset)); + + testAutoGeneratedCodeByProtobuf(assetUpdateMsg); + + Optional ruleChainUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainUpdateMsg.class); + Assert.assertTrue(ruleChainUpdateMsgOpt.isPresent()); + RuleChainUpdateMsg ruleChainUpdateMsg = ruleChainUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, ruleChainUpdateMsg.getMsgType()); + UUID ruleChainUUID = new UUID(ruleChainUpdateMsg.getIdMSB(), ruleChainUpdateMsg.getIdLSB()); + RuleChain ruleChain = doGet("/api/ruleChain/" + ruleChainUUID.toString(), RuleChain.class); + Assert.assertNotNull(ruleChain); + List edgeRuleChains = doGetTypedWithPageLink("/api/edge/" + edge.getId().getId().toString() + "/ruleChains?", + new TypeReference>() {}, new PageLink(100)).getData(); + Assert.assertTrue(edgeRuleChains.contains(ruleChain)); + + testAutoGeneratedCodeByProtobuf(ruleChainUpdateMsg); + + log.info("Received data checked"); + } + + private void testDevices() throws Exception { + log.info("Testing devices"); + + // 1 + edgeImitator.expectMessageAmount(1); + Device savedDevice = saveDevice("Edge Device 2", "Default"); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/device/" + savedDevice.getId().getId().toString(), Device.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); + DeviceUpdateMsg deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); + Assert.assertEquals(deviceUpdateMsg.getIdMSB(), savedDevice.getUuidId().getMostSignificantBits()); + Assert.assertEquals(deviceUpdateMsg.getIdLSB(), savedDevice.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(deviceUpdateMsg.getName(), savedDevice.getName()); + Assert.assertEquals(deviceUpdateMsg.getType(), savedDevice.getType()); + + // 2 + edgeImitator.expectMessageAmount(1); + doDelete("/api/edge/" + edge.getId().getId().toString() + + "/device/" + savedDevice.getId().getId().toString(), Device.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); + deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); + Assert.assertEquals(deviceUpdateMsg.getIdMSB(), savedDevice.getUuidId().getMostSignificantBits()); + Assert.assertEquals(deviceUpdateMsg.getIdLSB(), savedDevice.getUuidId().getLeastSignificantBits()); + + // 3 + edgeImitator.expectMessageAmount(1); + doDelete("/api/device/" + savedDevice.getId().getId().toString()) + .andExpect(status().isOk()); + // we should not get any message because device is not assigned to edge any more + Assert.assertFalse(edgeImitator.waitForMessages(1)); + + // 4 + edgeImitator.expectMessageAmount(1); + savedDevice = saveDevice("Edge Device 3", "Default"); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/device/" + savedDevice.getId().getId().toString(), Device.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); + deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); + Assert.assertEquals(deviceUpdateMsg.getIdMSB(), savedDevice.getUuidId().getMostSignificantBits()); + Assert.assertEquals(deviceUpdateMsg.getIdLSB(), savedDevice.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(deviceUpdateMsg.getName(), savedDevice.getName()); + Assert.assertEquals(deviceUpdateMsg.getType(), savedDevice.getType()); + + // 5 + edgeImitator.expectMessageAmount(1); + doDelete("/api/device/" + savedDevice.getId().getId().toString()) + .andExpect(status().isOk()); + // in this case we should get messages because device was assigned to edge + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); + deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); + Assert.assertEquals(deviceUpdateMsg.getIdMSB(), savedDevice.getUuidId().getMostSignificantBits()); + Assert.assertEquals(deviceUpdateMsg.getIdLSB(), savedDevice.getUuidId().getLeastSignificantBits()); + + log.info("Devices tested successfully"); + } + + + private void testAssets() throws Exception { + log.info("Testing assets"); + + // 1 + edgeImitator.expectMessageAmount(1); + Asset savedAsset = saveAsset("Edge Asset 2"); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AssetUpdateMsg); + AssetUpdateMsg assetUpdateMsg = (AssetUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); + Assert.assertEquals(assetUpdateMsg.getIdMSB(), savedAsset.getUuidId().getMostSignificantBits()); + Assert.assertEquals(assetUpdateMsg.getIdLSB(), savedAsset.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(assetUpdateMsg.getName(), savedAsset.getName()); + Assert.assertEquals(assetUpdateMsg.getType(), savedAsset.getType()); + + // 2 + edgeImitator.expectMessageAmount(1); + doDelete("/api/edge/" + edge.getId().getId().toString() + + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AssetUpdateMsg); + assetUpdateMsg = (AssetUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); + Assert.assertEquals(assetUpdateMsg.getIdMSB(), savedAsset.getUuidId().getMostSignificantBits()); + Assert.assertEquals(assetUpdateMsg.getIdLSB(), savedAsset.getUuidId().getLeastSignificantBits()); + + // 3 + edgeImitator.expectMessageAmount(1); + doDelete("/api/asset/" + savedAsset.getId().getId().toString()) + .andExpect(status().isOk()); + Assert.assertFalse(edgeImitator.waitForMessages(1)); + + // 4 + edgeImitator.expectMessageAmount(1); + savedAsset = saveAsset("Edge Asset 3"); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AssetUpdateMsg); + assetUpdateMsg = (AssetUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); + Assert.assertEquals(assetUpdateMsg.getIdMSB(), savedAsset.getUuidId().getMostSignificantBits()); + Assert.assertEquals(assetUpdateMsg.getIdLSB(), savedAsset.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(assetUpdateMsg.getName(), savedAsset.getName()); + Assert.assertEquals(assetUpdateMsg.getType(), savedAsset.getType()); + + // 5 + edgeImitator.expectMessageAmount(1); + doDelete("/api/asset/" + savedAsset.getId().getId().toString()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AssetUpdateMsg); + assetUpdateMsg = (AssetUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, assetUpdateMsg.getMsgType()); + Assert.assertEquals(assetUpdateMsg.getIdMSB(), savedAsset.getUuidId().getMostSignificantBits()); + Assert.assertEquals(assetUpdateMsg.getIdLSB(), savedAsset.getUuidId().getLeastSignificantBits()); + + log.info("Assets tested successfully"); + } + + private void testRuleChains() throws Exception { + log.info("Testing RuleChains"); + + // 1 + edgeImitator.expectMessageAmount(1); + RuleChain ruleChain = new RuleChain(); + ruleChain.setName("Edge Test Rule Chain"); + ruleChain.setType(RuleChainType.EDGE); + RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + createRuleChainMetadata(savedRuleChain); + // sleep 1 seconds to avoid ENTITY_UPDATED_RPC_MESSAGE for the rule chain + // rule chain metadata is going to be stored and updated event pushed to edge notification service + // while service will be processing this event assignment rule chain to edge will be completed if bad timing + Thread.sleep(1000); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/ruleChain/" + savedRuleChain.getId().getId().toString(), RuleChain.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof RuleChainUpdateMsg); + RuleChainUpdateMsg ruleChainUpdateMsg = (RuleChainUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainUpdateMsg.getMsgType()); + Assert.assertEquals(ruleChainUpdateMsg.getIdMSB(), savedRuleChain.getUuidId().getMostSignificantBits()); + Assert.assertEquals(ruleChainUpdateMsg.getIdLSB(), savedRuleChain.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(ruleChainUpdateMsg.getName(), savedRuleChain.getName()); + + // 2 + testRuleChainMetadataRequestMsg(savedRuleChain.getId()); + + // 3 + edgeImitator.expectMessageAmount(1); + doDelete("/api/edge/" + edge.getId().getId().toString() + + "/ruleChain/" + savedRuleChain.getId().getId().toString(), RuleChain.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof RuleChainUpdateMsg); + ruleChainUpdateMsg = (RuleChainUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, ruleChainUpdateMsg.getMsgType()); + Assert.assertEquals(ruleChainUpdateMsg.getIdMSB(), savedRuleChain.getUuidId().getMostSignificantBits()); + Assert.assertEquals(ruleChainUpdateMsg.getIdLSB(), savedRuleChain.getUuidId().getLeastSignificantBits()); + + // 4 + edgeImitator.expectMessageAmount(1); + doDelete("/api/ruleChain/" + savedRuleChain.getId().getId().toString()) + .andExpect(status().isOk()); + Assert.assertFalse(edgeImitator.waitForMessages(1)); + + log.info("RuleChains tested successfully"); + } + + private void testRuleChainMetadataRequestMsg(RuleChainId ruleChainId) throws Exception { + RuleChainMetadataRequestMsg.Builder ruleChainMetadataRequestMsgBuilder = RuleChainMetadataRequestMsg.newBuilder() + .setRuleChainIdMSB(ruleChainId.getId().getMostSignificantBits()) + .setRuleChainIdLSB(ruleChainId.getId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(ruleChainMetadataRequestMsgBuilder); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder() + .addRuleChainMetadataRequestMsg(ruleChainMetadataRequestMsgBuilder.build()); + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof RuleChainMetadataUpdateMsg); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = (RuleChainMetadataUpdateMsg) latestMessage; + RuleChainId receivedRuleChainId = + new RuleChainId(new UUID(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), ruleChainMetadataUpdateMsg.getRuleChainIdLSB())); + Assert.assertEquals(ruleChainId, receivedRuleChainId); + } + + private void createRuleChainMetadata(RuleChain ruleChain) throws Exception { + RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); + ruleChainMetaData.setRuleChainId(ruleChain.getId()); + + ObjectMapper mapper = new ObjectMapper(); + + RuleNode ruleNode1 = new RuleNode(); + ruleNode1.setName("name1"); + ruleNode1.setType("type1"); + ruleNode1.setConfiguration(mapper.readTree("\"key1\": \"val1\"")); + + RuleNode ruleNode2 = new RuleNode(); + ruleNode2.setName("name2"); + ruleNode2.setType("type2"); + ruleNode2.setConfiguration(mapper.readTree("\"key2\": \"val2\"")); + + RuleNode ruleNode3 = new RuleNode(); + ruleNode3.setName("name3"); + ruleNode3.setType("type3"); + ruleNode3.setConfiguration(mapper.readTree("\"key3\": \"val3\"")); + + List ruleNodes = new ArrayList<>(); + ruleNodes.add(ruleNode1); + ruleNodes.add(ruleNode2); + ruleNodes.add(ruleNode3); + ruleChainMetaData.setFirstNodeIndex(0); + ruleChainMetaData.setNodes(ruleNodes); + + ruleChainMetaData.addConnectionInfo(0, 1, "success"); + ruleChainMetaData.addConnectionInfo(0, 2, "fail"); + ruleChainMetaData.addConnectionInfo(1, 2, "success"); + + ruleChainMetaData.addRuleChainConnectionInfo(2, edge.getRootRuleChainId(), "success", mapper.createObjectNode()); + + doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); + } + + private void testDashboards() throws Exception { + log.info("Testing Dashboards"); + + // 1 + edgeImitator.expectMessageAmount(1); + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("Edge Test Dashboard"); + Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg); + DashboardUpdateMsg dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, dashboardUpdateMsg.getMsgType()); + Assert.assertEquals(dashboardUpdateMsg.getIdMSB(), savedDashboard.getUuidId().getMostSignificantBits()); + Assert.assertEquals(dashboardUpdateMsg.getIdLSB(), savedDashboard.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(dashboardUpdateMsg.getTitle(), savedDashboard.getName()); + testAutoGeneratedCodeByProtobuf(dashboardUpdateMsg); + + // 2 + edgeImitator.expectMessageAmount(1); + savedDashboard.setTitle("Updated Edge Test Dashboard"); + doPost("/api/dashboard", savedDashboard, Dashboard.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg); + dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, dashboardUpdateMsg.getMsgType()); + Assert.assertEquals(dashboardUpdateMsg.getTitle(), savedDashboard.getName()); + + // 3 + edgeImitator.expectMessageAmount(1); + doDelete("/api/edge/" + edge.getId().getId().toString() + + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DashboardUpdateMsg); + dashboardUpdateMsg = (DashboardUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, dashboardUpdateMsg.getMsgType()); + Assert.assertEquals(dashboardUpdateMsg.getIdMSB(), savedDashboard.getUuidId().getMostSignificantBits()); + Assert.assertEquals(dashboardUpdateMsg.getIdLSB(), savedDashboard.getUuidId().getLeastSignificantBits()); + + // 4 + edgeImitator.expectMessageAmount(1); + doDelete("/api/dashboard/" + savedDashboard.getId().getId().toString()) + .andExpect(status().isOk()); + Assert.assertFalse(edgeImitator.waitForMessages(1)); + + log.info("Dashboards tested successfully"); + } + + private void testRelations() throws Exception { + log.info("Testing Relations"); + + // 1 + edgeImitator.expectMessageAmount(1); + Device device = findDeviceByName("Edge Device 1"); + Asset asset = findAssetByName("Edge Asset 1"); + EntityRelation relation = new EntityRelation(); + relation.setType("test"); + relation.setFrom(device.getId()); + relation.setTo(asset.getId()); + relation.setTypeGroup(RelationTypeGroup.COMMON); + doPost("/api/relation", relation); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof RelationUpdateMsg); + RelationUpdateMsg relationUpdateMsg = (RelationUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, relationUpdateMsg.getMsgType()); + Assert.assertEquals(relationUpdateMsg.getType(), relation.getType()); + Assert.assertEquals(relationUpdateMsg.getFromIdMSB(), relation.getFrom().getId().getMostSignificantBits()); + Assert.assertEquals(relationUpdateMsg.getFromIdLSB(), relation.getFrom().getId().getLeastSignificantBits()); + Assert.assertEquals(relationUpdateMsg.getToEntityType(), relation.getTo().getEntityType().name()); + Assert.assertEquals(relationUpdateMsg.getFromIdMSB(), relation.getFrom().getId().getMostSignificantBits()); + Assert.assertEquals(relationUpdateMsg.getToIdLSB(), relation.getTo().getId().getLeastSignificantBits()); + Assert.assertEquals(relationUpdateMsg.getToEntityType(), relation.getTo().getEntityType().name()); + Assert.assertEquals(relationUpdateMsg.getTypeGroup(), relation.getTypeGroup().name()); + + // 2 + edgeImitator.expectMessageAmount(1); + doDelete("/api/relation?" + + "fromId=" + relation.getFrom().getId().toString() + + "&fromType=" + relation.getFrom().getEntityType().name() + + "&relationType=" + relation.getType() + + "&relationTypeGroup=" + relation.getTypeGroup().name() + + "&toId=" + relation.getTo().getId().toString() + + "&toType=" + relation.getTo().getEntityType().name()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof RelationUpdateMsg); + relationUpdateMsg = (RelationUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, relationUpdateMsg.getMsgType()); + Assert.assertEquals(relationUpdateMsg.getType(), relation.getType()); + Assert.assertEquals(relationUpdateMsg.getFromIdMSB(), relation.getFrom().getId().getMostSignificantBits()); + Assert.assertEquals(relationUpdateMsg.getFromIdLSB(), relation.getFrom().getId().getLeastSignificantBits()); + Assert.assertEquals(relationUpdateMsg.getToEntityType(), relation.getTo().getEntityType().name()); + Assert.assertEquals(relationUpdateMsg.getFromIdMSB(), relation.getFrom().getId().getMostSignificantBits()); + Assert.assertEquals(relationUpdateMsg.getToIdLSB(), relation.getTo().getId().getLeastSignificantBits()); + Assert.assertEquals(relationUpdateMsg.getToEntityType(), relation.getTo().getEntityType().name()); + Assert.assertEquals(relationUpdateMsg.getTypeGroup(), relation.getTypeGroup().name()); + + log.info("Relations tested successfully"); + } + + private void testAlarms() throws Exception { + log.info("Testing Alarms"); + + // 1 + edgeImitator.expectMessageAmount(1); + Device device = findDeviceByName("Edge Device 1"); + Alarm alarm = new Alarm(); + alarm.setOriginator(device.getId()); + alarm.setStatus(AlarmStatus.ACTIVE_UNACK); + alarm.setType("alarm"); + alarm.setSeverity(AlarmSeverity.CRITICAL); + Alarm savedAlarm = doPost("/api/alarm", alarm, Alarm.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg); + AlarmUpdateMsg alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, alarmUpdateMsg.getMsgType()); + Assert.assertEquals(alarmUpdateMsg.getType(), savedAlarm.getType()); + Assert.assertEquals(alarmUpdateMsg.getName(), savedAlarm.getName()); + Assert.assertEquals(alarmUpdateMsg.getOriginatorName(), device.getName()); + Assert.assertEquals(alarmUpdateMsg.getStatus(), savedAlarm.getStatus().name()); + Assert.assertEquals(alarmUpdateMsg.getSeverity(), savedAlarm.getSeverity().name()); + + // 2 + edgeImitator.expectMessageAmount(1); + doPost("/api/alarm/" + savedAlarm.getId().getId().toString() + "/ack"); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg); + alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ALARM_ACK_RPC_MESSAGE, alarmUpdateMsg.getMsgType()); + Assert.assertEquals(alarmUpdateMsg.getType(), savedAlarm.getType()); + Assert.assertEquals(alarmUpdateMsg.getName(), savedAlarm.getName()); + Assert.assertEquals(alarmUpdateMsg.getOriginatorName(), device.getName()); + Assert.assertEquals(alarmUpdateMsg.getStatus(), AlarmStatus.ACTIVE_ACK.name()); + + // 3 + edgeImitator.expectMessageAmount(1); + doPost("/api/alarm/" + savedAlarm.getId().getId().toString() + "/clear"); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg); + alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ALARM_CLEAR_RPC_MESSAGE, alarmUpdateMsg.getMsgType()); + Assert.assertEquals(alarmUpdateMsg.getType(), savedAlarm.getType()); + Assert.assertEquals(alarmUpdateMsg.getName(), savedAlarm.getName()); + Assert.assertEquals(alarmUpdateMsg.getOriginatorName(), device.getName()); + Assert.assertEquals(alarmUpdateMsg.getStatus(), AlarmStatus.CLEARED_ACK.name()); + + // 4 + edgeImitator.expectMessageAmount(1); + doDelete("/api/alarm/" + savedAlarm.getId().getId().toString()) + .andExpect(status().isOk()); + Assert.assertFalse(edgeImitator.waitForMessages(1)); + + log.info("Alarms tested successfully"); + } + + private void testEntityView() throws Exception { + log.info("Testing EntityView"); + + // 1 + edgeImitator.expectMessageAmount(1); + Device device = findDeviceByName("Edge Device 1"); + EntityView entityView = new EntityView(); + entityView.setName("Edge EntityView 1"); + entityView.setType("test"); + entityView.setEntityId(device.getId()); + EntityView savedEntityView = doPost("/api/entityView", entityView, EntityView.class); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/entityView/" + savedEntityView.getId().getId().toString(), EntityView.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof EntityViewUpdateMsg); + EntityViewUpdateMsg entityViewUpdateMsg = (EntityViewUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType()); + Assert.assertEquals(entityViewUpdateMsg.getType(), savedEntityView.getType()); + Assert.assertEquals(entityViewUpdateMsg.getName(), savedEntityView.getName()); + Assert.assertEquals(entityViewUpdateMsg.getIdMSB(), savedEntityView.getUuidId().getMostSignificantBits()); + Assert.assertEquals(entityViewUpdateMsg.getIdLSB(), savedEntityView.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(entityViewUpdateMsg.getEntityIdMSB(), device.getUuidId().getMostSignificantBits()); + Assert.assertEquals(entityViewUpdateMsg.getEntityIdLSB(), device.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(entityViewUpdateMsg.getEntityType().name(), device.getId().getEntityType().name()); + + // 2 + edgeImitator.expectMessageAmount(1); + doDelete("/api/edge/" + edge.getId().getId().toString() + + "/entityView/" + savedEntityView.getId().getId().toString(), EntityView.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof EntityViewUpdateMsg); + entityViewUpdateMsg = (EntityViewUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, entityViewUpdateMsg.getMsgType()); + Assert.assertEquals(entityViewUpdateMsg.getIdMSB(), savedEntityView.getUuidId().getMostSignificantBits()); + Assert.assertEquals(entityViewUpdateMsg.getIdLSB(), savedEntityView.getUuidId().getLeastSignificantBits()); + + // 3 + edgeImitator.expectMessageAmount(1); + doDelete("/api/entityView/" + savedEntityView.getId().getId().toString()) + .andExpect(status().isOk()); + Assert.assertFalse(edgeImitator.waitForMessages(1)); + + log.info("EntityView tested successfully"); + } + + private void testCustomer() throws Exception { + log.info("Testing Customer"); + + // 1 + edgeImitator.expectMessageAmount(1); + Customer customer = new Customer(); + customer.setTitle("Edge Customer 1"); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + doPost("/api/customer/" + savedCustomer.getId().getId().toString() + + "/edge/" + edge.getId().getId().toString(), Edge.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof CustomerUpdateMsg); + CustomerUpdateMsg customerUpdateMsg = (CustomerUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, customerUpdateMsg.getMsgType()); + Assert.assertEquals(customerUpdateMsg.getIdMSB(), savedCustomer.getUuidId().getMostSignificantBits()); + Assert.assertEquals(customerUpdateMsg.getIdLSB(), savedCustomer.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(customerUpdateMsg.getTitle(), savedCustomer.getTitle()); + testAutoGeneratedCodeByProtobuf(customerUpdateMsg); + + // 2 + edgeImitator.expectMessageAmount(1); + doDelete("/api/customer/edge/" + edge.getId().getId().toString(), Edge.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof CustomerUpdateMsg); + customerUpdateMsg = (CustomerUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, customerUpdateMsg.getMsgType()); + Assert.assertEquals(customerUpdateMsg.getIdMSB(), savedCustomer.getUuidId().getMostSignificantBits()); + Assert.assertEquals(customerUpdateMsg.getIdLSB(), savedCustomer.getUuidId().getLeastSignificantBits()); + + // 3 + edgeImitator.expectMessageAmount(1); + doDelete("/api/customer/" + savedCustomer.getId().getId().toString()) + .andExpect(status().isOk()); + Assert.assertFalse(edgeImitator.waitForMessages(1)); + + log.info("Customer tested successfully"); + } + + private void testWidgetsBundleAndWidgetType() throws Exception { + log.info("Testing WidgetsBundle and WidgetType"); + + // 1 + edgeImitator.expectMessageAmount(1); + WidgetsBundle widgetsBundle = new WidgetsBundle(); + widgetsBundle.setTitle("Test Widget Bundle"); + WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof WidgetsBundleUpdateMsg); + WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = (WidgetsBundleUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, widgetsBundleUpdateMsg.getMsgType()); + Assert.assertEquals(widgetsBundleUpdateMsg.getIdMSB(), savedWidgetsBundle.getUuidId().getMostSignificantBits()); + Assert.assertEquals(widgetsBundleUpdateMsg.getIdLSB(), savedWidgetsBundle.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(widgetsBundleUpdateMsg.getAlias(), savedWidgetsBundle.getAlias()); + Assert.assertEquals(widgetsBundleUpdateMsg.getTitle(), savedWidgetsBundle.getTitle()); + testAutoGeneratedCodeByProtobuf(widgetsBundleUpdateMsg); + + // 2 + edgeImitator.expectMessageAmount(1); + WidgetType widgetType = new WidgetType(); + widgetType.setName("Test Widget Type"); + widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); + ObjectNode descriptor = mapper.createObjectNode(); + descriptor.put("key", "value"); + widgetType.setDescriptor(descriptor); + WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof WidgetTypeUpdateMsg); + WidgetTypeUpdateMsg widgetTypeUpdateMsg = (WidgetTypeUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, widgetTypeUpdateMsg.getMsgType()); + Assert.assertEquals(widgetTypeUpdateMsg.getIdMSB(), savedWidgetType.getUuidId().getMostSignificantBits()); + Assert.assertEquals(widgetTypeUpdateMsg.getIdLSB(), savedWidgetType.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(widgetTypeUpdateMsg.getAlias(), savedWidgetType.getAlias()); + Assert.assertEquals(widgetTypeUpdateMsg.getName(), savedWidgetType.getName()); + Assert.assertEquals(JacksonUtil.toJsonNode(widgetTypeUpdateMsg.getDescriptorJson()), savedWidgetType.getDescriptor()); + + // 3 + edgeImitator.expectMessageAmount(1); + doDelete("/api/widgetType/" + savedWidgetType.getId().getId().toString()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof WidgetTypeUpdateMsg); + widgetTypeUpdateMsg = (WidgetTypeUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, widgetTypeUpdateMsg.getMsgType()); + Assert.assertEquals(widgetTypeUpdateMsg.getIdMSB(), savedWidgetType.getUuidId().getMostSignificantBits()); + Assert.assertEquals(widgetTypeUpdateMsg.getIdLSB(), savedWidgetType.getUuidId().getLeastSignificantBits()); + + // 4 + edgeImitator.expectMessageAmount(1); + doDelete("/api/widgetsBundle/" + savedWidgetsBundle.getId().getId().toString()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof WidgetsBundleUpdateMsg); + widgetsBundleUpdateMsg = (WidgetsBundleUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, widgetsBundleUpdateMsg.getMsgType()); + Assert.assertEquals(widgetsBundleUpdateMsg.getIdMSB(), savedWidgetsBundle.getUuidId().getMostSignificantBits()); + Assert.assertEquals(widgetsBundleUpdateMsg.getIdLSB(), savedWidgetsBundle.getUuidId().getLeastSignificantBits()); + + log.info("WidgetsBundle and WidgetType tested successfully"); + } + + private void testTimeseries() throws Exception { + log.info("Testing timeseries"); + + edgeImitator.expectMessageAmount(1); + Device device = findDeviceByName("Edge Device 1"); + String timeseriesData = "{\"data\":{\"temperature\":25},\"ts\":" + System.currentTimeMillis() + "}"; + JsonNode timeseriesEntityData = mapper.readTree(timeseriesData); + EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.TIMESERIES_UPDATED, device.getId().getId(), EdgeEventType.DEVICE, timeseriesEntityData); + edgeEventService.saveAsync(edgeEvent); + clusterService.onEdgeEventUpdate(tenantId, edge.getId()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof EntityDataProto); + EntityDataProto latestEntityDataMsg = (EntityDataProto) latestMessage; + Assert.assertEquals(latestEntityDataMsg.getEntityIdMSB(), device.getUuidId().getMostSignificantBits()); + Assert.assertEquals(latestEntityDataMsg.getEntityIdLSB(), device.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(latestEntityDataMsg.getEntityType(), device.getId().getEntityType().name()); + Assert.assertTrue(latestEntityDataMsg.hasPostTelemetryMsg()); + + TransportProtos.PostTelemetryMsg postTelemetryMsg = latestEntityDataMsg.getPostTelemetryMsg(); + Assert.assertEquals(1, postTelemetryMsg.getTsKvListCount()); + TransportProtos.TsKvListProto tsKvListProto = postTelemetryMsg.getTsKvList(0); + Assert.assertEquals(timeseriesEntityData.get("ts").asLong(), tsKvListProto.getTs()); + Assert.assertEquals(1, tsKvListProto.getKvCount()); + TransportProtos.KeyValueProto keyValueProto = tsKvListProto.getKv(0); + Assert.assertEquals("temperature", keyValueProto.getKey()); + Assert.assertEquals(25, keyValueProto.getLongV()); + log.info("Timeseries tested successfully"); + } + + private void testAttributes() throws Exception { + log.info("Testing attributes"); + Device device = findDeviceByName("Edge Device 1"); + + testAttributesUpdatedMsg(device); + testPostAttributesMsg(device); + testAttributesDeleteMsg(device); + + log.info("Attributes tested successfully"); + } + + private void testAttributesDeleteMsg(Device device) throws JsonProcessingException, InterruptedException { + String deleteAttributesData = "{\"scope\":\"SERVER_SCOPE\",\"keys\":[\"key1\",\"key2\"]}"; + JsonNode deleteAttributesEntityData = mapper.readTree(deleteAttributesData); + EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.ATTRIBUTES_DELETED, device.getId().getId(), EdgeEventType.DEVICE, deleteAttributesEntityData); + edgeImitator.expectMessageAmount(1); + edgeEventService.saveAsync(edgeEvent); + clusterService.onEdgeEventUpdate(tenantId, edge.getId()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof EntityDataProto); + EntityDataProto latestEntityDataMsg = (EntityDataProto) latestMessage; + Assert.assertEquals(device.getUuidId().getMostSignificantBits(), latestEntityDataMsg.getEntityIdMSB()); + Assert.assertEquals(device.getUuidId().getLeastSignificantBits(), latestEntityDataMsg.getEntityIdLSB()); + Assert.assertEquals(device.getId().getEntityType().name(), latestEntityDataMsg.getEntityType()); + + Assert.assertTrue(latestEntityDataMsg.hasAttributeDeleteMsg()); + + AttributeDeleteMsg attributeDeleteMsg = latestEntityDataMsg.getAttributeDeleteMsg(); + Assert.assertEquals(attributeDeleteMsg.getScope(), deleteAttributesEntityData.get("scope").asText()); + + Assert.assertEquals(2, attributeDeleteMsg.getAttributeNamesCount()); + Assert.assertEquals("key1", attributeDeleteMsg.getAttributeNames(0)); + Assert.assertEquals("key2", attributeDeleteMsg.getAttributeNames(1)); + } + + private void testPostAttributesMsg(Device device) throws JsonProcessingException, InterruptedException { + String postAttributesData = "{\"scope\":\"SERVER_SCOPE\",\"kv\":{\"key2\":\"value2\"}}"; + JsonNode postAttributesEntityData = mapper.readTree(postAttributesData); + EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.POST_ATTRIBUTES, device.getId().getId(), EdgeEventType.DEVICE, postAttributesEntityData); + edgeImitator.expectMessageAmount(1); + edgeEventService.saveAsync(edgeEvent); + clusterService.onEdgeEventUpdate(tenantId, edge.getId()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof EntityDataProto); + EntityDataProto latestEntityDataMsg = (EntityDataProto) latestMessage; + Assert.assertEquals(device.getUuidId().getMostSignificantBits(), latestEntityDataMsg.getEntityIdMSB()); + Assert.assertEquals(device.getUuidId().getLeastSignificantBits(), latestEntityDataMsg.getEntityIdLSB()); + Assert.assertEquals(device.getId().getEntityType().name(), latestEntityDataMsg.getEntityType()); + Assert.assertEquals("SERVER_SCOPE", latestEntityDataMsg.getPostAttributeScope()); + Assert.assertTrue(latestEntityDataMsg.hasPostAttributesMsg()); + + TransportProtos.PostAttributeMsg postAttributesMsg = latestEntityDataMsg.getPostAttributesMsg(); + Assert.assertEquals(1, postAttributesMsg.getKvCount()); + TransportProtos.KeyValueProto keyValueProto = postAttributesMsg.getKv(0); + Assert.assertEquals("key2", keyValueProto.getKey()); + Assert.assertEquals("value2", keyValueProto.getStringV()); + } + + private void testAttributesUpdatedMsg(Device device) throws JsonProcessingException, InterruptedException { + String attributesData = "{\"scope\":\"SERVER_SCOPE\",\"kv\":{\"key1\":\"value1\"}}"; + JsonNode attributesEntityData = mapper.readTree(attributesData); + EdgeEvent edgeEvent1 = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.ATTRIBUTES_UPDATED, device.getId().getId(), EdgeEventType.DEVICE, attributesEntityData); + edgeImitator.expectMessageAmount(1); + edgeEventService.saveAsync(edgeEvent1); + clusterService.onEdgeEventUpdate(tenantId, edge.getId()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof EntityDataProto); + EntityDataProto latestEntityDataMsg = (EntityDataProto) latestMessage; + Assert.assertEquals(device.getUuidId().getMostSignificantBits(), latestEntityDataMsg.getEntityIdMSB()); + Assert.assertEquals(device.getUuidId().getLeastSignificantBits(), latestEntityDataMsg.getEntityIdLSB()); + Assert.assertEquals(device.getId().getEntityType().name(), latestEntityDataMsg.getEntityType()); + Assert.assertEquals("SERVER_SCOPE", latestEntityDataMsg.getPostAttributeScope()); + Assert.assertTrue(latestEntityDataMsg.hasAttributesUpdatedMsg()); + + TransportProtos.PostAttributeMsg attributesUpdatedMsg = latestEntityDataMsg.getAttributesUpdatedMsg(); + Assert.assertEquals(1, attributesUpdatedMsg.getKvCount()); + TransportProtos.KeyValueProto keyValueProto = attributesUpdatedMsg.getKv(0); + Assert.assertEquals("key1", keyValueProto.getKey()); + Assert.assertEquals("value1", keyValueProto.getStringV()); + } + + private void testSendMessagesToCloud() throws Exception { + log.info("Sending messages to cloud"); + sendDevice(); + sendDeviceWithNameThatAlreadyExistsOnCloud(); + sendRelationRequest(); + sendAlarm(); + sendTelemetry(); + sendRelation(); + sendDeleteDeviceOnEdge(); + sendRuleChainMetadataRequest(); + sendUserCredentialsRequest(); + sendDeviceCredentialsRequest(); + sendDeviceRpcResponse(); + sendDeviceCredentialsUpdate(); + sendAttributesRequest(); + log.info("Messages were sent successfully"); + } + + private void sendDevice() throws Exception { + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + DeviceUpdateMsg.Builder deviceUpdateMsgBuilder = DeviceUpdateMsg.newBuilder(); + deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + deviceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + deviceUpdateMsgBuilder.setName("Edge Device 2"); + deviceUpdateMsgBuilder.setType("test"); + deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(deviceUpdateMsgBuilder); + uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); + DeviceUpdateMsg latestDeviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + Assert.assertEquals("Edge Device 2", latestDeviceUpdateMsg.getName()); + + UUID newDeviceId = new UUID(latestDeviceUpdateMsg.getIdMSB(), latestDeviceUpdateMsg.getIdLSB()); + + Device device = doGet("/api/device/" + newDeviceId, Device.class); + Assert.assertNotNull(device); + Assert.assertEquals("Edge Device 2", device.getName()); + } + + private void sendDeviceWithNameThatAlreadyExistsOnCloud() throws Exception { + String deviceOnCloudName = RandomStringUtils.randomAlphanumeric(15); + Device deviceOnCloud = saveDevice(deviceOnCloudName, "Default"); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + DeviceUpdateMsg.Builder deviceUpdateMsgBuilder = DeviceUpdateMsg.newBuilder(); + deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + deviceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + deviceUpdateMsgBuilder.setName(deviceOnCloudName); + deviceUpdateMsgBuilder.setType("test"); + deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(deviceUpdateMsgBuilder); + uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); + DeviceUpdateMsg latestDeviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + Assert.assertNotEquals(deviceOnCloudName, latestDeviceUpdateMsg.getName()); + Assert.assertEquals(deviceOnCloudName, latestDeviceUpdateMsg.getConflictName()); + + UUID newDeviceId = new UUID(latestDeviceUpdateMsg.getIdMSB(), latestDeviceUpdateMsg.getIdLSB()); + + Assert.assertNotEquals(deviceOnCloud.getId().getId(), newDeviceId); + + Device device = doGet("/api/device/" + newDeviceId, Device.class); + Assert.assertNotNull(device); + Assert.assertNotEquals(deviceOnCloudName, device.getName()); + } + + private void sendRelationRequest() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + Asset asset = findAssetByName("Edge Asset 1"); + + EntityRelation relation = new EntityRelation(); + relation.setType("test"); + relation.setFrom(device.getId()); + relation.setTo(asset.getId()); + relation.setTypeGroup(RelationTypeGroup.COMMON); + + edgeImitator.expectMessageAmount(1); + doPost("/api/relation", relation); + Assert.assertTrue(edgeImitator.waitForMessages()); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + RelationRequestMsg.Builder relationRequestMsgBuilder = RelationRequestMsg.newBuilder(); + relationRequestMsgBuilder.setEntityIdMSB(device.getId().getId().getMostSignificantBits()); + relationRequestMsgBuilder.setEntityIdLSB(device.getId().getId().getLeastSignificantBits()); + relationRequestMsgBuilder.setEntityType(device.getId().getEntityType().name()); + testAutoGeneratedCodeByProtobuf(relationRequestMsgBuilder); + + uplinkMsgBuilder.addRelationRequestMsg(relationRequestMsgBuilder.build()); + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof RelationUpdateMsg); + RelationUpdateMsg relationUpdateMsg = (RelationUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, relationUpdateMsg.getMsgType()); + Assert.assertEquals(relation.getType(), relationUpdateMsg.getType()); + + UUID fromUUID = new UUID(relationUpdateMsg.getFromIdMSB(), relationUpdateMsg.getFromIdLSB()); + EntityId fromEntityId = EntityIdFactory.getByTypeAndUuid(relationUpdateMsg.getFromEntityType(), fromUUID); + Assert.assertEquals(relation.getFrom(), fromEntityId); + + UUID toUUID = new UUID(relationUpdateMsg.getToIdMSB(), relationUpdateMsg.getToIdLSB()); + EntityId toEntityId = EntityIdFactory.getByTypeAndUuid(relationUpdateMsg.getToEntityType(), toUUID); + Assert.assertEquals(relation.getTo(), toEntityId); + + Assert.assertEquals(relation.getTypeGroup().name(), relationUpdateMsg.getTypeGroup()); + } + + private void sendAlarm() throws Exception { + Device device = findDeviceByName("Edge Device 2"); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + AlarmUpdateMsg.Builder alarmUpdateMgBuilder = AlarmUpdateMsg.newBuilder(); + alarmUpdateMgBuilder.setName("alarm from edge"); + alarmUpdateMgBuilder.setStatus(AlarmStatus.ACTIVE_UNACK.name()); + alarmUpdateMgBuilder.setSeverity(AlarmSeverity.CRITICAL.name()); + alarmUpdateMgBuilder.setOriginatorName(device.getName()); + alarmUpdateMgBuilder.setOriginatorType(EntityType.DEVICE.name()); + testAutoGeneratedCodeByProtobuf(alarmUpdateMgBuilder); + uplinkMsgBuilder.addAlarmUpdateMsg(alarmUpdateMgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + + + List alarms = doGetTypedWithPageLink("/api/alarm/{entityType}/{entityId}?", + new TypeReference>() {}, + new PageLink(100), device.getId().getEntityType().name(), device.getId().getId().toString()) + .getData(); + Optional foundAlarm = alarms.stream().filter(alarm -> alarm.getType().equals("alarm from edge")).findAny(); + Assert.assertTrue(foundAlarm.isPresent()); + AlarmInfo alarmInfo = foundAlarm.get(); + Assert.assertEquals(device.getId(), alarmInfo.getOriginator()); + Assert.assertEquals(AlarmStatus.ACTIVE_UNACK, alarmInfo.getStatus()); + Assert.assertEquals(AlarmSeverity.CRITICAL, alarmInfo.getSeverity()); + } + + private void sendRelation() throws Exception { + List edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getId().getId().toString() + "/devices?", + new TypeReference>() {}, new PageLink(100)).getData(); + Optional foundDevice1 = edgeDevices.stream().filter(device1 -> device1.getName().equals("Edge Device 1")).findAny(); + Assert.assertTrue(foundDevice1.isPresent()); + Device device1 = foundDevice1.get(); + Optional foundDevice2 = edgeDevices.stream().filter(device2 -> device2.getName().equals("Edge Device 2")).findAny(); + Assert.assertTrue(foundDevice2.isPresent()); + Device device2 = foundDevice2.get(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + RelationUpdateMsg.Builder relationUpdateMsgBuilder = RelationUpdateMsg.newBuilder(); + relationUpdateMsgBuilder.setType("test"); + relationUpdateMsgBuilder.setTypeGroup(RelationTypeGroup.COMMON.name()); + relationUpdateMsgBuilder.setToIdMSB(device1.getId().getId().getMostSignificantBits()); + relationUpdateMsgBuilder.setToIdLSB(device1.getId().getId().getLeastSignificantBits()); + relationUpdateMsgBuilder.setToEntityType(device1.getId().getEntityType().name()); + relationUpdateMsgBuilder.setFromIdMSB(device2.getId().getId().getMostSignificantBits()); + relationUpdateMsgBuilder.setFromIdLSB(device2.getId().getId().getLeastSignificantBits()); + relationUpdateMsgBuilder.setFromEntityType(device2.getId().getEntityType().name()); + relationUpdateMsgBuilder.setAdditionalInfo("{}"); + testAutoGeneratedCodeByProtobuf(relationUpdateMsgBuilder); + uplinkMsgBuilder.addRelationUpdateMsg(relationUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + + EntityRelation relation = doGet("/api/relation?" + + "&fromId=" + device2.getId().getId().toString() + + "&fromType=" + device2.getId().getEntityType().name() + + "&relationType=" + "test" + + "&relationTypeGroup=" + RelationTypeGroup.COMMON.name() + + "&toId=" + device1.getId().getId().toString() + + "&toType=" + device1.getId().getEntityType().name(), EntityRelation.class); + Assert.assertNotNull(relation); + } + + private void sendTelemetry() throws Exception { + List edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getId().getId().toString() + "/devices?", + new TypeReference>() {}, new PageLink(100)).getData(); + Optional foundDevice = edgeDevices.stream().filter(device1 -> device1.getName().equals("Edge Device 2")).findAny(); + Assert.assertTrue(foundDevice.isPresent()); + Device device = foundDevice.get(); + + edgeImitator.expectResponsesAmount(2); + + JsonObject data = new JsonObject(); + String timeseriesKey = "key"; + String timeseriesValue = "25"; + data.addProperty(timeseriesKey, timeseriesValue); + UplinkMsg.Builder uplinkMsgBuilder1 = UplinkMsg.newBuilder(); + EntityDataProto.Builder entityDataBuilder = EntityDataProto.newBuilder(); + entityDataBuilder.setPostTelemetryMsg(JsonConverter.convertToTelemetryProto(data, System.currentTimeMillis())); + entityDataBuilder.setEntityType(device.getId().getEntityType().name()); + entityDataBuilder.setEntityIdMSB(device.getUuidId().getMostSignificantBits()); + entityDataBuilder.setEntityIdLSB(device.getUuidId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(entityDataBuilder); + uplinkMsgBuilder1.addEntityData(entityDataBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder1.build()); + + JsonObject attributesData = new JsonObject(); + String attributesKey = "test_attr"; + String attributesValue = "test_value"; + attributesData.addProperty(attributesKey, attributesValue); + UplinkMsg.Builder uplinkMsgBuilder2 = UplinkMsg.newBuilder(); + EntityDataProto.Builder entityDataBuilder2 = EntityDataProto.newBuilder(); + entityDataBuilder2.setEntityType(device.getId().getEntityType().name()); + entityDataBuilder2.setEntityIdMSB(device.getId().getId().getMostSignificantBits()); + entityDataBuilder2.setEntityIdLSB(device.getId().getId().getLeastSignificantBits()); + entityDataBuilder2.setAttributesUpdatedMsg(JsonConverter.convertToAttributesProto(attributesData)); + entityDataBuilder2.setPostAttributeScope(DataConstants.SERVER_SCOPE); + testAutoGeneratedCodeByProtobuf(entityDataBuilder2); + + uplinkMsgBuilder2.addEntityData(entityDataBuilder2.build()); + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder2); + + edgeImitator.sendUplinkMsg(uplinkMsgBuilder2.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + + // Wait before device attributes saved to database before requesting them from controller + Thread.sleep(1000); + Map>> timeseries = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/values/timeseries?keys=" + timeseriesKey, new TypeReference<>() {}); + Assert.assertTrue(timeseries.containsKey(timeseriesKey)); + Assert.assertEquals(1, timeseries.get(timeseriesKey).size()); + Assert.assertEquals(timeseriesValue, timeseries.get(timeseriesKey).get(0).get("value")); + + List> attributes = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/attributes/" + DataConstants.SERVER_SCOPE, new TypeReference<>() {}); + Assert.assertEquals(1, attributes.size()); + Assert.assertEquals(attributes.get(0).get("key"), attributesKey); + Assert.assertEquals(attributes.get(0).get("value"), attributesValue); + + } + + private void sendRuleChainMetadataRequest() throws Exception { + RuleChainId edgeRootRuleChainId = edge.getRootRuleChainId(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + RuleChainMetadataRequestMsg.Builder ruleChainMetadataRequestMsgBuilder = RuleChainMetadataRequestMsg.newBuilder(); + ruleChainMetadataRequestMsgBuilder.setRuleChainIdMSB(edgeRootRuleChainId.getId().getMostSignificantBits()); + ruleChainMetadataRequestMsgBuilder.setRuleChainIdLSB(edgeRootRuleChainId.getId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(ruleChainMetadataRequestMsgBuilder); + uplinkMsgBuilder.addRuleChainMetadataRequestMsg(ruleChainMetadataRequestMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages());; + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof RuleChainMetadataUpdateMsg); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = (RuleChainMetadataUpdateMsg) latestMessage; + Assert.assertEquals(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), edgeRootRuleChainId.getId().getMostSignificantBits()); + Assert.assertEquals(ruleChainMetadataUpdateMsg.getRuleChainIdLSB(), edgeRootRuleChainId.getId().getLeastSignificantBits()); + + testAutoGeneratedCodeByProtobuf(ruleChainMetadataUpdateMsg); + } + + private void sendUserCredentialsRequest() throws Exception { + UserId userId = edgeImitator.getUserId(); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder(); + userCredentialsRequestMsgBuilder.setUserIdMSB(userId.getId().getMostSignificantBits()); + userCredentialsRequestMsgBuilder.setUserIdLSB(userId.getId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(userCredentialsRequestMsgBuilder); + uplinkMsgBuilder.addUserCredentialsRequestMsg(userCredentialsRequestMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); + UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; + Assert.assertEquals(userCredentialsUpdateMsg.getUserIdMSB(), userId.getId().getMostSignificantBits()); + Assert.assertEquals(userCredentialsUpdateMsg.getUserIdLSB(), userId.getId().getLeastSignificantBits()); + + testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg); + } + + private void sendDeviceCredentialsRequest() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + + DeviceCredentials deviceCredentials = doGet("/api/device/" + device.getId().getId().toString() + "/credentials", DeviceCredentials.class); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + DeviceCredentialsRequestMsg.Builder deviceCredentialsRequestMsgBuilder = DeviceCredentialsRequestMsg.newBuilder(); + deviceCredentialsRequestMsgBuilder.setDeviceIdMSB(device.getUuidId().getMostSignificantBits()); + deviceCredentialsRequestMsgBuilder.setDeviceIdLSB(device.getUuidId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(deviceCredentialsRequestMsgBuilder); + uplinkMsgBuilder.addDeviceCredentialsRequestMsg(deviceCredentialsRequestMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceCredentialsUpdateMsg); + DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = (DeviceCredentialsUpdateMsg) latestMessage; + Assert.assertEquals(deviceCredentialsUpdateMsg.getDeviceIdMSB(), device.getUuidId().getMostSignificantBits()); + Assert.assertEquals(deviceCredentialsUpdateMsg.getDeviceIdLSB(), device.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(deviceCredentialsUpdateMsg.getCredentialsType(), deviceCredentials.getCredentialsType().name()); + Assert.assertEquals(deviceCredentialsUpdateMsg.getCredentialsId(), deviceCredentials.getCredentialsId()); + } + + private void sendDeviceCredentialsUpdate() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + DeviceCredentialsUpdateMsg.Builder deviceCredentialsUpdateMsgBuilder = DeviceCredentialsUpdateMsg.newBuilder(); + deviceCredentialsUpdateMsgBuilder.setDeviceIdMSB(device.getUuidId().getMostSignificantBits()); + deviceCredentialsUpdateMsgBuilder.setDeviceIdLSB(device.getUuidId().getLeastSignificantBits()); + deviceCredentialsUpdateMsgBuilder.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN.name()); + deviceCredentialsUpdateMsgBuilder.setCredentialsId("NEW_TOKEN"); + testAutoGeneratedCodeByProtobuf(deviceCredentialsUpdateMsgBuilder); + uplinkMsgBuilder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + } + + private void sendDeviceRpcResponse() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + DeviceRpcCallMsg.Builder deviceRpcCallResponseBuilder = DeviceRpcCallMsg.newBuilder(); + deviceRpcCallResponseBuilder.setDeviceIdMSB(device.getUuidId().getMostSignificantBits()); + deviceRpcCallResponseBuilder.setDeviceIdLSB(device.getUuidId().getLeastSignificantBits()); + deviceRpcCallResponseBuilder.setOneway(true); + deviceRpcCallResponseBuilder.setRequestId(0); + deviceRpcCallResponseBuilder.setExpirationTime(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(10)); + RpcResponseMsg.Builder responseBuilder = + RpcResponseMsg.newBuilder().setResponse("{}"); + testAutoGeneratedCodeByProtobuf(responseBuilder); + + deviceRpcCallResponseBuilder.setResponseMsg(responseBuilder.build()); + testAutoGeneratedCodeByProtobuf(deviceRpcCallResponseBuilder); + + uplinkMsgBuilder.addDeviceRpcCallMsg(deviceRpcCallResponseBuilder.build()); + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + } + + private void sendAttributesRequest() throws Exception { + Device device = findDeviceByName("Edge Device 1"); + sendAttributesRequest(device, DataConstants.SERVER_SCOPE, "{\"key1\":\"value1\"}", "key1", "value1"); + sendAttributesRequest(device, DataConstants.SHARED_SCOPE, "{\"key2\":\"value2\"}", "key2", "value2"); + } + + private void sendAttributesRequest(Device device, String scope, String attributesDataStr, String expectedKey, String expectedValue) throws Exception { + JsonNode attributesData = mapper.readTree(attributesDataStr); + + doPost("/api/plugins/telemetry/DEVICE/" + device.getId().getId().toString() + "/attributes/" + scope, + attributesData); + + // Wait before device attributes saved to database before requesting them from edge + Thread.sleep(1000); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + AttributesRequestMsg.Builder attributesRequestMsgBuilder = AttributesRequestMsg.newBuilder(); + attributesRequestMsgBuilder.setEntityIdMSB(device.getUuidId().getMostSignificantBits()); + attributesRequestMsgBuilder.setEntityIdLSB(device.getUuidId().getLeastSignificantBits()); + attributesRequestMsgBuilder.setEntityType(EntityType.DEVICE.name()); + attributesRequestMsgBuilder.setScope(scope); + testAutoGeneratedCodeByProtobuf(attributesRequestMsgBuilder); + uplinkMsgBuilder.addAttributesRequestMsg(attributesRequestMsgBuilder.build()); + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof EntityDataProto); + EntityDataProto latestEntityDataMsg = (EntityDataProto) latestMessage; + Assert.assertEquals(device.getUuidId().getMostSignificantBits(), latestEntityDataMsg.getEntityIdMSB()); + Assert.assertEquals(device.getUuidId().getLeastSignificantBits(), latestEntityDataMsg.getEntityIdLSB()); + Assert.assertEquals(device.getId().getEntityType().name(), latestEntityDataMsg.getEntityType()); + Assert.assertEquals(scope, latestEntityDataMsg.getPostAttributeScope()); + Assert.assertTrue(latestEntityDataMsg.hasAttributesUpdatedMsg()); + + TransportProtos.PostAttributeMsg attributesUpdatedMsg = latestEntityDataMsg.getAttributesUpdatedMsg(); + Assert.assertEquals(1, attributesUpdatedMsg.getKvCount()); + TransportProtos.KeyValueProto keyValueProto = attributesUpdatedMsg.getKv(0); + Assert.assertEquals(expectedKey, keyValueProto.getKey()); + Assert.assertEquals(expectedValue, keyValueProto.getStringV()); + } + + private void sendDeleteDeviceOnEdge() throws Exception { + Device device = findDeviceByName("Edge Device 2"); + UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder(); + DeviceUpdateMsg.Builder deviceDeleteMsgBuilder = DeviceUpdateMsg.newBuilder(); + deviceDeleteMsgBuilder.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE); + deviceDeleteMsgBuilder.setIdMSB(device.getId().getId().getMostSignificantBits()); + deviceDeleteMsgBuilder.setIdLSB(device.getId().getId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(deviceDeleteMsgBuilder); + + upLinkMsgBuilder.addDeviceUpdateMsg(deviceDeleteMsgBuilder.build()); + testAutoGeneratedCodeByProtobuf(upLinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + device = doGet("/api/device/" + device.getId().getId().toString(), Device.class); + Assert.assertNotNull(device); + List edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getId().getId().toString() + "/devices?", + new TypeReference>() { + }, new PageLink(100)).getData(); + Assert.assertFalse(edgeDevices.contains(device)); + } + + private void installation() throws Exception { + edge = doPost("/api/edge", constructEdge("Test Edge", "test"), Edge.class); + + DeviceProfile deviceProfile = this.createDeviceProfile(CUSTOM_DEVICE_PROFILE_NAME, null); + extendDeviceProfileData(deviceProfile); + doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + + Device savedDevice = saveDevice("Edge Device 1", CUSTOM_DEVICE_PROFILE_NAME); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/device/" + savedDevice.getId().getId().toString(), Device.class); + + Asset savedAsset = saveAsset("Edge Asset 1"); + doPost("/api/edge/" + edge.getId().getId().toString() + + "/asset/" + savedAsset.getId().getId().toString(), Asset.class); + } + + private void extendDeviceProfileData(DeviceProfile deviceProfile) { + DeviceProfileData profileData = deviceProfile.getProfileData(); + List alarms = new ArrayList<>(); + DeviceProfileAlarm deviceProfileAlarm = new DeviceProfileAlarm(); + deviceProfileAlarm.setAlarmType("High Temperature"); + AlarmRule alarmRule = new AlarmRule(); + alarmRule.setAlarmDetails("Alarm Details"); + AlarmCondition alarmCondition = new AlarmCondition(); + alarmCondition.setSpec(new SimpleAlarmConditionSpec()); + List condition = new ArrayList<>(); + AlarmConditionFilter alarmConditionFilter = new AlarmConditionFilter(); + alarmConditionFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, "temperature")); + NumericFilterPredicate predicate = new NumericFilterPredicate(); + predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + predicate.setValue(new FilterPredicateValue<>(55.0)); + alarmConditionFilter.setPredicate(predicate); + alarmConditionFilter.setValueType(EntityKeyValueType.NUMERIC); + condition.add(alarmConditionFilter); + alarmCondition.setCondition(condition); + alarmRule.setCondition(alarmCondition); + deviceProfileAlarm.setClearRule(alarmRule); + TreeMap createRules = new TreeMap<>(); + createRules.put(AlarmSeverity.CRITICAL, alarmRule); + deviceProfileAlarm.setCreateRules(createRules); + alarms.add(deviceProfileAlarm); + profileData.setAlarms(alarms); + profileData.setProvisionConfiguration(new AllowCreateNewDevicesDeviceProfileProvisionConfiguration("123")); + } + + private EdgeEvent constructEdgeEvent(TenantId tenantId, EdgeId edgeId, EdgeEventActionType edgeEventAction, UUID entityId, EdgeEventType edgeEventType, JsonNode entityBody) { + EdgeEvent edgeEvent = new EdgeEvent(); + edgeEvent.setEdgeId(edgeId); + edgeEvent.setTenantId(tenantId); + edgeEvent.setAction(edgeEventAction); + edgeEvent.setEntityId(entityId); + edgeEvent.setType(edgeEventType); + edgeEvent.setBody(entityBody); + return edgeEvent; + } + + private void testAutoGeneratedCodeByProtobuf(MessageLite.Builder builder) throws InvalidProtocolBufferException { + MessageLite source = builder.build(); + + testAutoGeneratedCodeByProtobuf(source); + + MessageLite target = source.getParserForType().parseFrom(source.toByteArray()); + builder.clear().mergeFrom(target); + } + + private void testAutoGeneratedCodeByProtobuf(MessageLite source) throws InvalidProtocolBufferException { + MessageLite target = source.getParserForType().parseFrom(source.toByteArray()); + Assert.assertEquals(source, target); + Assert.assertEquals(source.hashCode(), target.hashCode()); + } +} diff --git a/application/src/test/java/org/thingsboard/server/edge/EdgeSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/edge/EdgeSqlTestSuite.java new file mode 100644 index 0000000000..19b6bd7f23 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/EdgeSqlTestSuite.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2021 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.edge; + +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.extensions.cpsuite.ClasspathSuite; +import org.junit.runner.RunWith; +import org.thingsboard.server.dao.CustomSqlUnit; +import org.thingsboard.server.queue.memory.InMemoryStorage; + +import java.util.Arrays; + +@RunWith(ClasspathSuite.class) +@ClasspathSuite.ClassnameFilters({"org.thingsboard.server.edge.sql.*Test"}) +public class EdgeSqlTestSuite { + + @ClassRule + public static CustomSqlUnit sqlUnit = new CustomSqlUnit( + Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"), + "sql/hsql/drop-all-tables.sql", + "sql-test.properties"); + + @BeforeClass + public static void cleanupInMemStorage(){ + InMemoryStorage.getInstance().cleanup(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java new file mode 100644 index 0000000000..6b4234f151 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -0,0 +1,322 @@ +/** + * Copyright © 2016-2021 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.edge.imitator; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.AbstractMessage; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.thingsboard.edge.rpc.EdgeGrpcClient; +import org.thingsboard.edge.rpc.EdgeRpcClient; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.gen.edge.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.CustomerUpdateMsg; +import org.thingsboard.server.gen.edge.DashboardUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.DeviceCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceProfileUpdateMsg; +import org.thingsboard.server.gen.edge.DeviceRpcCallMsg; +import org.thingsboard.server.gen.edge.DeviceUpdateMsg; +import org.thingsboard.server.gen.edge.DownlinkMsg; +import org.thingsboard.server.gen.edge.DownlinkResponseMsg; +import org.thingsboard.server.gen.edge.EdgeConfiguration; +import org.thingsboard.server.gen.edge.EntityDataProto; +import org.thingsboard.server.gen.edge.EntityViewUpdateMsg; +import org.thingsboard.server.gen.edge.RelationUpdateMsg; +import org.thingsboard.server.gen.edge.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.UplinkMsg; +import org.thingsboard.server.gen.edge.UplinkResponseMsg; +import org.thingsboard.server.gen.edge.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.UserUpdateMsg; +import org.thingsboard.server.gen.edge.WidgetTypeUpdateMsg; +import org.thingsboard.server.gen.edge.WidgetsBundleUpdateMsg; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +@Slf4j +public class EdgeImitator { + + private String routingKey; + private String routingSecret; + + private EdgeRpcClient edgeRpcClient; + + private final Lock lock = new ReentrantLock(); + + private CountDownLatch messagesLatch; + private CountDownLatch responsesLatch; + private List> ignoredTypes; + + @Getter + private EdgeConfiguration configuration; + @Getter + private UserId userId; + @Getter + private final List downlinkMsgs; + + public EdgeImitator(String host, int port, String routingKey, String routingSecret) throws NoSuchFieldException, IllegalAccessException { + edgeRpcClient = new EdgeGrpcClient(); + messagesLatch = new CountDownLatch(0); + responsesLatch = new CountDownLatch(0); + downlinkMsgs = new ArrayList<>(); + ignoredTypes = new ArrayList<>(); + this.routingKey = routingKey; + this.routingSecret = routingSecret; + setEdgeCredentials("rpcHost", host); + setEdgeCredentials("rpcPort", port); + setEdgeCredentials("keepAliveTimeSec", 300); + } + + private void setEdgeCredentials(String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException { + Field fieldToSet = edgeRpcClient.getClass().getDeclaredField(fieldName); + fieldToSet.setAccessible(true); + fieldToSet.set(edgeRpcClient, value); + fieldToSet.setAccessible(false); + } + + public void connect() { + edgeRpcClient.connect(routingKey, routingSecret, + this::onUplinkResponse, + this::onEdgeUpdate, + this::onDownlink, + this::onClose); + + edgeRpcClient.sendSyncRequestMsg(); + } + + public void disconnect() throws InterruptedException { + edgeRpcClient.disconnect(false); + } + + public void sendUplinkMsg(UplinkMsg uplinkMsg) { + edgeRpcClient.sendUplinkMsg(uplinkMsg); + } + + private void onUplinkResponse(UplinkResponseMsg msg) { + log.info("onUplinkResponse: {}", msg); + responsesLatch.countDown(); + } + + private void onEdgeUpdate(EdgeConfiguration edgeConfiguration) { + this.configuration = edgeConfiguration; + } + + private void onUserUpdate(UserUpdateMsg userUpdateMsg) { + this.userId = new UserId(new UUID(userUpdateMsg.getIdMSB(), userUpdateMsg.getIdLSB())); + } + + private void onDownlink(DownlinkMsg downlinkMsg) { + ListenableFuture> future = processDownlinkMsg(downlinkMsg); + Futures.addCallback(future, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List result) { + DownlinkResponseMsg downlinkResponseMsg = DownlinkResponseMsg.newBuilder().setSuccess(true).build(); + edgeRpcClient.sendDownlinkResponseMsg(downlinkResponseMsg); + } + + @Override + public void onFailure(Throwable t) { + DownlinkResponseMsg downlinkResponseMsg = DownlinkResponseMsg.newBuilder().setSuccess(false).setErrorMsg(t.getMessage()).build(); + edgeRpcClient.sendDownlinkResponseMsg(downlinkResponseMsg); + } + }, MoreExecutors.directExecutor()); + } + + private void onClose(Exception e) { + log.info("onClose: {}", e.getMessage()); + } + + private ListenableFuture> processDownlinkMsg(DownlinkMsg downlinkMsg) { + List> result = new ArrayList<>(); + if (downlinkMsg.getDeviceUpdateMsgCount() > 0) { + for (DeviceUpdateMsg deviceUpdateMsg: downlinkMsg.getDeviceUpdateMsgList()) { + result.add(saveDownlinkMsg(deviceUpdateMsg)); + } + } + if (downlinkMsg.getDeviceProfileUpdateMsgCount() > 0) { + for (DeviceProfileUpdateMsg deviceProfileUpdateMsg : downlinkMsg.getDeviceProfileUpdateMsgList()) { + result.add(saveDownlinkMsg(deviceProfileUpdateMsg)); + } + } + if (downlinkMsg.getDeviceCredentialsUpdateMsgCount() > 0) { + for (DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg: downlinkMsg.getDeviceCredentialsUpdateMsgList()) { + result.add(saveDownlinkMsg(deviceCredentialsUpdateMsg)); + } + } + if (downlinkMsg.getAssetUpdateMsgCount() > 0) { + for (AssetUpdateMsg assetUpdateMsg: downlinkMsg.getAssetUpdateMsgList()) { + result.add(saveDownlinkMsg(assetUpdateMsg)); + } + } + if (downlinkMsg.getRuleChainUpdateMsgCount() > 0) { + for (RuleChainUpdateMsg ruleChainUpdateMsg: downlinkMsg.getRuleChainUpdateMsgList()) { + result.add(saveDownlinkMsg(ruleChainUpdateMsg)); + } + } + if (downlinkMsg.getRuleChainMetadataUpdateMsgCount() > 0) { + for (RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg: downlinkMsg.getRuleChainMetadataUpdateMsgList()) { + result.add(saveDownlinkMsg(ruleChainMetadataUpdateMsg)); + } + } + if (downlinkMsg.getDashboardUpdateMsgCount() > 0) { + for (DashboardUpdateMsg dashboardUpdateMsg: downlinkMsg.getDashboardUpdateMsgList()) { + result.add(saveDownlinkMsg(dashboardUpdateMsg)); + } + } + if (downlinkMsg.getRelationUpdateMsgCount() > 0) { + for (RelationUpdateMsg relationUpdateMsg: downlinkMsg.getRelationUpdateMsgList()) { + result.add(saveDownlinkMsg(relationUpdateMsg)); + } + } + if (downlinkMsg.getAlarmUpdateMsgCount() > 0) { + for (AlarmUpdateMsg alarmUpdateMsg: downlinkMsg.getAlarmUpdateMsgList()) { + result.add(saveDownlinkMsg(alarmUpdateMsg)); + } + } + if (downlinkMsg.getEntityDataCount() > 0) { + for (EntityDataProto entityData: downlinkMsg.getEntityDataList()) { + result.add(saveDownlinkMsg(entityData)); + } + } + if (downlinkMsg.getEntityViewUpdateMsgCount() > 0) { + for (EntityViewUpdateMsg entityViewUpdateMsg: downlinkMsg.getEntityViewUpdateMsgList()) { + result.add(saveDownlinkMsg(entityViewUpdateMsg)); + } + } + if (downlinkMsg.getCustomerUpdateMsgCount() > 0) { + for (CustomerUpdateMsg customerUpdateMsg: downlinkMsg.getCustomerUpdateMsgList()) { + result.add(saveDownlinkMsg(customerUpdateMsg)); + } + } + if (downlinkMsg.getWidgetsBundleUpdateMsgCount() > 0) { + for (WidgetsBundleUpdateMsg widgetsBundleUpdateMsg: downlinkMsg.getWidgetsBundleUpdateMsgList()) { + result.add(saveDownlinkMsg(widgetsBundleUpdateMsg)); + } + } + if (downlinkMsg.getWidgetTypeUpdateMsgCount() > 0) { + for (WidgetTypeUpdateMsg widgetTypeUpdateMsg: downlinkMsg.getWidgetTypeUpdateMsgList()) { + result.add(saveDownlinkMsg(widgetTypeUpdateMsg)); + } + } + if (downlinkMsg.getUserUpdateMsgCount() > 0) { + for (UserUpdateMsg userUpdateMsg: downlinkMsg.getUserUpdateMsgList()) { + onUserUpdate(userUpdateMsg); + result.add(saveDownlinkMsg(userUpdateMsg)); + } + } + if (downlinkMsg.getUserCredentialsUpdateMsgCount() > 0) { + for (UserCredentialsUpdateMsg userCredentialsUpdateMsg: downlinkMsg.getUserCredentialsUpdateMsgList()) { + result.add(saveDownlinkMsg(userCredentialsUpdateMsg)); + } + } + if (downlinkMsg.getDeviceRpcCallMsgCount() > 0) { + for (DeviceRpcCallMsg deviceRpcCallMsg: downlinkMsg.getDeviceRpcCallMsgList()) { + result.add(saveDownlinkMsg(deviceRpcCallMsg)); + } + } + if (downlinkMsg.getDeviceCredentialsRequestMsgCount() > 0) { + for (DeviceCredentialsRequestMsg deviceCredentialsRequestMsg: downlinkMsg.getDeviceCredentialsRequestMsgList()) { + result.add(saveDownlinkMsg(deviceCredentialsRequestMsg)); + } + } + return Futures.allAsList(result); + } + + private ListenableFuture saveDownlinkMsg(AbstractMessage message) { + if (!ignoredTypes.contains(message.getClass())) { + try { + lock.lock(); + downlinkMsgs.add(message); + } finally { + lock.unlock(); + } + messagesLatch.countDown(); + } + return Futures.immediateFuture(null); + } + + public boolean waitForMessages() throws InterruptedException { + return waitForMessages(5); + } + + public boolean waitForMessages(int timeout) throws InterruptedException { + return messagesLatch.await(timeout, TimeUnit.SECONDS); + } + + public void expectMessageAmount(int messageAmount) { + messagesLatch = new CountDownLatch(messageAmount); + } + + public boolean waitForResponses() throws InterruptedException { + return responsesLatch.await(5, TimeUnit.SECONDS); + } + + public void expectResponsesAmount(int messageAmount) { + responsesLatch = new CountDownLatch(messageAmount); + } + + @SuppressWarnings("unchecked") + public Optional findMessageByType(Class tClass) { + Optional result; + try { + lock.lock(); + result = (Optional) downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg.getClass().isAssignableFrom(tClass)).findAny(); + } finally { + lock.unlock(); + } + return result; + } + + @SuppressWarnings("unchecked") + public List findAllMessagesByType(Class tClass) { + List result; + try { + lock.lock(); + result = (List) downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg.getClass().isAssignableFrom(tClass)).collect(Collectors.toList()); + } finally { + lock.unlock(); + } + return result; + } + + public AbstractMessage getLatestMessage() { + return downlinkMsgs.get(downlinkMsgs.size() - 1); + } + + public void ignoreType(Class type) { + ignoredTypes.add(type); + } + + public void allowIgnoredTypes() { + ignoredTypes.clear(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/edge/sql/EdgeSqlTest.java b/application/src/test/java/org/thingsboard/server/edge/sql/EdgeSqlTest.java new file mode 100644 index 0000000000..041023d0b1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/sql/EdgeSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2021 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.edge.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.edge.BaseEdgeTest; + +@DaoSqlTest +public class EdgeSqlTest extends BaseEdgeTest { +} diff --git a/application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java new file mode 100644 index 0000000000..0d470c78b9 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2021 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; + +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.extensions.cpsuite.ClasspathSuite; +import org.junit.runner.RunWith; +import org.thingsboard.server.dao.CustomSqlUnit; +import org.thingsboard.server.queue.memory.InMemoryStorage; + +import java.util.Arrays; + +@RunWith(ClasspathSuite.class) +@ClasspathSuite.ClassnameFilters({ + "org.thingsboard.server.service.resource.*Test", + }) +public class ServiceSqlTestSuite { + + @ClassRule + public static CustomSqlUnit sqlUnit = new CustomSqlUnit( + Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/schema-entities-idx.sql", "sql/system-data.sql"), + "sql/hsql/drop-all-tables.sql", + "sql-test.properties"); + + @BeforeClass + public static void cleanupInMemStorage(){ + InMemoryStorage.getInstance().cleanup(); + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/BaseTbResourceServiceTest.java similarity index 88% rename from dao/src/test/java/org/thingsboard/server/dao/service/BaseTbResourceServiceTest.java rename to application/src/test/java/org/thingsboard/server/service/resource/BaseTbResourceServiceTest.java index 21842af176..ffdefc27ed 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/BaseTbResourceServiceTest.java @@ -13,28 +13,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.service; +package org.thingsboard.server.service.resource; import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; 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.security.Authority; +import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.AbstractServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; import java.util.Base64; import java.util.Collections; import java.util.List; -public abstract class BaseTbResourceServiceTest extends AbstractServiceTest { +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class BaseTbResourceServiceTest extends AbstractControllerTest { private static final String LWM2M_TEST_MODEL = "\n" + "\n" + @@ -67,18 +76,38 @@ public abstract class BaseTbResourceServiceTest extends AbstractServiceTest { private TenantId tenantId; + @Autowired + private TbResourceService resourceService; + + private Tenant savedTenant; + private User tenantAdmin; + @Before - public void before() { + public void beforeTest() throws Exception { + loginSysAdmin(); + Tenant tenant = new Tenant(); tenant.setTitle("My tenant"); - Tenant savedTenant = tenantService.saveTenant(tenant); - Assert.assertNotNull(savedTenant); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); tenantId = savedTenant.getId(); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); } @After - public void after() { - tenantService.deleteTenant(tenantId); + public void afterTest() throws Exception { + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); } @Test @@ -239,9 +268,10 @@ public abstract class BaseTbResourceServiceTest extends AbstractServiceTest { @Test public void testFindTenantResourcesByTenantId() throws Exception { + loginSysAdmin(); Tenant tenant = new Tenant(); tenant.setTitle("Test tenant"); - tenant = tenantService.saveTenant(tenant); + tenant = doPost("/api/tenant", tenant, Tenant.class); TenantId tenantId = tenant.getId(); @@ -279,14 +309,17 @@ public abstract class BaseTbResourceServiceTest extends AbstractServiceTest { Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); - tenantService.deleteTenant(tenantId); + doDelete("/api/tenant/" + tenantId.getId().toString()) + .andExpect(status().isOk()); } @Test public void testFindAllTenantResourcesByTenantId() throws Exception { + loginSysAdmin(); + Tenant tenant = new Tenant(); tenant.setTitle("Test tenant"); - tenant = tenantService.saveTenant(tenant); + tenant = doPost("/api/tenant", tenant, Tenant.class); TenantId tenantId = tenant.getId(); @@ -344,7 +377,8 @@ public abstract class BaseTbResourceServiceTest extends AbstractServiceTest { Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); - tenantService.deleteTenant(tenantId); + doDelete("/api/tenant/" + tenantId.getId().toString()) + .andExpect(status().isOk()); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java index 89ef53b19d..74523852a9 100644 --- a/application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java @@ -15,35 +15,11 @@ */ package org.thingsboard.server.transport; -import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; -import org.eclipse.paho.client.mqttv3.MqttConnectOptions; -import org.eclipse.paho.client.mqttv3.MqttException; -import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; -import org.junit.Assert; -import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.DeviceProfileProvisionType; -import org.thingsboard.server.common.data.DeviceProfileType; -import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.device.profile.AllowCreateNewDevicesDeviceProfileProvisionConfiguration; -import org.thingsboard.server.common.data.device.profile.CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration; -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.DeviceProfileProvisionConfiguration; -import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; -import org.thingsboard.server.common.data.device.profile.JsonTransportPayloadConfiguration; -import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; -import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; -import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; -import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.gen.transport.TransportProtos; @@ -51,8 +27,6 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Slf4j @@ -105,6 +79,22 @@ public abstract class AbstractTransportIntegrationTest extends AbstractControlle " }\n" + "}"; + protected static final String DEVICE_RPC_RESPONSE_PROTO_SCHEMA = "syntax =\"proto3\";\n" + + "package rpc;\n" + + "\n" + + "message RpcResponseMsg {\n" + + " string payload = 1;\n" + + "}"; + + protected static final String DEVICE_RPC_REQUEST_PROTO_SCHEMA = "syntax =\"proto3\";\n" + + "package rpc;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " string method = 1;\n" + + " int32 requestId = 2;\n" + + " string params = 3;\n" + + "}"; + protected Tenant savedTenant; protected User tenantAdmin; diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java index ff64d70423..83023e21b9 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java @@ -53,7 +53,7 @@ import static org.junit.Assert.assertNotNull; public abstract class AbstractCoapIntegrationTest extends AbstractTransportIntegrationTest { protected void processBeforeTest(String deviceName, CoapDeviceType coapDeviceType, TransportPayloadType payloadType) throws Exception { - this.processBeforeTest(deviceName, coapDeviceType, payloadType, null, null, DeviceProfileProvisionType.DISABLED, null, null); + this.processBeforeTest(deviceName, coapDeviceType, payloadType, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); } protected void processBeforeTest(String deviceName, @@ -61,8 +61,11 @@ public abstract class AbstractCoapIntegrationTest extends AbstractTransportInteg TransportPayloadType payloadType, String telemetryProtoSchema, String attributesProtoSchema, - DeviceProfileProvisionType provisionType, - String provisionKey, String provisionSecret + String rpcResponseProtoSchema, + String rpcRequestProtoSchema, + String provisionKey, + String provisionSecret, + DeviceProfileProvisionType provisionType ) throws Exception { loginSysAdmin(); @@ -85,7 +88,7 @@ public abstract class AbstractCoapIntegrationTest extends AbstractTransportInteg device.setType("default"); if (coapDeviceType != null) { - DeviceProfile coapDeviceProfile = createCoapDeviceProfile(payloadType, coapDeviceType, attributesProtoSchema, provisionType, provisionKey, provisionSecret, telemetryProtoSchema); + DeviceProfile coapDeviceProfile = createCoapDeviceProfile(payloadType, coapDeviceType, provisionSecret, provisionType, provisionKey, attributesProtoSchema, telemetryProtoSchema, rpcResponseProtoSchema, rpcRequestProtoSchema); deviceProfile = doPost("/api/deviceProfile", coapDeviceProfile, DeviceProfile.class); device.setType(deviceProfile.getName()); device.setDeviceProfileId(deviceProfile.getId()); @@ -103,8 +106,9 @@ public abstract class AbstractCoapIntegrationTest extends AbstractTransportInteg } protected DeviceProfile createCoapDeviceProfile(TransportPayloadType transportPayloadType, CoapDeviceType coapDeviceType, - String attributesProtoSchema, DeviceProfileProvisionType provisionType, - String provisionKey, String provisionSecret, String telemetryProtoSchema) { + String provisionSecret, DeviceProfileProvisionType provisionType, + String provisionKey, String attributesProtoSchema, + String telemetryProtoSchema, String rpcResponseProtoSchema, String rpcRequestProtoSchema) { DeviceProfile deviceProfile = new DeviceProfile(); deviceProfile.setName(transportPayloadType.name()); deviceProfile.setType(DeviceProfileType.DEFAULT); @@ -127,8 +131,16 @@ public abstract class AbstractCoapIntegrationTest extends AbstractTransportInteg if (StringUtils.isEmpty(attributesProtoSchema)) { attributesProtoSchema = DEVICE_ATTRIBUTES_PROTO_SCHEMA; } + if (StringUtils.isEmpty(rpcResponseProtoSchema)) { + rpcResponseProtoSchema = DEVICE_RPC_RESPONSE_PROTO_SCHEMA; + } + if (StringUtils.isEmpty(rpcRequestProtoSchema)) { + rpcRequestProtoSchema = DEVICE_RPC_REQUEST_PROTO_SCHEMA; + } protoTransportPayloadConfiguration.setDeviceTelemetryProtoSchema(telemetryProtoSchema); protoTransportPayloadConfiguration.setDeviceAttributesProtoSchema(attributesProtoSchema); + protoTransportPayloadConfiguration.setDeviceRpcResponseProtoSchema(rpcResponseProtoSchema); + protoTransportPayloadConfiguration.setDeviceRpcRequestProtoSchema(rpcRequestProtoSchema); transportPayloadTypeConfiguration = protoTransportPayloadConfiguration; } else { transportPayloadTypeConfiguration = new JsonTransportPayloadConfiguration(); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/request/AbstractCoapAttributesRequestProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/request/AbstractCoapAttributesRequestProtoIntegrationTest.java index 4fbe103430..5bef44434e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/request/AbstractCoapAttributesRequestProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/request/AbstractCoapAttributesRequestProtoIntegrationTest.java @@ -79,7 +79,7 @@ public abstract class AbstractCoapAttributesRequestProtoIntegrationTest extends @Test public void testRequestAttributesValuesFromTheServer() throws Exception { super.processBeforeTest("Test Request attribute values from the server proto", CoapDeviceType.DEFAULT, - TransportPayloadType.PROTOBUF, null, ATTRIBUTES_SCHEMA_STR, DeviceProfileProvisionType.DISABLED, null, null); + TransportPayloadType.PROTOBUF, null, ATTRIBUTES_SCHEMA_STR, null, null, null, null, DeviceProfileProvisionType.DISABLED); processTestRequestAttributesValuesFromTheServer(); } diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/provision/AbstractCoapProvisionJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/provision/AbstractCoapProvisionJsonDeviceTest.java index b4a55ae271..caecaea8ec 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/provision/AbstractCoapProvisionJsonDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/provision/AbstractCoapProvisionJsonDeviceTest.java @@ -88,7 +88,7 @@ public abstract class AbstractCoapProvisionJsonDeviceTest extends AbstractCoapIn private void processTestProvisioningDisabledDevice() throws Exception { - super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.DISABLED, null, null); + super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); byte[] result = createCoapClientAndPublish().getPayload(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString()); @@ -97,7 +97,7 @@ public abstract class AbstractCoapProvisionJsonDeviceTest extends AbstractCoapIn private void processTestProvisioningCreateNewDeviceWithoutCredentials() throws Exception { - super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); byte[] result = createCoapClientAndPublish().getPayload(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -113,7 +113,7 @@ public abstract class AbstractCoapProvisionJsonDeviceTest extends AbstractCoapIn private void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception { - super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); String requestCredentials = ",\"credentialsType\": \"ACCESS_TOKEN\",\"token\": \"test_token\""; byte[] result = createCoapClientAndPublish(requestCredentials).getPayload(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -132,7 +132,7 @@ public abstract class AbstractCoapProvisionJsonDeviceTest extends AbstractCoapIn private void processTestProvisioningCreateNewDeviceWithCert() throws Exception { - super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); String requestCredentials = ",\"credentialsType\": \"X509_CERTIFICATE\",\"hash\": \"testHash\""; byte[] result = createCoapClientAndPublish(requestCredentials).getPayload(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -156,7 +156,7 @@ public abstract class AbstractCoapProvisionJsonDeviceTest extends AbstractCoapIn } private void processTestProvisioningCheckPreProvisionedDevice() throws Exception { - super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); byte[] result = createCoapClientAndPublish().getPayload(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -167,7 +167,7 @@ public abstract class AbstractCoapProvisionJsonDeviceTest extends AbstractCoapIn } private void processTestProvisioningWithBadKeyDevice() throws Exception { - super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKeyOrig", "testProvisionSecret"); + super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.JSON, null, null, null, null, "testProvisionKeyOrig", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); byte[] result = createCoapClientAndPublish().getPayload(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString()); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/provision/AbstractCoapProvisionProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/provision/AbstractCoapProvisionProtoDeviceTest.java index 9f1d8f1f18..682d9c7e7b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/provision/AbstractCoapProvisionProtoDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/provision/AbstractCoapProvisionProtoDeviceTest.java @@ -92,14 +92,14 @@ public abstract class AbstractCoapProvisionProtoDeviceTest extends AbstractCoapI private void processTestProvisioningDisabledDevice() throws Exception { - super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.DISABLED, null, null); + super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); ProvisionDeviceResponseMsg result = ProvisionDeviceResponseMsg.parseFrom(createCoapClientAndPublish().getPayload()); Assert.assertNotNull(result); Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), result.getStatus().toString()); } private void processTestProvisioningCreateNewDeviceWithoutCredentials() throws Exception { - super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createCoapClientAndPublish().getPayload()); Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device"); @@ -113,7 +113,7 @@ public abstract class AbstractCoapProvisionProtoDeviceTest extends AbstractCoapI } private void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception { - super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateDeviceTokenRequestMsg(ValidateDeviceTokenRequestMsg.newBuilder().setToken("test_token").build()).build(); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createCoapClientAndPublish(createTestsProvisionMessage(CredentialsType.ACCESS_TOKEN, requestCredentials)).getPayload()); @@ -131,7 +131,7 @@ public abstract class AbstractCoapProvisionProtoDeviceTest extends AbstractCoapI } private void processTestProvisioningCreateNewDeviceWithCert() throws Exception { - super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateDeviceX509CertRequestMsg(ValidateDeviceX509CertRequestMsg.newBuilder().setHash("testHash").build()).build(); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createCoapClientAndPublish(createTestsProvisionMessage(CredentialsType.X509_CERTIFICATE, requestCredentials)).getPayload()); @@ -155,7 +155,7 @@ public abstract class AbstractCoapProvisionProtoDeviceTest extends AbstractCoapI } private void processTestProvisioningCheckPreProvisionedDevice() throws Exception { - super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createCoapClientAndPublish().getPayload()); DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), savedDevice.getId()); @@ -165,7 +165,7 @@ public abstract class AbstractCoapProvisionProtoDeviceTest extends AbstractCoapI } private void processTestProvisioningWithBadKeyDevice() throws Exception { - super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKeyOrig", "testProvisionSecret"); + super.processBeforeTest("Test Provision device", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, null, null, "testProvisionKeyOrig", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createCoapClientAndPublish().getPayload()); Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.getStatus().toString()); } diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java index a36660267e..d5d56db63b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java @@ -124,7 +124,7 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC return COAP_BASE_URL + token + "/" + FeatureType.RPC.name().toLowerCase() + "/" + requestId; } - private class TestCoapCallback implements CoapHandler { + protected class TestCoapCallback implements CoapHandler { private final CoapClient client; private final CountDownLatch latch; @@ -136,7 +136,7 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC private Integer observe; - private TestCoapCallback(CoapClient client, CountDownLatch latch, boolean isOneWayRpc) { + TestCoapCallback(CoapClient client, CountDownLatch latch, boolean isOneWayRpc) { this.client = client; this.latch = latch; this.isOneWayRpc = isOneWayRpc; @@ -144,7 +144,7 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC @Override public void onLoad(CoapResponse response) { - log.warn("coap response: {}, {}", response, response.getCode()); + log.warn("coap response: {}, {}", response.getResponseText(), response.getCode()); assertNotNull(response.getPayload()); assertEquals(response.getCode(), CoAP.ResponseCode.CONTENT); observe = response.getOptions().getObserve(); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcJsonIntegrationTest.java index 5387d3c7bf..cecdb1fae0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcJsonIntegrationTest.java @@ -36,12 +36,12 @@ public abstract class AbstractCoapServerSideRpcJsonIntegrationTest extends Abstr } @Test - public void testServerMqttOneWayRpc() throws Exception { + public void testServerCoapOneWayRpc() throws Exception { processOneWayRpcTest(); } @Test - public void testServerMqttTwoWayRpc() throws Exception { + public void testServerCoapTwoWayRpc() throws Exception { processTwoWayRpcTest(); } diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcProtoIntegrationTest.java index edbdf69848..706a560024 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcProtoIntegrationTest.java @@ -15,26 +15,62 @@ */ package org.thingsboard.server.transport.coap.rpc; +import com.github.os72.protobuf.dynamic.DynamicSchema; +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import com.squareup.wire.schema.internal.parser.ProtoFileElement; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapHandler; +import org.eclipse.californium.core.CoapObserveRelation; import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.eclipse.californium.core.coap.Request; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.CoapDeviceType; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.TransportPayloadType; -import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.CoapDeviceTypeConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultCoapDeviceTypeConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; +import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; +import org.thingsboard.server.common.msg.session.FeatureType; +import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Slf4j public abstract class AbstractCoapServerSideRpcProtoIntegrationTest extends AbstractCoapServerSideRpcIntegrationTest { + private static final String RPC_REQUEST_PROTO_SCHEMA = "syntax =\"proto3\";\n" + + "package rpc;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " string method = 1;\n" + + " int32 requestId = 2;\n" + + " Params params = 3;\n" + + "\n" + + " message Params {\n" + + " string pin = 1;\n" + + " int32 value = 2;\n" + + " }\n" + + "}"; + @Before public void beforeTest() throws Exception { - processBeforeTest("RPC test device", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF); + processBeforeTest("RPC test device", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, null, null, null, RPC_REQUEST_PROTO_SCHEMA, null, null, DeviceProfileProvisionType.DISABLED); } @After @@ -43,33 +79,91 @@ public abstract class AbstractCoapServerSideRpcProtoIntegrationTest extends Abst } @Test - public void testServerMqttOneWayRpc() throws Exception { + public void testServerCoapOneWayRpc() throws Exception { processOneWayRpcTest(); } @Test - public void testServerMqttTwoWayRpc() throws Exception { + public void testServerCoapTwoWayRpc() throws Exception { processTwoWayRpcTest(); } + protected void processTwoWayRpcTest() throws Exception { + CoapClient client = getCoapClient(FeatureType.RPC); + client.useCONs(); + + CountDownLatch latch = new CountDownLatch(1); + TestCoapCallback testCoapCallback = new TestCoapCallback(client, latch, false); + + Request request = Request.newGet().setObserve(); + request.setType(CoAP.Type.CON); + CoapObserveRelation observeRelation = client.observe(request, testCoapCallback); + + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; + String deviceId = savedDevice.getId().getId().toString(); + + String expected = "{\"payload\":\"{\\\"value1\\\":\\\"A\\\",\\\"value2\\\":\\\"B\\\"}\"}"; + + String result = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); + latch.await(3, TimeUnit.SECONDS); + + assertEquals(expected, result); + assertEquals(0, testCoapCallback.getObserve().intValue()); + observeRelation.proactiveCancel(); + assertTrue(observeRelation.isCanceled()); + } + @Override protected void processOnLoadResponse(CoapResponse response, CoapClient client, Integer observe, CountDownLatch latch) { client.setURI(getRpcResponseFeatureTokenUrl(accessToken, observe)); - TransportProtos.ToDeviceRpcResponseMsg toDeviceRpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() - .setPayload(DEVICE_RESPONSE) - .setRequestId(observe) - .build(); - client.post(new CoapHandler() { - @Override - public void onLoad(CoapResponse response) { - log.warn("Command Response Ack: {}, {}", response.getCode(), response.getResponseText()); - latch.countDown(); - } + ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = getProtoTransportPayloadConfiguration(); + ProtoFileElement rpcRequestProtoSchemaFile = protoTransportPayloadConfiguration.getTransportProtoSchema(RPC_REQUEST_PROTO_SCHEMA); + DynamicSchema rpcRequestProtoSchema = protoTransportPayloadConfiguration.getDynamicSchema(rpcRequestProtoSchemaFile, ProtoTransportPayloadConfiguration.RPC_REQUEST_PROTO_SCHEMA); - @Override - public void onError() { - log.warn("Command Response Ack Error, No connect"); + byte[] requestPayload = response.getPayload(); + DynamicMessage.Builder rpcRequestMsg = rpcRequestProtoSchema.newMessageBuilder("RpcRequestMsg"); + Descriptors.Descriptor rpcRequestMsgDescriptor = rpcRequestMsg.getDescriptorForType(); + assertNotNull(rpcRequestMsgDescriptor); + try { + DynamicMessage dynamicMessage = DynamicMessage.parseFrom(rpcRequestMsgDescriptor, requestPayload); + List fields = rpcRequestMsgDescriptor.getFields(); + for (Descriptors.FieldDescriptor fieldDescriptor: fields) { + assertTrue(dynamicMessage.hasField(fieldDescriptor)); } - }, toDeviceRpcResponseMsg.toByteArray(), MediaTypeRegistry.APPLICATION_JSON); + ProtoFileElement rpcResponseProtoSchemaFile = protoTransportPayloadConfiguration.getTransportProtoSchema(DEVICE_RPC_RESPONSE_PROTO_SCHEMA); + DynamicSchema rpcResponseProtoSchema = protoTransportPayloadConfiguration.getDynamicSchema(rpcResponseProtoSchemaFile, ProtoTransportPayloadConfiguration.RPC_RESPONSE_PROTO_SCHEMA); + DynamicMessage.Builder rpcResponseBuilder = rpcResponseProtoSchema.newMessageBuilder("RpcResponseMsg"); + Descriptors.Descriptor rpcResponseMsgDescriptor = rpcResponseBuilder.getDescriptorForType(); + assertNotNull(rpcResponseMsgDescriptor); + DynamicMessage rpcResponseMsg = rpcResponseBuilder + .setField(rpcResponseMsgDescriptor.findFieldByName("payload"), DEVICE_RESPONSE) + .build(); + client.post(new CoapHandler() { + @Override + public void onLoad(CoapResponse response) { + log.warn("Command Response Ack: {}, {}", response.getCode(), response.getResponseText()); + latch.countDown(); + } + + @Override + public void onError() { + log.warn("Command Response Ack Error, No connect"); + } + }, rpcResponseMsg.toByteArray(), MediaTypeRegistry.APPLICATION_JSON); + } catch (InvalidProtocolBufferException e) { + log.warn("Command Response Ack Error, Invalid response received: ", e); + } + } + + private ProtoTransportPayloadConfiguration getProtoTransportPayloadConfiguration() { + DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); + assertTrue(transportConfiguration instanceof CoapDeviceProfileTransportConfiguration); + CoapDeviceProfileTransportConfiguration coapDeviceProfileTransportConfiguration = (CoapDeviceProfileTransportConfiguration) transportConfiguration; + CoapDeviceTypeConfiguration coapDeviceTypeConfiguration = coapDeviceProfileTransportConfiguration.getCoapDeviceTypeConfiguration(); + assertTrue(coapDeviceTypeConfiguration instanceof DefaultCoapDeviceTypeConfiguration); + DefaultCoapDeviceTypeConfiguration defaultCoapDeviceTypeConfiguration = (DefaultCoapDeviceTypeConfiguration) coapDeviceTypeConfiguration; + TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = defaultCoapDeviceTypeConfiguration.getTransportPayloadTypeConfiguration(); + assertTrue(transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration); + return (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; } } diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/telemetry/timeseries/AbstractCoapTimeseriesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/telemetry/timeseries/AbstractCoapTimeseriesProtoIntegrationTest.java index 2ae686b211..0cc3db9cf1 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/telemetry/timeseries/AbstractCoapTimeseriesProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/telemetry/timeseries/AbstractCoapTimeseriesProtoIntegrationTest.java @@ -114,7 +114,7 @@ public abstract class AbstractCoapTimeseriesProtoIntegrationTest extends Abstrac " }\n" + " }\n" + "}"; - super.processBeforeTest("Test Post Telemetry device proto payload", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, schemaStr, null, DeviceProfileProvisionType.DISABLED, null, null); + super.processBeforeTest("Test Post Telemetry device proto payload", CoapDeviceType.DEFAULT, TransportPayloadType.PROTOBUF, schemaStr, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); assertTrue(transportConfiguration instanceof CoapDeviceProfileTransportConfiguration); CoapDeviceProfileTransportConfiguration coapDeviceProfileTransportConfiguration = (CoapDeviceProfileTransportConfiguration) transportConfiguration; diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java index cd074b3197..0d11c9a773 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java @@ -62,7 +62,7 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg protected DeviceProfile deviceProfile; protected void processBeforeTest (String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) throws Exception { - this.processBeforeTest(deviceName, gatewayName, payloadType, telemetryTopic, attributesTopic, null, null, DeviceProfileProvisionType.DISABLED, null, null); + this.processBeforeTest(deviceName, gatewayName, payloadType, telemetryTopic, attributesTopic, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); } protected void processBeforeTest(String deviceName, @@ -72,9 +72,12 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg String attributesTopic, String telemetryProtoSchema, String attributesProtoSchema, - DeviceProfileProvisionType provisionType, - String provisionKey, String provisionSecret - ) throws Exception { + String rpcResponseProtoSchema, + String rpcRequestProtoSchema, + String provisionKey, + String provisionSecret, + DeviceProfileProvisionType provisionType + ) throws Exception { loginSysAdmin(); Tenant tenant = new Tenant(); @@ -103,7 +106,7 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg gateway.setAdditionalInfo(additionalInfo); if (payloadType != null) { - DeviceProfile mqttDeviceProfile = createMqttDeviceProfile(payloadType, telemetryTopic, attributesTopic, telemetryProtoSchema, attributesProtoSchema, provisionType, provisionKey, provisionSecret); + DeviceProfile mqttDeviceProfile = createMqttDeviceProfile(payloadType, telemetryTopic, attributesTopic, telemetryProtoSchema, attributesProtoSchema, rpcResponseProtoSchema, rpcRequestProtoSchema, provisionKey, provisionSecret, provisionType); deviceProfile = doPost("/api/deviceProfile", mqttDeviceProfile, DeviceProfile.class); device.setType(deviceProfile.getName()); device.setDeviceProfileId(deviceProfile.getId()); @@ -157,8 +160,9 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg protected DeviceProfile createMqttDeviceProfile(TransportPayloadType transportPayloadType, String telemetryTopic, String attributesTopic, String telemetryProtoSchema, String attributesProtoSchema, - DeviceProfileProvisionType provisionType, - String provisionKey, String provisionSecret) { + String rpcResponseProtoSchema, String rpcRequestProtoSchema, + String provisionKey, String provisionSecret, + DeviceProfileProvisionType provisionType) { DeviceProfile deviceProfile = new DeviceProfile(); deviceProfile.setName(transportPayloadType.name()); deviceProfile.setType(DeviceProfileType.DEFAULT); @@ -186,8 +190,16 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg if (StringUtils.isEmpty(attributesProtoSchema)) { attributesProtoSchema = DEVICE_ATTRIBUTES_PROTO_SCHEMA; } + if (StringUtils.isEmpty(rpcResponseProtoSchema)) { + rpcResponseProtoSchema = DEVICE_RPC_RESPONSE_PROTO_SCHEMA; + } + if (StringUtils.isEmpty(rpcRequestProtoSchema)) { + rpcRequestProtoSchema = DEVICE_RPC_REQUEST_PROTO_SCHEMA; + } protoTransportPayloadConfiguration.setDeviceTelemetryProtoSchema(telemetryProtoSchema); protoTransportPayloadConfiguration.setDeviceAttributesProtoSchema(attributesProtoSchema); + protoTransportPayloadConfiguration.setDeviceRpcResponseProtoSchema(rpcResponseProtoSchema); + protoTransportPayloadConfiguration.setDeviceRpcRequestProtoSchema(rpcRequestProtoSchema); transportPayloadTypeConfiguration = protoTransportPayloadConfiguration; } mqttDeviceProfileTransportConfiguration.setTransportPayloadTypeConfiguration(transportPayloadTypeConfiguration); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java index ff8b54c79c..6949a544de 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java @@ -83,7 +83,7 @@ public abstract class AbstractMqttAttributesRequestProtoIntegrationTest extends @Test public void testRequestAttributesValuesFromTheServer() throws Exception { super.processBeforeTest("Test Request attribute values from the server proto", "Gateway Test Request attribute values from the server proto", - TransportPayloadType.PROTOBUF, null, null, null, ATTRIBUTES_SCHEMA_STR, DeviceProfileProvisionType.DISABLED, null, null); + TransportPayloadType.PROTOBUF, null, null, null, ATTRIBUTES_SCHEMA_STR, null, null, null, null, DeviceProfileProvisionType.DISABLED); processTestRequestAttributesValuesFromTheServer(); } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java index 106c801676..870b62b195 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java @@ -94,7 +94,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn protected void processTestProvisioningDisabledDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, DeviceProfileProvisionType.DISABLED, null, null); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); byte[] result = createMqttClientAndPublish().getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString()); @@ -103,7 +103,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn protected void processTestProvisioningCreateNewDeviceWithoutCredentials() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); byte[] result = createMqttClientAndPublish().getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -119,7 +119,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn protected void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); String requestCredentials = ",\"credentialsType\": \"ACCESS_TOKEN\",\"token\": \"test_token\""; byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -138,7 +138,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn protected void processTestProvisioningCreateNewDeviceWithCert() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); String requestCredentials = ",\"credentialsType\": \"X509_CERTIFICATE\",\"hash\": \"testHash\""; byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -163,7 +163,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn protected void processTestProvisioningCreateNewDeviceWithMqttBasic() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); String requestCredentials = ",\"credentialsType\": \"MQTT_BASIC\",\"clientId\": \"test_clientId\",\"username\": \"test_username\",\"password\": \"test_password\""; byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -188,7 +188,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn } protected void processTestProvisioningCheckPreProvisionedDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); byte[] result = createMqttClientAndPublish().getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -199,7 +199,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn } protected void processTestProvisioningWithBadKeyDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKeyOrig", "testProvisionSecret"); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKeyOrig", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); byte[] result = createMqttClientAndPublish().getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString()); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java index 53fde9412b..97ebbfe4a7 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java @@ -101,14 +101,14 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI protected void processTestProvisioningDisabledDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, DeviceProfileProvisionType.DISABLED, null, null); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); ProvisionDeviceResponseMsg result = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); Assert.assertNotNull(result); Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), result.getStatus().toString()); } protected void processTestProvisioningCreateNewDeviceWithoutCredentials() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device"); @@ -122,7 +122,7 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI } protected void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null,null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null,null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateDeviceTokenRequestMsg(ValidateDeviceTokenRequestMsg.newBuilder().setToken("test_token").build()).build(); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.ACCESS_TOKEN, requestCredentials)).getPayloadBytes()); @@ -140,7 +140,7 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI } protected void processTestProvisioningCreateNewDeviceWithCert() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateDeviceX509CertRequestMsg(ValidateDeviceX509CertRequestMsg.newBuilder().setHash("testHash").build()).build(); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.X509_CERTIFICATE, requestCredentials)).getPayloadBytes()); @@ -164,7 +164,7 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI } protected void processTestProvisioningCreateNewDeviceWithMqttBasic() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateBasicMqttCredRequestMsg( ValidateBasicMqttCredRequestMsg.newBuilder() .setClientId("test_clientId") @@ -195,7 +195,7 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI } protected void processTestProvisioningCheckPreProvisionedDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKey", "testProvisionSecret"); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), savedDevice.getId()); @@ -205,7 +205,7 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI } protected void processTestProvisioningWithBadKeyDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, "testProvisionKeyOrig", "testProvisionSecret"); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKeyOrig", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.getStatus().toString()); } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java index 90de461627..f24cb0c2bc 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java @@ -180,7 +180,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM return message; } - private class TestMqttCallback implements MqttCallback { + protected class TestMqttCallback implements MqttCallback { private final MqttAsyncClient client; private final CountDownLatch latch; diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java index 575dfdb052..eea800fb0e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java @@ -15,25 +15,60 @@ */ package org.thingsboard.server.transport.mqtt.rpc; +import com.github.os72.protobuf.dynamic.DynamicSchema; +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; import com.google.protobuf.InvalidProtocolBufferException; +import com.squareup.wire.schema.internal.parser.ProtoFileElement; import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.CoapHandler; +import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.coap.MediaTypeRegistry; import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.jetbrains.annotations.NotNull; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; +import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.gen.transport.TransportApiProtos; -import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Slf4j public abstract class AbstractMqttServerSideRpcProtoIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { + private static final String RPC_REQUEST_PROTO_SCHEMA = "syntax =\"proto3\";\n" + + "package rpc;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " string method = 1;\n" + + " int32 requestId = 2;\n" + + " Params params = 3;\n" + + "\n" + + " message Params {\n" + + " string pin = 1;\n" + + " int32 value = 2;\n" + + " }\n" + + "}"; + @Before public void beforeTest() throws Exception { - processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null); + processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, RPC_REQUEST_PROTO_SCHEMA, null, null, DeviceProfileProvisionType.DISABLED); } @After @@ -83,14 +118,55 @@ public abstract class AbstractMqttServerSideRpcProtoIntegrationTest extends Abst return builder.build(); } + protected void processTwoWayRpcTest() throws Exception { + MqttAsyncClient client = getMqttAsyncClient(accessToken); + client.subscribe(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC, 1); + + CountDownLatch latch = new CountDownLatch(1); + TestMqttCallback callback = new TestMqttCallback(client, latch); + client.setCallback(callback); + + Thread.sleep(1000); + + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; + String deviceId = savedDevice.getId().getId().toString(); + + String result = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); + String expected = "{\"payload\":\"{\\\"value1\\\":\\\"A\\\",\\\"value2\\\":\\\"B\\\"}\"}"; + latch.await(3, TimeUnit.SECONDS); + Assert.assertEquals(expected, result); + } + protected MqttMessage processMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException { MqttMessage message = new MqttMessage(); if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC)) { - TransportProtos.ToDeviceRpcResponseMsg toDeviceRpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() - .setPayload(DEVICE_RESPONSE) - .setRequestId(0) + ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = getProtoTransportPayloadConfiguration(); + ProtoFileElement rpcRequestProtoSchemaFile = protoTransportPayloadConfiguration.getTransportProtoSchema(RPC_REQUEST_PROTO_SCHEMA); + DynamicSchema rpcRequestProtoSchema = protoTransportPayloadConfiguration.getDynamicSchema(rpcRequestProtoSchemaFile, ProtoTransportPayloadConfiguration.RPC_REQUEST_PROTO_SCHEMA); + + byte[] requestPayload = mqttMessage.getPayload(); + DynamicMessage.Builder rpcRequestMsg = rpcRequestProtoSchema.newMessageBuilder("RpcRequestMsg"); + Descriptors.Descriptor rpcRequestMsgDescriptor = rpcRequestMsg.getDescriptorForType(); + assertNotNull(rpcRequestMsgDescriptor); + try { + DynamicMessage dynamicMessage = DynamicMessage.parseFrom(rpcRequestMsgDescriptor, requestPayload); + List fields = rpcRequestMsgDescriptor.getFields(); + for (Descriptors.FieldDescriptor fieldDescriptor: fields) { + assertTrue(dynamicMessage.hasField(fieldDescriptor)); + } + ProtoFileElement transportProtoSchemaFile = protoTransportPayloadConfiguration.getTransportProtoSchema(DEVICE_RPC_RESPONSE_PROTO_SCHEMA); + DynamicSchema rpcResponseProtoSchema = protoTransportPayloadConfiguration.getDynamicSchema(transportProtoSchemaFile, ProtoTransportPayloadConfiguration.RPC_RESPONSE_PROTO_SCHEMA); + + DynamicMessage.Builder rpcResponseBuilder = rpcResponseProtoSchema.newMessageBuilder("RpcResponseMsg"); + Descriptors.Descriptor rpcResponseMsgDescriptor = rpcResponseBuilder.getDescriptorForType(); + assertNotNull(rpcResponseMsgDescriptor); + DynamicMessage rpcResponseMsg = rpcResponseBuilder + .setField(rpcResponseMsgDescriptor.findFieldByName("payload"), DEVICE_RESPONSE) .build(); - message.setPayload(toDeviceRpcResponseMsg.toByteArray()); + message.setPayload(rpcResponseMsg.toByteArray()); + } catch (InvalidProtocolBufferException e) { + log.warn("Command Response Ack Error, Invalid response received: ", e); + } } else { TransportApiProtos.GatewayDeviceRpcRequestMsg msg = TransportApiProtos.GatewayDeviceRpcRequestMsg.parseFrom(mqttMessage.getPayload()); String deviceName = msg.getDeviceName(); @@ -105,6 +181,14 @@ public abstract class AbstractMqttServerSideRpcProtoIntegrationTest extends Abst return message; } + private ProtoTransportPayloadConfiguration getProtoTransportPayloadConfiguration() { + DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); + assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); + MqttDeviceProfileTransportConfiguration mqttTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; + TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttTransportConfiguration.getTransportPayloadTypeConfiguration(); + assertTrue(transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration); + return (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java index 93a004318d..6140b7d8ed 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java @@ -119,7 +119,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac " }\n" + " }\n" + "}"; - super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null, schemaStr, null, DeviceProfileProvisionType.DISABLED, null, null); + super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null, schemaStr, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); @@ -172,7 +172,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac @Test public void testPushMqttTelemetryGateway() throws Exception { - super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, null, null, null, null, DeviceProfileProvisionType.DISABLED, null, null); + super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); TransportApiProtos.GatewayTelemetryMsg.Builder gatewayTelemetryMsgProtoBuilder = TransportApiProtos.GatewayTelemetryMsg.newBuilder(); List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); String deviceName1 = "Device A"; @@ -186,7 +186,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac @Test public void testGatewayConnect() throws Exception { - super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null, null, null, DeviceProfileProvisionType.DISABLED, null, null); + super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); String deviceName = "Device A"; TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName); MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml new file mode 100644 index 0000000000..8adbe4329a --- /dev/null +++ b/common/coap-server/pom.xml @@ -0,0 +1,73 @@ + + + + 4.0.0 + + org.thingsboard + 3.3.0-SNAPSHOT + common + + org.thingsboard.common + coap-server + jar + + Thingsboard CoAP server + https://thingsboard.io + + + UTF-8 + ${basedir}/../.. + + + + + org.thingsboard.common + queue + + + org.thingsboard.common + data + + + org.thingsboard.common.transport + transport-api + + + org.springframework + spring-context + + + org.springframework.boot + spring-boot-starter-web + provided + + + org.eclipse.californium + californium-core + + + org.eclipse.californium + scandium + + + + + \ No newline at end of file diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerContext.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerContext.java new file mode 100644 index 0000000000..4129108ba3 --- /dev/null +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerContext.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.coapserver; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; + +@Slf4j +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')") +@Component +public class CoapServerContext { + + @Getter + @Value("${transport.coap.bind_address}") + private String host; + + @Getter + @Value("${transport.coap.bind_port}") + private Integer port; + + @Getter + @Value("${transport.coap.timeout}") + private Long timeout; + + @Getter + @Autowired(required = false) + private TbCoapDtlsSettings dtlsSettings; + +} diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerService.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerService.java new file mode 100644 index 0000000000..f8b3ffefc2 --- /dev/null +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerService.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.coapserver; + +import org.eclipse.californium.core.CoapServer; + +import java.net.UnknownHostException; +import java.util.concurrent.ConcurrentMap; + +public interface CoapServerService { + + CoapServer getCoapServer() throws UnknownHostException; + + ConcurrentMap getDtlsSessionsMap(); + + long getTimeout(); + +} diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java new file mode 100644 index 0000000000..aebac1a86d --- /dev/null +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java @@ -0,0 +1,133 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.coapserver; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.CoapServer; +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.core.network.config.NetworkConfig; +import org.eclipse.californium.core.server.resources.Resource; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.Random; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')") +public class DefaultCoapServerService implements CoapServerService { + + @Autowired + private CoapServerContext coapServerContext; + + private CoapServer server; + + private TbCoapDtlsCertificateVerifier tbDtlsCertificateVerifier; + + private ScheduledExecutorService dtlsSessionsExecutor; + + @PostConstruct + public void init() throws UnknownHostException { + createCoapServer(); + } + + @PreDestroy + public void shutdown() { + if (dtlsSessionsExecutor != null) { + dtlsSessionsExecutor.shutdownNow(); + } + log.info("Stopping CoAP server!"); + server.destroy(); + log.info("CoAP server stopped!"); + } + + @Override + public CoapServer getCoapServer() throws UnknownHostException { + if (server != null) { + return server; + } else { + return createCoapServer(); + } + } + + @Override + public ConcurrentMap getDtlsSessionsMap() { + return tbDtlsCertificateVerifier != null ? tbDtlsCertificateVerifier.getTbCoapDtlsSessionIdsMap() : null; + } + + @Override + public long getTimeout() { + return coapServerContext.getTimeout(); + } + + private CoapServer createCoapServer() throws UnknownHostException { + server = new CoapServer(); + + CoapEndpoint.Builder noSecCoapEndpointBuilder = new CoapEndpoint.Builder(); + InetAddress addr = InetAddress.getByName(coapServerContext.getHost()); + InetSocketAddress sockAddr = new InetSocketAddress(addr, coapServerContext.getPort()); + noSecCoapEndpointBuilder.setInetSocketAddress(sockAddr); + noSecCoapEndpointBuilder.setNetworkConfig(NetworkConfig.getStandard()); + CoapEndpoint noSecCoapEndpoint = noSecCoapEndpointBuilder.build(); + server.addEndpoint(noSecCoapEndpoint); + + if (isDtlsEnabled()) { + CoapEndpoint.Builder dtlsCoapEndpointBuilder = new CoapEndpoint.Builder(); + TbCoapDtlsSettings dtlsSettings = coapServerContext.getDtlsSettings(); + DtlsConnectorConfig dtlsConnectorConfig = dtlsSettings.dtlsConnectorConfig(); + DTLSConnector connector = new DTLSConnector(dtlsConnectorConfig); + dtlsCoapEndpointBuilder.setConnector(connector); + CoapEndpoint dtlsCoapEndpoint = dtlsCoapEndpointBuilder.build(); + server.addEndpoint(dtlsCoapEndpoint); + if (dtlsConnectorConfig.isClientAuthenticationRequired()) { + tbDtlsCertificateVerifier = (TbCoapDtlsCertificateVerifier) dtlsConnectorConfig.getAdvancedCertificateVerifier(); + dtlsSessionsExecutor = Executors.newSingleThreadScheduledExecutor(); + dtlsSessionsExecutor.scheduleAtFixedRate(this::evictTimeoutSessions, new Random().nextInt((int) getDtlsSessionReportTimeout()), getDtlsSessionReportTimeout(), TimeUnit.MILLISECONDS); + } + } + Resource root = server.getRoot(); + TbCoapServerMessageDeliverer messageDeliverer = new TbCoapServerMessageDeliverer(root); + server.setMessageDeliverer(messageDeliverer); + + server.start(); + return server; + } + + private boolean isDtlsEnabled() { + return coapServerContext.getDtlsSettings() != null; + } + + private void evictTimeoutSessions() { + tbDtlsCertificateVerifier.evictTimeoutSessions(); + } + + private long getDtlsSessionReportTimeout() { + return tbDtlsCertificateVerifier.getDtlsSessionReportTimeout(); + } + +} diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsCertificateVerifier.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsCertificateVerifier.java similarity index 99% rename from common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsCertificateVerifier.java rename to common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsCertificateVerifier.java index a94ed6caeb..99a35d4831 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsCertificateVerifier.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsCertificateVerifier.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.transport.coap; +package org.thingsboard.server.coapserver; import lombok.Data; import lombok.extern.slf4j.Slf4j; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSessionInMemoryStorage.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInMemoryStorage.java similarity index 97% rename from common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSessionInMemoryStorage.java rename to common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInMemoryStorage.java index d7dd9c1829..618a10a1eb 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSessionInMemoryStorage.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInMemoryStorage.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.transport.coap; +package org.thingsboard.server.coapserver; import lombok.Data; import lombok.extern.slf4j.Slf4j; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSessionInfo.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInfo.java similarity index 96% rename from common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSessionInfo.java rename to common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInfo.java index 452c5eb792..893ca38a5c 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSessionInfo.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInfo.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.transport.coap; +package org.thingsboard.server.coapserver; import lombok.Data; import org.thingsboard.server.common.data.DeviceProfile; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSettings.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java similarity index 96% rename from common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSettings.java rename to common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java index d7cd0b3f39..417a78da12 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSettings.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.transport.coap; +package org.thingsboard.server.coapserver; import com.google.common.io.Resources; import lombok.extern.slf4j.Slf4j; @@ -39,15 +39,15 @@ import java.util.Collections; import java.util.Optional; @Slf4j +@ConditionalOnExpression("'${transport.coap.enabled}'=='true'") @ConditionalOnProperty(prefix = "transport.coap.dtls", value = "enabled", havingValue = "true", matchIfMissing = false) -@ConditionalOnExpression("'${transport.type:null}'=='null' || ('${transport.type}'=='local' && '${transport.coap.enabled}'=='true')") @Component public class TbCoapDtlsSettings { - @Value("${transport.coap.bind_address}") + @Value("${transport.coap.dtls.bind_address}") private String host; - @Value("${transport.coap.bind_port}") + @Value("${transport.coap.dtls.bind_port}") private Integer port; @Value("${transport.coap.dtls.mode}") diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapServerMessageDeliverer.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerMessageDeliverer.java similarity index 97% rename from common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapServerMessageDeliverer.java rename to common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerMessageDeliverer.java index fa189e35f1..307455a6a5 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapServerMessageDeliverer.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerMessageDeliverer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.transport.coap; +package org.thingsboard.server.coapserver; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.OptionSet; diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java index a0b44775f5..0280a81f1a 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java @@ -22,9 +22,11 @@ import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; 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.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import java.util.List; import java.util.Optional; @@ -74,4 +76,12 @@ public interface AssetService { ListenableFuture> findAssetsByQuery(TenantId tenantId, AssetSearchQuery query); ListenableFuture> findAssetTypesByTenantId(TenantId tenantId); + + Asset assignAssetToEdge(TenantId tenantId, AssetId assetId, EdgeId edgeId); + + Asset unassignAssetFromEdge(TenantId tenantId, AssetId assetId, EdgeId edgeId); + + PageData findAssetsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink); + + PageData findAssetsByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java index 62c7e02e2f..fc3eaf9a12 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java @@ -20,10 +20,10 @@ import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EdgeId; 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.page.TimePageLink; public interface DashboardService { @@ -53,4 +53,9 @@ public interface DashboardService { void updateCustomerDashboards(TenantId tenantId, CustomerId customerId); + Dashboard assignDashboardToEdge(TenantId tenantId, DashboardId dashboardId, EdgeId edgeId); + + Dashboard unassignDashboardFromEdge(TenantId tenantId, DashboardId dashboardId, EdgeId edgeId); + + PageData findDashboardsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java index f98c56d89a..b3cfb66a47 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; 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.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -90,4 +91,13 @@ public interface DeviceService { Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile); + Device assignDeviceToEdge(TenantId tenantId, DeviceId deviceId, EdgeId edgeId); + + Device unassignDeviceFromEdge(TenantId tenantId, DeviceId deviceId, EdgeId edgeId); + + PageData findDevicesByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink); + + PageData findDevicesByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink); + + long countByTenantId(TenantId tenantId); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java new file mode 100644 index 0000000000..e1f3203bc6 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.TimePageLink; + +public interface EdgeEventService { + + ListenableFuture saveAsync(EdgeEvent edgeEvent); + + PageData findEdgeEvents(TenantId tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate); + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java new file mode 100644 index 0000000000..a7b9145c01 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java @@ -0,0 +1,96 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeInfo; +import org.thingsboard.server.common.data.edge.EdgeSearchQuery; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleChainId; +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 java.util.List; +import java.util.Optional; + +public interface EdgeService { + + Edge findEdgeById(TenantId tenantId, EdgeId edgeId); + + EdgeInfo findEdgeInfoById(TenantId tenantId, EdgeId edgeId); + + ListenableFuture findEdgeByIdAsync(TenantId tenantId, EdgeId edgeId); + + Edge findEdgeByTenantIdAndName(TenantId tenantId, String name); + + Optional findEdgeByRoutingKey(TenantId tenantId, String routingKey); + + Edge saveEdge(Edge edge); + + Edge assignEdgeToCustomer(TenantId tenantId, EdgeId edgeId, CustomerId customerId); + + Edge unassignEdgeFromCustomer(TenantId tenantId, EdgeId edgeId); + + void deleteEdge(TenantId tenantId, EdgeId edgeId); + + PageData findEdgesByTenantId(TenantId tenantId, PageLink pageLink); + + PageData findEdgesByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink); + + PageData findEdgeInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink); + + PageData findEdgeInfosByTenantId(TenantId tenantId, PageLink pageLink); + + ListenableFuture> findEdgesByTenantIdAndIdsAsync(TenantId tenantId, List edgeIds); + + void deleteEdgesByTenantId(TenantId tenantId); + + PageData findEdgesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); + + PageData findEdgesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink); + + PageData findEdgeInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); + + PageData findEdgeInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink); + + ListenableFuture> findEdgesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List edgeIds); + + void unassignCustomerEdges(TenantId tenantId, CustomerId customerId); + + ListenableFuture> findEdgesByQuery(TenantId tenantId, EdgeSearchQuery query); + + ListenableFuture> findEdgeTypesByTenantId(TenantId tenantId); + + void assignDefaultRuleChainsToEdge(TenantId tenantId, EdgeId edgeId); + + ListenableFuture> findEdgesByTenantIdAndRuleChainId(TenantId tenantId, RuleChainId ruleChainId); + + ListenableFuture> findEdgesByTenantIdAndDashboardId(TenantId tenantId, DashboardId dashboardId); + + ListenableFuture> findRelatedEdgeIdsByEntityId(TenantId tenantId, EntityId entityId); + + Object checkInstance(Object request); + + Object activateInstance(String licenseSecret, String releaseDate); + + String findMissingToRelatedRuleChains(TenantId tenantId, EdgeId edgeId); +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java index bfa407c071..832a7822ee 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java @@ -19,14 +19,15 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; -import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; 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.page.TimePageLink; import java.util.List; @@ -76,4 +77,12 @@ public interface EntityViewService { void deleteEntityViewsByTenantId(TenantId tenantId); ListenableFuture> findEntityViewTypesByTenantId(TenantId tenantId); + + EntityView assignEntityViewToEdge(TenantId tenantId, EntityViewId entityViewId, EdgeId edgeId); + + EntityView unassignEntityViewFromEdge(TenantId tenantId, EntityViewId entityViewId, EdgeId edgeId); + + PageData findEntityViewsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink); + + PageData findEntityViewsByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java index 2bcb0d3df0..ea25568375 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.event; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.event.EventFilter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -41,6 +42,8 @@ public interface EventService { List findLatestEvents(TenantId tenantId, EntityId entityId, String eventType, int limit); + PageData findEventsByFilter(TenantId tenantId, EntityId entityId, EventFilter eventFilter, TimePageLink pageLink); + void removeEvents(TenantId tenantId, EntityId entityId); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java new file mode 100644 index 0000000000..68c2c1f5f0 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2021 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.resource; + +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.id.TbResourceId; +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 java.util.List; + +public interface ResourceService { + TbResource saveResource(TbResource resource); + + TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceId); + + TbResource findResourceById(TenantId tenantId, TbResourceId resourceId); + + TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId); + + PageData findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink); + + PageData findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink); + + List findTenantResourcesByResourceTypeAndObjectIds(TenantId tenantId, ResourceType lwm2mModel, String[] objectIds); + + PageData findTenantResourcesByResourceTypeAndPageLink(TenantId tenantId, ResourceType lwm2mModel, PageLink pageLink); + + void deleteResource(TenantId tenantId, TbResourceId resourceId); + + void deleteResourcesByTenantId(TenantId tenantId); + + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java index aff0fbc3ac..5740abb354 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java @@ -17,16 +17,19 @@ package org.thingsboard.server.dao.rule; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.EdgeId; 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.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainData; import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import java.util.List; @@ -40,7 +43,7 @@ public interface RuleChainService { boolean setRootRuleChain(TenantId tenantId, RuleChainId ruleChainId); - RuleChainMetaData saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData); + boolean saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData); RuleChainMetaData loadRuleChainMetaData(TenantId tenantId, RuleChainId ruleChainId); @@ -60,7 +63,7 @@ public interface RuleChainService { List getRuleNodeRelations(TenantId tenantId, RuleNodeId ruleNodeId); - PageData findTenantRuleChains(TenantId tenantId, PageLink pageLink); + PageData findTenantRuleChainsByType(TenantId tenantId, RuleChainType type, PageLink pageLink); void deleteRuleChainById(TenantId tenantId, RuleChainId ruleChainId); @@ -68,6 +71,22 @@ public interface RuleChainService { RuleChainData exportTenantRuleChains(TenantId tenantId, PageLink pageLink) throws ThingsboardException; - List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite); + List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, RuleChainType type, boolean overwrite); + + RuleChain assignRuleChainToEdge(TenantId tenantId, RuleChainId ruleChainId, EdgeId edgeId); + + RuleChain unassignRuleChainFromEdge(TenantId tenantId, RuleChainId ruleChainId, EdgeId edgeId, boolean remove); + + PageData findRuleChainsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink); + + RuleChain getEdgeTemplateRootRuleChain(TenantId tenantId); + + boolean setEdgeTemplateRootRuleChain(TenantId tenantId, RuleChainId ruleChainId); + + boolean setAutoAssignToEdgeRuleChain(TenantId tenantId, RuleChainId ruleChainId); + + boolean unsetAutoAssignToEdgeRuleChain(TenantId tenantId, RuleChainId ruleChainId); + + ListenableFuture> findAutoAssignToEdgeRuleChainsByTenantId(TenantId tenantId); } diff --git a/common/data/pom.xml b/common/data/pom.xml index 92efc053a9..d011fcc5a7 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -87,10 +87,6 @@ org.thingsboard protobuf-dynamic - - org.eclipse.leshan - leshan-core - diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index ba14b24292..ce4576705b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -22,6 +22,7 @@ public class CacheConstants { public static final String SESSIONS_CACHE = "sessions"; public static final String ASSET_CACHE = "assets"; public static final String ENTITY_VIEW_CACHE = "entityViews"; + public static final String EDGE_CACHE = "edges"; public static final String CLAIM_DEVICES_CACHE = "claimDevices"; public static final String SECURITY_SETTINGS_CACHE = "securitySettings"; public static final String TENANT_PROFILE_CACHE = "tenantProfiles"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java index b1699a624c..854c5ff64b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java @@ -19,13 +19,18 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.NoXss; -import java.util.*; +import javax.validation.Valid; +import java.util.HashSet; +import java.util.Set; public class DashboardInfo extends SearchTextBased implements HasName, HasTenantId { private TenantId tenantId; + @NoXss private String title; + @Valid private Set assignedCustomers; public DashboardInfo() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 4f19e1589f..e244873556 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -70,6 +70,8 @@ public class DataConstants { public static final String ENTITY_ASSIGNED_TO_TENANT = "ENTITY_ASSIGNED_TO_TENANT"; public static final String PROVISION_SUCCESS = "PROVISION_SUCCESS"; public static final String PROVISION_FAILURE = "PROVISION_FAILURE"; + public static final String ENTITY_ASSIGNED_TO_EDGE = "ENTITY_ASSIGNED_TO_EDGE"; + public static final String ENTITY_UNASSIGNED_FROM_EDGE = "ENTITY_UNASSIGNED_FROM_EDGE"; public static final String RPC_CALL_FROM_SERVER_TO_DEVICE = "RPC_CALL_FROM_SERVER_TO_DEVICE"; @@ -106,4 +108,6 @@ public class DataConstants { public static final String FIRMWARE_SIZE = "fw_size"; public static final String FIRMWARE_CHECKSUM = "fw_checksum"; public static final String FIRMWARE_CHECKSUM_ALGORITHM = "fw_checksum_algorithm"; + public static final String EDGE_MSG_SOURCE = "edge"; + public static final String MSG_SOURCE_KEY = "source"; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java new file mode 100644 index 0000000000..8b78bcab3a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2021 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; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.edge.EdgeEventType; + +@Slf4j +public final class EdgeUtils { + + private EdgeUtils() { + } + + public static EdgeEventType getEdgeEventTypeByEntityType(EntityType entityType) { + switch (entityType) { + case EDGE: + return EdgeEventType.EDGE; + case DEVICE: + return EdgeEventType.DEVICE; + case DEVICE_PROFILE: + return EdgeEventType.DEVICE_PROFILE; + case ASSET: + return EdgeEventType.ASSET; + case ENTITY_VIEW: + return EdgeEventType.ENTITY_VIEW; + case DASHBOARD: + return EdgeEventType.DASHBOARD; + case USER: + return EdgeEventType.USER; + case RULE_CHAIN: + return EdgeEventType.RULE_CHAIN; + case ALARM: + return EdgeEventType.ALARM; + case TENANT: + return EdgeEventType.TENANT; + case CUSTOMER: + return EdgeEventType.CUSTOMER; + case WIDGETS_BUNDLE: + return EdgeEventType.WIDGETS_BUNDLE; + case WIDGET_TYPE: + return EdgeEventType.WIDGET_TYPE; + default: + log.warn("Unsupported entity type [{}]", entityType); + return null; + } + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 36be053789..d3508d42dd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -19,5 +19,5 @@ package org.thingsboard.server.common.data; * @author Andrew Shvayka */ public enum EntityType { - TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, API_USAGE_STATE, TB_RESOURCE, FIRMWARE; + TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, API_USAGE_STATE, TB_RESOURCE, FIRMWARE, EDGE; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java index aa03cd7e59..d73dee6e1b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java @@ -19,6 +19,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.validation.NoXss; /** * Created by igor on 2/27/18. @@ -31,6 +32,7 @@ public class ShortCustomerInfo { private CustomerId customerId; @Getter @Setter + @NoXss private String title; @Getter @Setter 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 new file mode 100644 index 0000000000..cbe7094715 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2021 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; + +public class StringUtils { + + public static boolean isEmpty(String source) { + return source == null || source.isEmpty(); + } + + public static boolean isNotEmpty(String source) { + return source != null && !source.isEmpty(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java index 33e94b2e0c..28c7e73e26 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java @@ -19,6 +19,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.validation.NoXss; @Slf4j @Data @@ -27,6 +28,7 @@ public class TbResource extends TbResourceInfo { private static final long serialVersionUID = 7379609705527272306L; + @NoXss private String fileName; private String data; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java index 7fa869e14f..95fbcd46ff 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java @@ -20,6 +20,7 @@ import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.NoXss; @Slf4j @Data @@ -29,6 +30,7 @@ public class TbResourceInfo extends SearchTextBased implements Has private static final long serialVersionUID = 7282664529021651736L; private TenantId tenantId; + @NoXss private String title; private ResourceType resourceType; private String resourceKey; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java index 91bd902e9a..80e3730955 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java @@ -19,11 +19,8 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TimePageLink; -import java.util.UUID; - /** * Created by ashvayka on 11.05.17. */ @@ -37,6 +34,5 @@ public class AlarmQuery { private AlarmSearchStatus searchStatus; private AlarmStatus status; private Boolean fetchOriginator; - private UUID idOffset; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index 4b0d6accc0..2594c73ebe 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -45,7 +45,9 @@ public enum ActionType { ASSIGNED_FROM_TENANT(false), ASSIGNED_TO_TENANT(false), PROVISION_SUCCESS(false), - PROVISION_FAILURE(false); + PROVISION_FAILURE(false), + ASSIGNED_TO_EDGE(false), // log edge name + UNASSIGNED_FROM_EDGE(false); private final boolean isRead; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java index dee683d242..90de622fb0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java @@ -18,12 +18,13 @@ package org.thingsboard.server.common.data.device.profile; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; +import java.io.Serializable; import javax.validation.Valid; import java.util.List; @Data @JsonIgnoreProperties(ignoreUnknown = true) -public class AlarmCondition { +public class AlarmCondition implements Serializable { @Valid private List condition; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java index b6dc9da8ba..96fbd3465f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java @@ -22,8 +22,10 @@ import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.Valid; +import java.io.Serializable; + @Data -public class AlarmConditionFilter { +public class AlarmConditionFilter implements Serializable { @Valid private AlarmConditionFilterKey key; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java index a82d2e362f..258e50a6a0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java @@ -18,8 +18,10 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; import org.thingsboard.server.common.data.validation.NoXss; +import java.io.Serializable; + @Data -public class AlarmConditionFilterKey { +public class AlarmConditionFilterKey implements Serializable { private final AlarmConditionKeyType type; @NoXss diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java index f48b5a25e6..1a34894997 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import java.io.Serializable; + @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -29,7 +31,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = SimpleAlarmConditionSpec.class, name = "SIMPLE"), @JsonSubTypes.Type(value = DurationAlarmConditionSpec.class, name = "DURATION"), @JsonSubTypes.Type(value = RepeatingAlarmConditionSpec.class, name = "REPEATING")}) -public interface AlarmConditionSpec { +public interface AlarmConditionSpec extends Serializable { @JsonIgnore AlarmConditionSpecType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java index 208cd03347..09c9f084cb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java @@ -20,8 +20,10 @@ import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.Valid; +import java.io.Serializable; + @Data -public class AlarmRule { +public class AlarmRule implements Serializable { @Valid private AlarmCondition condition; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java index 7e3517aa34..1d5eceb1aa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import java.io.Serializable; + @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -28,7 +30,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = AnyTimeSchedule.class, name = "ANY_TIME"), @JsonSubTypes.Type(value = SpecificTimeSchedule.class, name = "SPECIFIC_TIME"), @JsonSubTypes.Type(value = CustomTimeSchedule.class, name = "CUSTOM")}) -public interface AlarmSchedule { +public interface AlarmSchedule extends Serializable { AlarmScheduleType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java index edee91f877..5a63413b2a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java @@ -17,10 +17,10 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; -import java.util.List; +import java.io.Serializable; @Data -public class CustomTimeScheduleItem { +public class CustomTimeScheduleItem implements Serializable { private boolean enabled; private int dayOfWeek; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java index ee9bdfb30f..7a99ccb41e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java @@ -19,12 +19,13 @@ import lombok.Data; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.validation.NoXss; +import java.io.Serializable; import javax.validation.Valid; import java.util.List; import java.util.TreeMap; @Data -public class DeviceProfileAlarm { +public class DeviceProfileAlarm implements Serializable { private String id; @NoXss diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java index 91a810f0d7..ce019212eb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java @@ -21,6 +21,8 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.server.common.data.DeviceProfileType; +import java.io.Serializable; + @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -28,7 +30,7 @@ import org.thingsboard.server.common.data.DeviceProfileType; property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = DefaultDeviceProfileConfiguration.class, name = "DEFAULT")}) -public interface DeviceProfileConfiguration { +public interface DeviceProfileConfiguration extends Serializable { @JsonIgnore DeviceProfileType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java index 1bf8a88a3a..f77e6388a5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java @@ -17,13 +17,15 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; +import java.io.Serializable; import javax.validation.Valid; import java.util.List; @Data -public class DeviceProfileData { +public class DeviceProfileData implements Serializable { private DeviceProfileConfiguration configuration; + @Valid private DeviceProfileTransportConfiguration transportConfiguration; private DeviceProfileProvisionConfiguration provisionConfiguration; @Valid diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java index 97de41124a..2b0f1b8da1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java @@ -21,6 +21,8 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import java.io.Serializable; + @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( @@ -31,7 +33,7 @@ import org.thingsboard.server.common.data.DeviceProfileProvisionType; @JsonSubTypes.Type(value = DisabledDeviceProfileProvisionConfiguration.class, name = "DISABLED"), @JsonSubTypes.Type(value = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class, name = "ALLOW_CREATE_NEW_DEVICES"), @JsonSubTypes.Type(value = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class, name = "CHECK_PRE_PROVISIONED_DEVICES")}) -public interface DeviceProfileProvisionConfiguration { +public interface DeviceProfileProvisionConfiguration extends Serializable { String getProvisionDeviceSecret(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java index 3558ef5349..d5ec9d2136 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java @@ -19,9 +19,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import java.io.Serializable; + @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -32,7 +33,7 @@ import org.thingsboard.server.common.data.DeviceTransportType; @JsonSubTypes.Type(value = MqttDeviceProfileTransportConfiguration.class, name = "MQTT"), @JsonSubTypes.Type(value = Lwm2mDeviceProfileTransportConfiguration.class, name = "LWM2M"), @JsonSubTypes.Type(value = CoapDeviceProfileTransportConfiguration.class, name = "COAP")}) -public interface DeviceProfileTransportConfiguration { +public interface DeviceProfileTransportConfiguration extends Serializable { @JsonIgnore DeviceTransportType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java index 3530cec583..63bb6324c6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java @@ -17,11 +17,14 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.validation.NoXss; @Data public class MqttDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { + @NoXss private String deviceTelemetryTopic = MqttTopics.DEVICE_TELEMETRY_TOPIC; + @NoXss private String deviceAttributesTopic = MqttTopics.DEVICE_ATTRIBUTES_TOPIC; private TransportPayloadTypeConfiguration transportPayloadTypeConfiguration; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java index 13982f7e0b..159fbced16 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.common.data.device.profile; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.github.os72.protobuf.dynamic.DynamicSchema; import com.github.os72.protobuf.dynamic.EnumDefinition; import com.github.os72.protobuf.dynamic.MessageDefinition; @@ -46,9 +45,13 @@ public class ProtoTransportPayloadConfiguration implements TransportPayloadTypeC public static final Location LOCATION = new Location("", "", -1, -1); public static final String ATTRIBUTES_PROTO_SCHEMA = "attributes proto schema"; public static final String TELEMETRY_PROTO_SCHEMA = "telemetry proto schema"; + public static final String RPC_RESPONSE_PROTO_SCHEMA = "rpc response proto schema"; + public static final String RPC_REQUEST_PROTO_SCHEMA = "rpc request proto schema"; private String deviceTelemetryProtoSchema; private String deviceAttributesProtoSchema; + private String deviceRpcRequestProtoSchema; + private String deviceRpcResponseProtoSchema; @Override public TransportPayloadType getTransportPayloadType() { @@ -63,13 +66,45 @@ public class ProtoTransportPayloadConfiguration implements TransportPayloadTypeC return getDescriptor(deviceAttributesProtoSchema, ATTRIBUTES_PROTO_SCHEMA); } + public Descriptors.Descriptor getRpcResponseDynamicMessageDescriptor(String deviceRpcResponseProtoSchema) { + return getDescriptor(deviceRpcResponseProtoSchema, RPC_RESPONSE_PROTO_SCHEMA); + } + + public DynamicMessage.Builder getRpcRequestDynamicMessageBuilder(String deviceRpcRequestProtoSchema) { + return getDynamicMessageBuilder(deviceRpcRequestProtoSchema, RPC_REQUEST_PROTO_SCHEMA); + } + + public String getDeviceRpcResponseProtoSchema() { + if (!isEmptyStr(deviceRpcResponseProtoSchema)) { + return deviceRpcResponseProtoSchema; + } else { + return "syntax =\"proto3\";\n" + + "package rpc;\n" + + "\n" + + "message RpcResponseMsg {\n" + + " string payload = 1;\n" + + "}"; + } + } + + public String getDeviceRpcRequestProtoSchema() { + if (!isEmptyStr(deviceRpcRequestProtoSchema)) { + return deviceRpcRequestProtoSchema; + } else { + return "syntax =\"proto3\";\n" + + "package rpc;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " string method = 1;\n" + + " int32 requestId = 2;\n" + + " string params = 3;\n" + + "}"; + } + } + private Descriptors.Descriptor getDescriptor(String protoSchema, String schemaName) { try { - ProtoFileElement protoFileElement = getTransportProtoSchema(protoSchema); - DynamicSchema dynamicSchema = getDynamicSchema(protoFileElement, schemaName); - String lastMsgName = getMessageTypes(protoFileElement.getTypes()).stream() - .map(MessageElement::getName).reduce((previous, last) -> last).get(); - DynamicMessage.Builder builder = dynamicSchema.newMessageBuilder(lastMsgName); + DynamicMessage.Builder builder = getDynamicMessageBuilder(protoSchema, schemaName); return builder.getDescriptorForType(); } catch (Exception e) { log.warn("Failed to get Message Descriptor due to {}", e.getMessage()); @@ -77,6 +112,14 @@ public class ProtoTransportPayloadConfiguration implements TransportPayloadTypeC } } + public DynamicMessage.Builder getDynamicMessageBuilder(String protoSchema, String schemaName) { + ProtoFileElement protoFileElement = getTransportProtoSchema(protoSchema); + DynamicSchema dynamicSchema = getDynamicSchema(protoFileElement, schemaName); + String lastMsgName = getMessageTypes(protoFileElement.getTypes()).stream() + .map(MessageElement::getName).reduce((previous, last) -> last).get(); + return dynamicSchema.newMessageBuilder(lastMsgName); + } + public DynamicSchema getDynamicSchema(ProtoFileElement protoFileElement, String schemaName) { DynamicSchema.Builder schemaBuilder = DynamicSchema.newBuilder(); schemaBuilder.setName(schemaName); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java new file mode 100644 index 0000000000..939bfb3648 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.thingsboard.server.common.data.HasCustomerId; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Getter +@Setter +public class Edge extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId { + + private static final long serialVersionUID = 4934987555236873728L; + + private TenantId tenantId; + private CustomerId customerId; + private RuleChainId rootRuleChainId; + private String name; + private String type; + private String label; + private String routingKey; + private String secret; + private String edgeLicenseKey; + private String cloudEndpoint; + + public Edge() { + super(); + } + + public Edge(EdgeId id) { + super(id); + } + + public Edge(Edge edge) { + super(edge); + this.tenantId = edge.getTenantId(); + this.customerId = edge.getCustomerId(); + this.rootRuleChainId = edge.getRootRuleChainId(); + this.type = edge.getType(); + this.label = edge.getLabel(); + this.name = edge.getName(); + this.routingKey = edge.getRoutingKey(); + this.secret = edge.getSecret(); + this.edgeLicenseKey = edge.getEdgeLicenseKey(); + this.cloudEndpoint = edge.getCloudEndpoint(); + } + + @Override + public String getSearchText() { + return getName(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java new file mode 100644 index 0000000000..32d63e8973 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.id.EdgeEventId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.UUID; + +@Data +public class EdgeEvent extends BaseData { + + private TenantId tenantId; + private EdgeId edgeId; + private EdgeEventActionType action; + private UUID entityId; + private String uid; + private EdgeEventType type; + private transient JsonNode body; + + public EdgeEvent() { + super(); + } + + public EdgeEvent(EdgeEventId id) { + super(id); + } + + public EdgeEvent(EdgeEvent event) { + super(event); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java new file mode 100644 index 0000000000..b6664ea57d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2021 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.edge; + +public enum EdgeEventActionType { + ADDED, + DELETED, + UPDATED, + POST_ATTRIBUTES, + ATTRIBUTES_UPDATED, + ATTRIBUTES_DELETED, + TIMESERIES_UPDATED, + CREDENTIALS_UPDATED, + ASSIGNED_TO_CUSTOMER, + UNASSIGNED_FROM_CUSTOMER, + RELATION_ADD_OR_UPDATE, + RELATION_DELETED, + RPC_CALL, + ALARM_ACK, + ALARM_CLEAR, + ASSIGNED_TO_EDGE, + UNASSIGNED_FROM_EDGE, + CREDENTIALS_REQUEST, + ENTITY_MERGE_REQUEST +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java new file mode 100644 index 0000000000..19a0314b41 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2021 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.edge; + +public enum EdgeEventType { + DASHBOARD, + ASSET, + DEVICE, + DEVICE_PROFILE, + ENTITY_VIEW, + ALARM, + RULE_CHAIN, + RULE_CHAIN_METADATA, + EDGE, + USER, + CUSTOMER, + RELATION, + TENANT, + WIDGETS_BUNDLE, + WIDGET_TYPE, + ADMIN_SETTINGS +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInfo.java new file mode 100644 index 0000000000..4ba2892574 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeInfo.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2021 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.edge; + +import lombok.Data; +import org.thingsboard.server.common.data.id.EdgeId; + +@Data +public class EdgeInfo extends Edge { + + private String customerTitle; + private boolean customerIsPublic; + + public EdgeInfo() { + super(); + } + + public EdgeInfo(EdgeId edgeId) { + super(edgeId); + } + + public EdgeInfo(Edge edge, String customerTitle, boolean customerIsPublic) { + super(edge); + this.customerTitle = customerTitle; + this.customerIsPublic = customerIsPublic; + } +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeSearchQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeSearchQuery.java new file mode 100644 index 0000000000..0da7fd08af --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeSearchQuery.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2021 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.edge; + +import lombok.Data; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationsQuery; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationsSearchParameters; + +import java.util.Collections; +import java.util.List; + +@Data +public class EdgeSearchQuery { + + private RelationsSearchParameters parameters; + private String relationType; + private List edgeTypes; + + public EntityRelationsQuery toEntitySearchQuery() { + EntityRelationsQuery query = new EntityRelationsQuery(); + query.setParameters(parameters); + query.setFilters( + Collections.singletonList(new RelationEntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, + Collections.singletonList(EntityType.EDGE)))); + return query; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEvent.java new file mode 100644 index 0000000000..a95949acbd --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEvent.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2021 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.event; + +import lombok.Data; +import org.thingsboard.server.common.data.StringUtils; + +@Data +public abstract class DebugEvent implements EventFilter { + + private String msgDirectionType; + private String server; + private String dataSearch; + private String metadataSearch; + private String entityName; + private String relationType; + private String entityId; + private String msgType; + private boolean isError; + private String error; + + public void setIsError(boolean isError) { + this.isError = isError; + } + + @Override + public boolean hasFilterForJsonBody() { + return !StringUtils.isEmpty(msgDirectionType) || !StringUtils.isEmpty(server) || !StringUtils.isEmpty(dataSearch) || !StringUtils.isEmpty(metadataSearch) + || !StringUtils.isEmpty(entityName) || !StringUtils.isEmpty(relationType) || !StringUtils.isEmpty(entityId) || !StringUtils.isEmpty(msgType) || !StringUtils.isEmpty(error) || isError; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleChainEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleChainEventFilter.java new file mode 100644 index 0000000000..cc16d780f0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleChainEventFilter.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2021 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.event; + +public class DebugRuleChainEventFilter extends DebugEvent { + @Override + public EventType getEventType() { + return EventType.DEBUG_RULE_CHAIN; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleNodeEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleNodeEventFilter.java new file mode 100644 index 0000000000..abe73e6e85 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleNodeEventFilter.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2021 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.event; + +public class DebugRuleNodeEventFilter extends DebugEvent { + @Override + public EventType getEventType() { + return EventType.DEBUG_RULE_NODE; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/ErrorEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/ErrorEventFilter.java new file mode 100644 index 0000000000..c9e9886c64 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/ErrorEventFilter.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2021 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.event; + +import lombok.Data; +import org.thingsboard.server.common.data.StringUtils; + +@Data +public class ErrorEventFilter implements EventFilter { + private String server; + private String method; + private String error; + + @Override + public EventType getEventType() { + return EventType.ERROR; + } + + @Override + public boolean hasFilterForJsonBody() { + return !StringUtils.isEmpty(server) || !StringUtils.isEmpty(method) || !StringUtils.isEmpty(error); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java new file mode 100644 index 0000000000..eebab7e7d4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2021 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.event; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "eventType") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DebugRuleNodeEventFilter.class, name = "DEBUG_RULE_NODE"), + @JsonSubTypes.Type(value = DebugRuleChainEventFilter.class, name = "DEBUG_RULE_CHAIN"), + @JsonSubTypes.Type(value = ErrorEventFilter.class, name = "ERROR"), + @JsonSubTypes.Type(value = LifeCycleEventFilter.class, name = "LC_EVENT"), + @JsonSubTypes.Type(value = StatisticsEventFilter.class, name = "STATS") +}) +public interface EventFilter { + @JsonIgnore + EventType getEventType(); + + boolean hasFilterForJsonBody(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/EventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventType.java new file mode 100644 index 0000000000..a0700fdfbc --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2021 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.event; + +public enum EventType { + ERROR, LC_EVENT, STATS, DEBUG_RULE_NODE, DEBUG_RULE_CHAIN +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java new file mode 100644 index 0000000000..a871ccb106 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2021 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.event; + +import lombok.Data; +import org.thingsboard.server.common.data.StringUtils; + +@Data +public class LifeCycleEventFilter implements EventFilter { + private String server; + private String event; + private String status; + private String error; + + @Override + public EventType getEventType() { + return EventType.LC_EVENT; + } + + @Override + public boolean hasFilterForJsonBody() { + return !StringUtils.isEmpty(server) || !StringUtils.isEmpty(event) || !StringUtils.isEmpty(status) || !StringUtils.isEmpty(error); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/StatisticsEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/StatisticsEventFilter.java new file mode 100644 index 0000000000..9fad8b4bca --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/StatisticsEventFilter.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2021 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.event; + +import lombok.Data; +import org.thingsboard.server.common.data.StringUtils; + +@Data +public class StatisticsEventFilter implements EventFilter { + private String server; + private Integer messagesProcessed; + private Integer errorsOccurred; + + @Override + public EventType getEventType() { + return EventType.STATS; + } + + @Override + public boolean hasFilterForJsonBody() { + return !StringUtils.isEmpty(server) || (messagesProcessed != null && messagesProcessed > 0) || (errorsOccurred != null && errorsOccurred > 0); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java index c9d896cc9e..04930d6464 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java @@ -28,7 +28,8 @@ public enum ThingsboardErrorCode { BAD_REQUEST_PARAMS(31), ITEM_NOT_FOUND(32), TOO_MANY_REQUESTS(33), - TOO_MANY_UPDATES(34); + TOO_MANY_UPDATES(34), + SUBSCRIPTION_VIOLATION(40); private int errorCode; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeEventId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeEventId.java new file mode 100644 index 0000000000..ce352632db --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeEventId.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2021 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.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.UUID; + +public class EdgeEventId extends UUIDBased { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public EdgeEventId(@JsonProperty("id") UUID id) { + super(id); + } + + public static EdgeEventId fromString(String edgeEventId) { + return new EdgeEventId(UUID.fromString(edgeEventId)); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java new file mode 100644 index 0000000000..92421c39fe --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2021 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.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; + +import java.util.UUID; + +public class EdgeId extends UUIDBased implements EntityId { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public EdgeId(@JsonProperty("id") UUID id) { + super(id); + } + + public static EdgeId fromString(String integrationId) { + return new EdgeId(UUID.fromString(integrationId)); + } + + @JsonIgnore + @Override + public EntityType getEntityType() { + return EntityType.EDGE; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index 9179984a12..df2846eead 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.id; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edge.EdgeEventType; import java.util.UUID; @@ -72,8 +73,40 @@ public class EntityIdFactory { return new TbResourceId(uuid); case FIRMWARE: return new FirmwareId(uuid); + case EDGE: + return new EdgeId(uuid); } throw new IllegalArgumentException("EntityType " + type + " is not supported!"); } + public static EntityId getByEdgeEventTypeAndUuid(EdgeEventType edgeEventType, UUID uuid) { + switch (edgeEventType) { + case CUSTOMER: + return new CustomerId(uuid); + case USER: + return new UserId(uuid); + case DASHBOARD: + return new DashboardId(uuid); + case DEVICE: + return new DeviceId(uuid); + case DEVICE_PROFILE: + return new DeviceProfileId(uuid); + case ASSET: + return new AssetId(uuid); + case ALARM: + return new AlarmId(uuid); + case RULE_CHAIN: + return new RuleChainId(uuid); + case ENTITY_VIEW: + return new EntityViewId(uuid); + case WIDGETS_BUNDLE: + return new WidgetsBundleId(uuid); + case WIDGET_TYPE: + return new WidgetTypeId(uuid); + case EDGE: + return new EdgeId(uuid); + } + throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java index 6f824155de..307447ef9c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java @@ -20,9 +20,11 @@ import lombok.Data; import lombok.RequiredArgsConstructor; import org.thingsboard.server.common.data.validation.NoXss; +import java.io.Serializable; + @Data @RequiredArgsConstructor -public class DynamicValue { +public class DynamicValue implements Serializable { @JsonIgnore private T resolvedValue; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeSearchQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeSearchQueryFilter.java new file mode 100644 index 0000000000..9424609e57 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeSearchQueryFilter.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2021 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.query; + +import lombok.Data; + +import java.util.List; + +@Data +public class EdgeSearchQueryFilter extends EntitySearchQueryFilter { + + @Override + public EntityFilterType getType() { + return EntityFilterType.EDGE_SEARCH_QUERY; + } + + private List edgeTypes; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeTypeFilter.java new file mode 100644 index 0000000000..3158a6f0d3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EdgeTypeFilter.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2021 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.query; + +import lombok.Data; + +@Data +public class EdgeTypeFilter implements EntityFilter { + + @Override + public EntityFilterType getType() { + return EntityFilterType.EDGE_TYPE; + } + + private String edgeType; + + private String edgeNameFilter; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java index efdd70ec7b..958b8367ed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java @@ -32,12 +32,14 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = EntityTypeFilter.class, name = "entityType"), @JsonSubTypes.Type(value = AssetTypeFilter.class, name = "assetType"), @JsonSubTypes.Type(value = DeviceTypeFilter.class, name = "deviceType"), + @JsonSubTypes.Type(value = EdgeTypeFilter.class, name = "edgeType"), @JsonSubTypes.Type(value = EntityViewTypeFilter.class, name = "entityViewType"), @JsonSubTypes.Type(value = ApiUsageStateFilter.class, name = "apiUsageState"), @JsonSubTypes.Type(value = RelationsQueryFilter.class, name = "relationsQuery"), @JsonSubTypes.Type(value = AssetSearchQueryFilter.class, name = "assetSearchQuery"), @JsonSubTypes.Type(value = DeviceSearchQueryFilter.class, name = "deviceSearchQuery"), - @JsonSubTypes.Type(value = EntityViewSearchQueryFilter.class, name = "entityViewSearchQuery")}) + @JsonSubTypes.Type(value = EntityViewSearchQueryFilter.class, name = "entityViewSearchQuery"), + @JsonSubTypes.Type(value = EdgeSearchQueryFilter.class, name = "edgeSearchQuery")}) public interface EntityFilter { @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java index 6b590c4695..1a843c7b0f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java @@ -23,10 +23,12 @@ public enum EntityFilterType { ASSET_TYPE("assetType"), DEVICE_TYPE("deviceType"), ENTITY_VIEW_TYPE("entityViewType"), + EDGE_TYPE("edgeType"), RELATIONS_QUERY("relationsQuery"), ASSET_SEARCH_QUERY("assetSearchQuery"), DEVICE_SEARCH_QUERY("deviceSearchQuery"), ENTITY_VIEW_SEARCH_QUERY("entityViewSearchQuery"), + EDGE_SEARCH_QUERY("edgeSearchQuery"), API_USAGE_STATE("apiUsageState"); private final String label; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java index 191ef02f11..233af353d1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java @@ -17,8 +17,10 @@ package org.thingsboard.server.common.data.query; import lombok.Data; +import java.io.Serializable; + @Data -public class EntityKey { +public class EntityKey implements Serializable { private final EntityKeyType type; private final String key; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java index 9228718b83..aedf516096 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java @@ -24,8 +24,10 @@ import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.Valid; +import java.io.Serializable; + @Data -public class FilterPredicateValue { +public class FilterPredicateValue implements Serializable { @Getter @NoXss diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java index e8cdd20334..1b1ec51314 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java @@ -17,8 +17,10 @@ package org.thingsboard.server.common.data.query; import lombok.Data; +import java.io.Serializable; + @Data -public class KeyFilter { +public class KeyFilter implements Serializable { private EntityKey key; private EntityKeyValueType valueType; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilterPredicate.java index 5d992bf2ca..79f2f0e132 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilterPredicate.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import java.io.Serializable; + @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, @@ -28,7 +30,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = NumericFilterPredicate.class, name = "NUMERIC"), @JsonSubTypes.Type(value = BooleanFilterPredicate.class, name = "BOOLEAN"), @JsonSubTypes.Type(value = ComplexFilterPredicate.class, name = "COMPLEX")}) -public interface KeyFilterPredicate { +public interface KeyFilterPredicate extends Serializable { @JsonIgnore FilterPredicateType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java index 66b0b32129..7833acc050 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java @@ -32,6 +32,7 @@ public class EntityRelation implements Serializable { private static final long serialVersionUID = 2807343040519543363L; + public static final String EDGE_TYPE = "ManagedByEdge"; public static final String CONTAINS_TYPE = "Contains"; public static final String MANAGES_TYPE = "Manages"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java index 5ff5b685aa..c6ca10088d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java @@ -21,6 +21,8 @@ public enum RelationTypeGroup { ALARM, DASHBOARD, RULE_CHAIN, - RULE_NODE + RULE_NODE, + EDGE, + EDGE_AUTO_ASSIGN_RULE_CHAIN } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java index f2610696f6..da84f15427 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java @@ -38,10 +38,12 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo im private TenantId tenantId; @NoXss private String name; + private RuleChainType type; private RuleNodeId firstRuleNodeId; private boolean root; private boolean debugMode; private transient JsonNode configuration; + @JsonIgnore private byte[] configurationBytes; @@ -57,6 +59,7 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo im super(ruleChain); this.tenantId = ruleChain.getTenantId(); this.name = ruleChain.getName(); + this.type = ruleChain.getType(); this.firstRuleNodeId = ruleChain.getFirstRuleNodeId(); this.root = ruleChain.isRoot(); this.setConfiguration(ruleChain.getConfiguration()); @@ -79,5 +82,4 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo im public void setConfiguration(JsonNode data) { setJson(data, json -> this.configuration = json, bytes -> this.configurationBytes = bytes); } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainType.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainType.java new file mode 100644 index 0000000000..938538c132 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2021 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.rule; + +public enum RuleChainType { + CORE, EDGE +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index fc63133510..72713f12d3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -15,10 +15,16 @@ */ package org.thingsboard.server.common.data.tenant.profile; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.TenantProfileType; +@AllArgsConstructor +@NoArgsConstructor +@Builder @Data public class DefaultTenantProfileConfiguration implements TenantProfileConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java index e3463f3614..300015fa23 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.validation.NoXss; @Data public class BaseWidgetType extends BaseData implements HasTenantId { @@ -27,8 +28,11 @@ public class BaseWidgetType extends BaseData implements HasTenantI private static final long serialVersionUID = 8388684344603660756L; private TenantId tenantId; + @NoXss private String bundleAlias; + @NoXss private String alias; + @NoXss private String name; public BaseWidgetType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java index 232a10e755..c43d59041c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java @@ -15,14 +15,15 @@ */ package org.thingsboard.server.common.data.widget; -import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.validation.NoXss; @Data public class WidgetTypeDetails extends WidgetType { private String image; + @NoXss private String description; public WidgetTypeDetails() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java index 645483bcc3..2fde13552b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java @@ -17,12 +17,15 @@ package org.thingsboard.server.common.data.widget; import lombok.Data; import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.validation.NoXss; @Data public class WidgetTypeInfo extends BaseWidgetType { private String image; + @NoXss private String description; + @NoXss private String widgetType; public WidgetTypeInfo() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java index a15cd8baf5..df09f75922 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java @@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; +import org.thingsboard.server.common.data.validation.NoXss; import java.util.Arrays; @@ -27,9 +28,12 @@ public class WidgetsBundle extends SearchTextBased implements H private static final long serialVersionUID = -7627368878362410489L; private TenantId tenantId; + @NoXss private String alias; + @NoXss private String title; private String image; + @NoXss private String description; public WidgetsBundle() { diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml new file mode 100644 index 0000000000..cf501686a2 --- /dev/null +++ b/common/edge-api/pom.xml @@ -0,0 +1,133 @@ + + + 4.0.0 + + org.thingsboard + 3.3.0-SNAPSHOT + common + + org.thingsboard.common + edge-api + jar + + Thingsboard Server Remote Edge wrapper + https://thingsboard.io + + + UTF-8 + ${basedir}/../.. + + + + + org.thingsboard.common + data + + + org.thingsboard.common + queue + + + org.thingsboard.common + message + + + com.google.code.gson + gson + + + org.slf4j + slf4j-api + + + org.slf4j + log4j-over-slf4j + + + ch.qos.logback + logback-core + + + ch.qos.logback + logback-classic + + + org.springframework + spring-context + + + org.springframework.boot + spring-boot-starter-web + + + io.netty + netty-all + provided + + + com.google.guava + guava + + + io.grpc + grpc-netty + + + netty-transport + io.netty + + + netty-common + io.netty + + + + + io.grpc + grpc-protobuf + + + io.grpc + grpc-stub + + + com.google.protobuf + protobuf-java + + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + + + + thingsboard-repo-deploy + ThingsBoard Repo Deployment + https://repo.thingsboard.io/artifactory/libs-release-public + + + + diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/exception/EdgeConnectionException.java b/common/edge-api/src/main/java/org/thingsboard/edge/exception/EdgeConnectionException.java new file mode 100644 index 0000000000..edd53f15e4 --- /dev/null +++ b/common/edge-api/src/main/java/org/thingsboard/edge/exception/EdgeConnectionException.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2021 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.edge.exception; + +public class EdgeConnectionException extends RuntimeException { + + private static final long serialVersionUID = -4372754681230555723L; + + public EdgeConnectionException(String message) { + super(message); + } + + public EdgeConnectionException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..c5099069ee --- /dev/null +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -0,0 +1,221 @@ +/** + * Copyright © 2016-2021 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.edge.rpc; + +import com.google.common.io.Resources; +import io.grpc.ManagedChannel; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.NettyChannelBuilder; +import io.grpc.stub.StreamObserver; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.thingsboard.edge.exception.EdgeConnectionException; +import org.thingsboard.server.gen.edge.ConnectRequestMsg; +import org.thingsboard.server.gen.edge.ConnectResponseCode; +import org.thingsboard.server.gen.edge.ConnectResponseMsg; +import org.thingsboard.server.gen.edge.DownlinkMsg; +import org.thingsboard.server.gen.edge.DownlinkResponseMsg; +import org.thingsboard.server.gen.edge.EdgeConfiguration; +import org.thingsboard.server.gen.edge.EdgeRpcServiceGrpc; +import org.thingsboard.server.gen.edge.RequestMsg; +import org.thingsboard.server.gen.edge.RequestMsgType; +import org.thingsboard.server.gen.edge.ResponseMsg; +import org.thingsboard.server.gen.edge.UplinkMsg; +import org.thingsboard.server.gen.edge.UplinkResponseMsg; + +import javax.net.ssl.SSLException; +import java.io.File; +import java.net.URISyntaxException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +@Service +@Slf4j +public class EdgeGrpcClient implements EdgeRpcClient { + + @Value("${cloud.rpc.host}") + private String rpcHost; + @Value("${cloud.rpc.port}") + private int rpcPort; + @Value("${cloud.rpc.timeout}") + private int timeoutSecs; + @Value("${cloud.rpc.keep_alive_time_sec}") + private int keepAliveTimeSec; + @Value("${cloud.rpc.ssl.enabled}") + private boolean sslEnabled; + @Value("${cloud.rpc.ssl.cert}") + private String certResource; + + private ManagedChannel channel; + + private StreamObserver inputStream; + + private static final ReentrantLock uplinkMsgLock = new ReentrantLock(); + + @Override + public void connect(String edgeKey, + String edgeSecret, + Consumer onUplinkResponse, + Consumer onEdgeUpdate, + Consumer onDownlink, + Consumer onError) { + NettyChannelBuilder builder = NettyChannelBuilder.forAddress(rpcHost, rpcPort) + .keepAliveTime(keepAliveTimeSec, TimeUnit.SECONDS); + if (sslEnabled) { + try { + builder.sslContext(GrpcSslContexts.forClient().trustManager(new File(Resources.getResource(certResource).toURI())).build()); + } catch (URISyntaxException | SSLException e) { + log.error("Failed to initialize channel!", e); + throw new RuntimeException(e); + } + } else { + builder.usePlaintext(); + } + channel = builder.build(); + EdgeRpcServiceGrpc.EdgeRpcServiceStub stub = EdgeRpcServiceGrpc.newStub(channel); + log.info("[{}] Sending a connect request to the TB!", edgeKey); + this.inputStream = stub.handleMsgs(initOutputStream(edgeKey, onUplinkResponse, onEdgeUpdate, onDownlink, onError)); + this.inputStream.onNext(RequestMsg.newBuilder() + .setMsgType(RequestMsgType.CONNECT_RPC_MESSAGE) + .setConnectRequestMsg(ConnectRequestMsg.newBuilder().setEdgeRoutingKey(edgeKey).setEdgeSecret(edgeSecret).build()) + .build()); + } + + private StreamObserver initOutputStream(String edgeKey, + Consumer onUplinkResponse, + Consumer onEdgeUpdate, + Consumer onDownlink, + Consumer onError) { + return new StreamObserver() { + @Override + public void onNext(ResponseMsg responseMsg) { + if (responseMsg.hasConnectResponseMsg()) { + ConnectResponseMsg connectResponseMsg = responseMsg.getConnectResponseMsg(); + if (connectResponseMsg.getResponseCode().equals(ConnectResponseCode.ACCEPTED)) { + log.info("[{}] Configuration received: {}", edgeKey, connectResponseMsg.getConfiguration()); + onEdgeUpdate.accept(connectResponseMsg.getConfiguration()); + } else { + log.error("[{}] Failed to establish the connection! Code: {}. Error message: {}.", edgeKey, connectResponseMsg.getResponseCode(), connectResponseMsg.getErrorMsg()); + try { + EdgeGrpcClient.this.disconnect(true); + } catch (InterruptedException e) { + log.error("[{}] Got interruption during disconnect!", edgeKey, e); + } + onError.accept(new EdgeConnectionException("Failed to establish the connection! Response code: " + connectResponseMsg.getResponseCode().name())); + } + } else if (responseMsg.hasEdgeUpdateMsg()) { + log.debug("[{}] Edge update message received {}", edgeKey, responseMsg.getEdgeUpdateMsg()); + onEdgeUpdate.accept(responseMsg.getEdgeUpdateMsg().getConfiguration()); + } else if (responseMsg.hasUplinkResponseMsg()) { + log.debug("[{}] Uplink response message received {}", edgeKey, responseMsg.getUplinkResponseMsg()); + onUplinkResponse.accept(responseMsg.getUplinkResponseMsg()); + } else if (responseMsg.hasDownlinkMsg()) { + log.debug("[{}] Downlink message received {}", edgeKey, responseMsg.getDownlinkMsg()); + onDownlink.accept(responseMsg.getDownlinkMsg()); + } + } + + @Override + public void onError(Throwable t) { + log.debug("[{}] The rpc session received an error!", edgeKey, t); + try { + EdgeGrpcClient.this.disconnect(true); + } catch (InterruptedException e) { + log.error("[{}] Got interruption during disconnect!", edgeKey, e); + } + onError.accept(new RuntimeException(t)); + } + + @Override + public void onCompleted() { + log.debug("[{}] The rpc session was closed!", edgeKey); + } + }; + } + + @Override + public void disconnect(boolean onError) throws InterruptedException { + if (!onError) { + try { + inputStream.onCompleted(); + } catch (Exception e) { + log.error("Exception during onCompleted", e); + } + } + if (channel != null) { + channel.shutdown(); + int attempt = 0; + do { + try { + channel.awaitTermination(timeoutSecs, TimeUnit.SECONDS); + } catch (Exception e) { + log.error("Channel await termination was interrupted", e); + } + if (attempt > 5) { + log.warn("We had reached maximum of termination attempts. Force closing channel"); + try { + channel.shutdownNow(); + } catch (Exception e) { + log.error("Exception during shutdownNow", e); + } + break; + } + attempt++; + } while (!channel.isTerminated()); + } + } + + @Override + public void sendUplinkMsg(UplinkMsg msg) { + try { + uplinkMsgLock.lock(); + this.inputStream.onNext(RequestMsg.newBuilder() + .setMsgType(RequestMsgType.UPLINK_RPC_MESSAGE) + .setUplinkMsg(msg) + .build()); + } finally { + uplinkMsgLock.unlock(); + } + } + + @Override + public void sendSyncRequestMsg() { + try { + uplinkMsgLock.lock(); + this.inputStream.onNext(RequestMsg.newBuilder() + .setMsgType(RequestMsgType.SYNC_REQUEST_RPC_MESSAGE) + .build()); + } finally { + uplinkMsgLock.unlock(); + } + } + + @Override + public void sendDownlinkResponseMsg(DownlinkResponseMsg downlinkResponseMsg) { + try { + uplinkMsgLock.lock(); + this.inputStream.onNext(RequestMsg.newBuilder() + .setMsgType(RequestMsgType.UPLINK_RPC_MESSAGE) + .setDownlinkResponseMsg(downlinkResponseMsg) + .build()); + } finally { + uplinkMsgLock.unlock(); + } + } +} diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java new file mode 100644 index 0000000000..b3928819a1 --- /dev/null +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2021 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.edge.rpc; + +import org.thingsboard.server.gen.edge.DownlinkMsg; +import org.thingsboard.server.gen.edge.DownlinkResponseMsg; +import org.thingsboard.server.gen.edge.EdgeConfiguration; +import org.thingsboard.server.gen.edge.UplinkMsg; +import org.thingsboard.server.gen.edge.UplinkResponseMsg; + +import java.util.function.Consumer; + +public interface EdgeRpcClient { + + void connect(String integrationKey, + String integrationSecret, + Consumer onUplinkResponse, + Consumer onEdgeUpdate, + Consumer onDownlink, + Consumer onError); + + void disconnect(boolean onError) throws InterruptedException; + + void sendSyncRequestMsg(); + + void sendUplinkMsg(UplinkMsg uplinkMsg); + + void sendDownlinkResponseMsg(DownlinkResponseMsg downlinkResponseMsg); +} diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto new file mode 100644 index 0000000000..9b50b628fd --- /dev/null +++ b/common/edge-api/src/main/proto/edge.proto @@ -0,0 +1,439 @@ +/** + * Copyright © 2016-2021 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. + */ +syntax = "proto3"; + +option java_package = "org.thingsboard.server.gen.edge"; +option java_multiple_files = true; +option java_outer_classname = "EdgeProtos"; + +import "queue.proto"; + +package edge; + +// Interface exported by the ThingsBoard Edge Transport. +service EdgeRpcService { + + rpc handleMsgs(stream RequestMsg) returns (stream ResponseMsg) {} + +} + +/** + * Data Structures; + */ +message RequestMsg { + RequestMsgType msgType = 1; + ConnectRequestMsg connectRequestMsg = 2; + UplinkMsg uplinkMsg = 3; + DownlinkResponseMsg downlinkResponseMsg = 4; +} + +message ResponseMsg { + ConnectResponseMsg connectResponseMsg = 1; + UplinkResponseMsg uplinkResponseMsg = 2; + DownlinkMsg downlinkMsg = 3; + EdgeUpdateMsg edgeUpdateMsg = 4; +} + +enum RequestMsgType { + CONNECT_RPC_MESSAGE = 0; + UPLINK_RPC_MESSAGE = 1; + SYNC_REQUEST_RPC_MESSAGE = 2; +} + +message EdgeUpdateMsg { + EdgeConfiguration configuration = 1; +} + +message ConnectRequestMsg { + string edgeRoutingKey = 1; + string edgeSecret = 2; +} + +enum ConnectResponseCode { + ACCEPTED = 0; + BAD_CREDENTIALS = 1; + SERVER_UNAVAILABLE = 2; +} + +message ConnectResponseMsg { + ConnectResponseCode responseCode = 1; + string errorMsg = 2; + EdgeConfiguration configuration = 3; +} + +message EdgeConfiguration { + int64 edgeIdMSB = 1; + int64 edgeIdLSB = 2; + int64 tenantIdMSB = 3; + int64 tenantIdLSB = 4; + int64 customerIdMSB = 5; + int64 customerIdLSB = 6; + string name = 7; + string type = 8; + string routingKey = 9; + string secret = 10; + string edgeLicenseKey = 11; + string cloudEndpoint = 12; + string additionalInfo = 13; + string cloudType = 14; +} + +enum UpdateMsgType { + ENTITY_CREATED_RPC_MESSAGE = 0; + ENTITY_UPDATED_RPC_MESSAGE = 1; + ENTITY_DELETED_RPC_MESSAGE = 2; + ALARM_ACK_RPC_MESSAGE = 3; + ALARM_CLEAR_RPC_MESSAGE = 4; + ENTITY_MERGE_RPC_MESSAGE = 5; +} + +message EntityDataProto { + int64 entityIdMSB = 1; + int64 entityIdLSB = 2; + string entityType = 3; + transport.PostTelemetryMsg postTelemetryMsg = 4; + transport.PostAttributeMsg postAttributesMsg = 5; + transport.PostAttributeMsg attributesUpdatedMsg = 6; + string postAttributeScope = 7; + AttributeDeleteMsg attributeDeleteMsg = 8; +} + +message AttributeDeleteMsg { + string scope = 1; + repeated string attributeNames = 2; +} + +message RuleChainUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string name = 4; + int64 firstRuleNodeIdMSB = 5; + int64 firstRuleNodeIdLSB = 6; + bool root = 7; + bool debugMode = 8; + string configuration = 9; +} + +message RuleChainMetadataUpdateMsg { + UpdateMsgType msgType = 1; + int64 ruleChainIdMSB = 2; + int64 ruleChainIdLSB = 3; + int32 firstNodeIndex = 4; + repeated RuleNodeProto nodes = 5; + repeated NodeConnectionInfoProto connections = 6; + repeated RuleChainConnectionInfoProto ruleChainConnections = 7; +} + +message RuleNodeProto { + int64 idMSB = 1; + int64 idLSB = 2; + string type = 3; + string name = 4; + bool debugMode = 5; + string configuration = 6; + string additionalInfo = 7; +} + +message NodeConnectionInfoProto { + int32 fromIndex = 1; + int32 toIndex = 2; + string type = 3; +} + +message RuleChainConnectionInfoProto { + int32 fromIndex = 1; + int64 targetRuleChainIdMSB = 2; + int64 targetRuleChainIdLSB = 3; + string type = 4; + string additionalInfo = 5; +} + +message DashboardUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + int64 customerIdMSB = 4; + int64 customerIdLSB = 5; + string title = 6; + string configuration = 7; +} + +message DeviceUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + int64 customerIdMSB = 4; + int64 customerIdLSB = 5; + int64 deviceProfileIdMSB = 6; + int64 deviceProfileIdLSB = 7; + string name = 8; + string type = 9; + string label = 10; + string additionalInfo = 11; + string conflictName = 12; +} + +message DeviceProfileUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string name = 4; + string description = 5; + bool default = 6; + string type = 7; + string transportType = 8; + string provisionType = 9; + int64 defaultRuleChainIdMSB = 10; + int64 defaultRuleChainIdLSB = 11; + string defaultQueueName = 12; + bytes profileDataBytes = 13; + string provisionDeviceKey = 14; +} + +message DeviceCredentialsUpdateMsg { + int64 deviceIdMSB = 1; + int64 deviceIdLSB = 2; + string credentialsType = 3; + string credentialsId = 4; + string credentialsValue = 5; +} + +message AssetUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + int64 customerIdMSB = 4; + int64 customerIdLSB = 5; + string name = 6; + string type = 7; + string label = 8; + string additionalInfo = 9; +} + +message EntityViewUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + int64 customerIdMSB = 4; + int64 customerIdLSB = 5; + string name = 6; + string type = 7; + int64 entityIdMSB = 8; + int64 entityIdLSB = 9; + EdgeEntityType entityType = 10; + string additionalInfo = 11; +} + +message AlarmUpdateMsg { + UpdateMsgType msgType = 1; + string name = 2; + string type = 3; + string originatorType = 4; + string originatorName = 5; + string severity = 6; + string status = 7; + int64 startTs = 8; + int64 endTs = 9; + int64 ackTs = 10; + int64 clearTs = 11; + string details = 12; + bool propagate = 13; +} + +message CustomerUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string title = 4; + string country = 5; + string state = 6; + string city = 7; + string address = 8; + string address2 = 9; + string zip = 10; + string phone = 11; + string email = 12; + string additionalInfo = 13; +} + +message RelationUpdateMsg { + UpdateMsgType msgType = 1; + int64 fromIdMSB = 2; + int64 fromIdLSB = 3; + string fromEntityType = 4; + int64 toIdMSB = 5; + int64 toIdLSB = 6; + string toEntityType = 7; + string type = 8; + string typeGroup = 9; + string additionalInfo = 10; +} + +message UserUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + int64 customerIdMSB = 4; + int64 customerIdLSB = 5; + string email = 6; + string authority = 7; + string firstName = 8; + string lastName = 9; + string additionalInfo = 10; +} + +message WidgetsBundleUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string title = 4; + string alias = 5; + bytes image = 6; + bool isSystem = 7; +} + +message WidgetTypeUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string bundleAlias = 4; + string alias = 5; + string name = 6; + string descriptorJson = 7; + bool isSystem = 8; +} + +message AdminSettingsUpdateMsg { + bool isSystem = 1; + string key = 2; + string jsonValue = 3; +} + +message UserCredentialsUpdateMsg { + int64 userIdMSB = 1; + int64 userIdLSB = 2; + bool enabled = 3; + string password = 4; +} + +message RuleChainMetadataRequestMsg { + int64 ruleChainIdMSB = 1; + int64 ruleChainIdLSB = 2; +} + +message AttributesRequestMsg { + int64 entityIdMSB = 1; + int64 entityIdLSB = 2; + string entityType = 3; + string scope = 4; +} + +message RelationRequestMsg { + int64 entityIdMSB = 1; + int64 entityIdLSB = 2; + string entityType = 3; +} + +message UserCredentialsRequestMsg { + int64 userIdMSB = 1; + int64 userIdLSB = 2; +} + +message DeviceCredentialsRequestMsg { + int64 deviceIdMSB = 1; + int64 deviceIdLSB = 2; +} + +message DeviceRpcCallMsg { + int64 deviceIdMSB = 1; + int64 deviceIdLSB = 2; + int64 requestUuidMSB = 3; + int64 requestUuidLSB = 4; + int32 requestId = 5; + int64 expirationTime = 6; + bool oneway = 7; + RpcRequestMsg requestMsg = 8; + RpcResponseMsg responseMsg = 9; +} + +message RpcRequestMsg { + string method = 1; + string params = 2; +} + +message RpcResponseMsg { + string response = 1; + string error = 2; +} + +enum EdgeEntityType { + DEVICE = 0; + ASSET = 1; +} + +/** + * Main Messages; + */ + +message UplinkMsg { + int32 uplinkMsgId = 1; + repeated EntityDataProto entityData = 2; + repeated DeviceUpdateMsg deviceUpdateMsg = 3; + repeated DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = 4; + repeated AlarmUpdateMsg alarmUpdateMsg = 5; + repeated RelationUpdateMsg relationUpdateMsg = 6; + repeated RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg = 7; + repeated AttributesRequestMsg attributesRequestMsg = 8; + repeated RelationRequestMsg relationRequestMsg = 9; + repeated UserCredentialsRequestMsg userCredentialsRequestMsg = 10; + repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 11; + repeated DeviceRpcCallMsg deviceRpcCallMsg = 12; +} + +message UplinkResponseMsg { + bool success = 1; + string errorMsg = 2; +} + +message DownlinkResponseMsg { + bool success = 1; + string errorMsg = 2; +} + +message DownlinkMsg { + int32 downlinkMsgId = 1; + repeated EntityDataProto entityData = 2; + repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 3; + repeated DeviceUpdateMsg deviceUpdateMsg = 4; + repeated DeviceProfileUpdateMsg deviceProfileUpdateMsg = 5; + repeated DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = 6; + repeated RuleChainUpdateMsg ruleChainUpdateMsg = 7; + repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 8; + repeated DashboardUpdateMsg dashboardUpdateMsg = 9; + repeated AssetUpdateMsg assetUpdateMsg = 10; + repeated EntityViewUpdateMsg entityViewUpdateMsg = 11; + repeated AlarmUpdateMsg alarmUpdateMsg = 12; + repeated UserUpdateMsg userUpdateMsg = 13; + repeated UserCredentialsUpdateMsg userCredentialsUpdateMsg = 14; + repeated CustomerUpdateMsg customerUpdateMsg = 15; + repeated RelationUpdateMsg relationUpdateMsg = 16; + repeated WidgetsBundleUpdateMsg widgetsBundleUpdateMsg = 17; + repeated WidgetTypeUpdateMsg widgetTypeUpdateMsg = 18; + repeated AdminSettingsUpdateMsg adminSettingsUpdateMsg = 19; + repeated DeviceRpcCallMsg deviceRpcCallMsg = 20; +} + diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java index 13d0a6e9eb..d9f05e3c84 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java @@ -82,8 +82,12 @@ public enum MsgType { DEVICE_NAME_OR_TYPE_UPDATE_TO_DEVICE_ACTOR_MSG, + DEVICE_EDGE_UPDATE_TO_DEVICE_ACTOR_MSG, + DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG, + DEVICE_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG, + SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG, DEVICE_ACTOR_SERVER_SIDE_RPC_TIMEOUT_MSG, @@ -101,6 +105,11 @@ public enum MsgType { /** * Message that is sent by TransportRuleEngineService to Device Actor. Represents messages from the device itself. */ - TRANSPORT_TO_DEVICE_ACTOR_MSG; + TRANSPORT_TO_DEVICE_ACTOR_MSG, + + /** + * Message that is sent on Edge Event to Edge Session + */ + EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG; } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeEventUpdateMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeEventUpdateMsg.java new file mode 100644 index 0000000000..544e60bba4 --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/EdgeEventUpdateMsg.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2021 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.msg.edge; + +import lombok.Getter; +import lombok.ToString; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.aware.TenantAwareMsg; +import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg; + +@ToString +public class EdgeEventUpdateMsg implements TenantAwareMsg, ToAllNodesMsg { + @Getter + private final TenantId tenantId; + @Getter + private final EdgeId edgeId; + + public EdgeEventUpdateMsg(TenantId tenantId, EdgeId edgeId) { + this.tenantId = tenantId; + this.edgeId = edgeId; + } + + @Override + public MsgType getMsgType() { + return MsgType.EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG; + } +} diff --git a/common/pom.xml b/common/pom.xml index 7568d18ca6..892f4fb582 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -43,6 +43,8 @@ dao-api stats cache + coap-server + edge-api diff --git a/common/queue/pom.xml b/common/queue/pom.xml index e0b30dd654..31ec44e8f7 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -112,6 +112,10 @@ com.google.protobuf protobuf-java + + com.google.protobuf + protobuf-java-util + org.apache.curator curator-recipes diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java index 6bd2a69b2b..4481e52461 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java @@ -21,22 +21,28 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueMsg; +import javax.annotation.Nonnull; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Queue; import java.util.Set; -import java.util.concurrent.locks.Lock; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +import static java.util.Collections.emptyList; + @Slf4j public abstract class AbstractTbQueueConsumerTemplate implements TbQueueConsumer { private volatile boolean subscribed; protected volatile boolean stopped = false; protected volatile Set partitions; - protected final Lock consumerLock = new ReentrantLock(); + protected final ReentrantLock consumerLock = new ReentrantLock(); //NonfairSync + final Queue> subscribeQueue = new ConcurrentLinkedQueue<>(); @Getter private final String topic; @@ -47,84 +53,101 @@ public abstract class AbstractTbQueueConsumerTemplate i @Override public void subscribe() { - consumerLock.lock(); - try { - partitions = Collections.singleton(new TopicPartitionInfo(topic, null, null, true)); - subscribed = false; - } finally { - consumerLock.unlock(); + log.info("enqueue topic subscribe {} ", topic); + if (stopped) { + log.error("trying subscribe, but consumer stopped for topic {}", topic); + return; } + subscribeQueue.add(Collections.singleton(new TopicPartitionInfo(topic, null, null, true))); } @Override public void subscribe(Set partitions) { + log.info("enqueue topics subscribe {} ", partitions); + if (stopped) { + log.error("trying subscribe, but consumer stopped for topic {}", topic); + return; + } + subscribeQueue.add(partitions); + } + + @Override + public List poll(long durationInMillis) { + List records; + long startNanos = System.nanoTime(); + if (stopped) { + return errorAndReturnEmpty(); + } + if (!subscribed && partitions == null && subscribeQueue.isEmpty()) { + return sleepAndReturnEmpty(startNanos, durationInMillis); + } + + if (consumerLock.isLocked()) { + log.error("poll. consumerLock is locked. will wait with no timeout. it looks like a race conditions or deadlock", new RuntimeException("stacktrace")); + } + consumerLock.lock(); try { - this.partitions = partitions; - subscribed = false; + while (!subscribeQueue.isEmpty()) { + subscribed = false; + partitions = subscribeQueue.poll(); + } + if (!subscribed) { + List topicNames = partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); + doSubscribe(topicNames); + subscribed = true; + } + records = partitions.isEmpty() ? emptyList() : doPoll(durationInMillis); } finally { consumerLock.unlock(); } + + if (records.isEmpty()) { return sleepAndReturnEmpty(startNanos, durationInMillis); } + + return decodeRecords(records); } - @Override - public List poll(long durationInMillis) { - if (!subscribed && partitions == null) { - try { - Thread.sleep(durationInMillis); - } catch (InterruptedException e) { - log.debug("Failed to await subscription", e); - } - } else { - long pollStartTs = System.currentTimeMillis(); - consumerLock.lock(); + @Nonnull + List decodeRecords(@Nonnull List records) { + List result = new ArrayList<>(records.size()); + records.forEach(record -> { try { - if (!subscribed) { - List topicNames = partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); - doSubscribe(topicNames); - subscribed = true; + if (record != null) { + result.add(decode(record)); } + } catch (IOException e) { + log.error("Failed decode record: [{}]", record); + throw new RuntimeException("Failed to decode record: ", e); + } + }); + return result; + } - List records; - if (partitions.isEmpty()) { - records = Collections.emptyList(); - } else { - records = doPoll(durationInMillis); - } - if (!records.isEmpty()) { - List result = new ArrayList<>(records.size()); - records.forEach(record -> { - try { - if (record != null) { - result.add(decode(record)); - } - } catch (IOException e) { - log.error("Failed decode record: [{}]", record); - throw new RuntimeException("Failed to decode record: ", e); - } - }); - return result; - } else { - long pollDuration = System.currentTimeMillis() - pollStartTs; - if (pollDuration < durationInMillis) { - try { - Thread.sleep(durationInMillis - pollDuration); - } catch (InterruptedException e) { - if (!stopped) { - log.error("Failed to wait.", e); - } - } - } + List errorAndReturnEmpty() { + log.error("poll invoked but consumer stopped for topic" + topic, new RuntimeException("stacktrace")); + return emptyList(); + } + + List sleepAndReturnEmpty(final long startNanos, final long durationInMillis) { + long durationNanos = TimeUnit.MILLISECONDS.toNanos(durationInMillis); + long spentNanos = System.nanoTime() - startNanos; + if (spentNanos < durationNanos) { + try { + Thread.sleep(Math.max(TimeUnit.NANOSECONDS.toMillis(durationNanos - spentNanos), 1)); + } catch (InterruptedException e) { + if (!stopped) { + log.error("Failed to wait", e); } - } finally { - consumerLock.unlock(); } } - return Collections.emptyList(); + return emptyList(); } @Override public void commit() { + if (consumerLock.isLocked()) { + log.error("commit. consumerLock is locked. will wait with no timeout. it looks like a race conditions or deadlock", new RuntimeException("stacktrace")); + } consumerLock.lock(); try { doCommit(); @@ -135,6 +158,7 @@ public abstract class AbstractTbQueueConsumerTemplate i @Override public void unsubscribe() { + log.info("unsubscribe topic and stop consumer {}", getTopic()); stopped = true; consumerLock.lock(); try { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java index 5253f57248..7e7f280cc9 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java @@ -125,8 +125,10 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi log.debug("Ignoring application ready event, ZK client is not started, ZK client state [{}]", client.getState()); return; } + log.info("Going to publish current server..."); publishCurrentServer(); - partitionService.recalculatePartitions(serviceInfoProvider.getServiceInfo(), getOtherServers()); + log.info("Going to recalculate partitions..."); + recalculatePartitions(); } public synchronized void publishCurrentServer() { @@ -281,11 +283,19 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi case CHILD_ADDED: case CHILD_UPDATED: case CHILD_REMOVED: - partitionService.recalculatePartitions(serviceInfoProvider.getServiceInfo(), getOtherServers()); + recalculatePartitions(); break; default: break; } } + /** + * A single entry point to recalculate partitions + * Synchronized to ensure that other servers info is up to date + * */ + synchronized void recalculatePartitions() { + partitionService.recalculatePartitions(serviceInfoProvider.getServiceInfo(), getOtherServers()); + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java index b1b3d5ce05..4429f3518f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java @@ -72,8 +72,10 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue protected void doSubscribe(List topicNames) { if (!topicNames.isEmpty()) { topicNames.forEach(admin::createTopicIfNotExists); + log.info("subscribe topics {}", topicNames); consumer.subscribe(topicNames); } else { + log.info("unsubscribe due to empty topic list"); consumer.unsubscribe(); } } @@ -102,6 +104,7 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue @Override protected void doUnsubscribe() { + log.info("unsubscribe topic and close consumer for topic {}", getTopic()); if (consumer != null) { consumer.unsubscribe(); consumer.close(); diff --git a/common/queue/src/main/proto/queue.proto b/common/queue/src/main/proto/queue.proto index 7abcd3613f..6335670f07 100644 --- a/common/queue/src/main/proto/queue.proto +++ b/common/queue/src/main/proto/queue.proto @@ -553,6 +553,21 @@ message FromDeviceRPCResponseProto { int32 error = 4; } +message EdgeNotificationMsgProto { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + int64 edgeIdMSB = 3; + int64 edgeIdLSB = 4; + string type = 5; + string action = 6; + int64 entityIdMSB = 7; + int64 entityIdLSB = 8; + string entityType = 9; + string body = 10; + PostTelemetryMsg postTelemetryMsg = 11; + PostAttributeMsg postAttributesMsg = 12; +} + /** * Main messages; */ @@ -588,6 +603,7 @@ message ToCoreMsg { DeviceStateServiceMsgProto deviceStateServiceMsg = 2; SubscriptionMgrMsgProto toSubscriptionMgrMsg = 3; bytes toDeviceActorNotificationMsg = 4; + EdgeNotificationMsgProto edgeNotificationMsg = 5; } /* High priority messages with low latency are handled by ThingsBoard Core Service separately */ @@ -595,6 +611,7 @@ message ToCoreNotificationMsg { LocalSubscriptionServiceMsgProto toLocalSubscriptionServiceMsg = 1; FromDeviceRPCResponseProto fromDeviceRpcResponse = 2; bytes componentLifecycleMsg = 3; + bytes edgeEventUpdateMsg = 4; } /* Messages that are handled by ThingsBoard RuleEngine Service */ diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index bd4ea45d8b..2fefb8f300 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -40,6 +40,10 @@ org.thingsboard.common.transport transport-api + + org.thingsboard.common + coap-server + org.eclipse.californium californium-core diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java index 73bb318c70..9133809225 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java @@ -18,13 +18,12 @@ package org.thingsboard.server.transport.coap; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.TransportContext; -import org.thingsboard.server.transport.coap.efento.adaptor.EfentoCoapAdaptor; import org.thingsboard.server.transport.coap.adaptors.JsonCoapAdaptor; import org.thingsboard.server.transport.coap.adaptors.ProtoCoapAdaptor; +import org.thingsboard.server.transport.coap.efento.adaptor.EfentoCoapAdaptor; /** @@ -35,22 +34,6 @@ import org.thingsboard.server.transport.coap.adaptors.ProtoCoapAdaptor; @Component public class CoapTransportContext extends TransportContext { - @Getter - @Value("${transport.coap.bind_address}") - private String host; - - @Getter - @Value("${transport.coap.bind_port}") - private Integer port; - - @Getter - @Value("${transport.coap.timeout}") - private Long timeout; - - @Getter - @Autowired(required = false) - private TbCoapDtlsSettings dtlsSettings; - @Getter @Autowired private JsonCoapAdaptor jsonCoapAdaptor; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index cacf6cfc27..aa4236ccf1 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.coap; import com.google.gson.JsonParseException; import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.CoAP; @@ -28,6 +29,8 @@ import org.eclipse.californium.core.server.resources.CoapExchange; import org.eclipse.californium.core.server.resources.Resource; import org.eclipse.californium.core.server.resources.ResourceObserver; import org.springframework.util.StringUtils; +import org.thingsboard.server.coapserver.CoapServerService; +import org.thingsboard.server.coapserver.TbCoapDtlsSessionInfo; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; @@ -74,12 +77,14 @@ public class CoapTransportResource extends AbstractCoapTransportResource { private final Set attributeSubscriptions = ConcurrentHashMap.newKeySet(); private ConcurrentMap dtlsSessionIdMap; + private long timeout; - public CoapTransportResource(CoapTransportContext coapTransportContext, ConcurrentMap dtlsSessionIdMap, String name) { + public CoapTransportResource(CoapTransportContext coapTransportContext, CoapServerService coapServerService, String name) { super(coapTransportContext, name); this.setObservable(true); // enable observing this.addObserver(new CoapResourceObserver()); - this.dtlsSessionIdMap = dtlsSessionIdMap; + this.dtlsSessionIdMap = coapServerService.getDtlsSessionsMap(); + this.timeout = coapServerService.getTimeout(); // this.setObservable(false); // disable observing // this.setObserveType(CoAP.Type.CON); // configure the notification type to CONs // this.getAttributes().setObservable(); // mark observable in the Link-Format @@ -257,7 +262,8 @@ public class CoapTransportResource extends AbstractCoapTransportResource { TransportProtos.SessionInfoProto currentAttrSession = tokenToSessionIdMap.get(getTokenFromRequest(request)); if (currentAttrSession == null) { attributeSubscriptions.add(sessionId); - registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request)); + registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, + transportConfigurationContainer.getRpcRequestDynamicMessageBuilder(), getTokenFromRequest(request)); transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.getDefaultInstance(), new CoapNoOpCallback(exchange)); } @@ -277,7 +283,8 @@ public class CoapTransportResource extends AbstractCoapTransportResource { TransportProtos.SessionInfoProto currentRpcSession = tokenToSessionIdMap.get(getTokenFromRequest(request)); if (currentRpcSession == null) { rpcSubscriptions.add(sessionId); - registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request)); + registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, + transportConfigurationContainer.getRpcRequestDynamicMessageBuilder(), getTokenFromRequest(request)); transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.getDefaultInstance(), new CoapNoOpCallback(exchange)); @@ -299,17 +306,19 @@ public class CoapTransportResource extends AbstractCoapTransportResource { break; case TO_DEVICE_RPC_RESPONSE: transportService.process(sessionInfo, - coapTransportAdaptor.convertToDeviceRpcResponse(sessionId, request), + coapTransportAdaptor.convertToDeviceRpcResponse(sessionId, request, transportConfigurationContainer.getRpcResponseMsgDescriptor()), new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); break; case TO_SERVER_RPC_REQUEST: - transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), transportContext.getTimeout()); + transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor, + transportConfigurationContainer.getRpcRequestDynamicMessageBuilder()), timeout); transportService.process(sessionInfo, coapTransportAdaptor.convertToServerRpcRequest(sessionId, request), new CoapNoOpCallback(exchange)); break; case GET_ATTRIBUTES_REQUEST: - transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), transportContext.getTimeout()); + transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor, + transportConfigurationContainer.getRpcRequestDynamicMessageBuilder()), timeout); transportService.process(sessionInfo, coapTransportAdaptor.convertToGetAttributes(sessionId, request), new CoapNoOpCallback(exchange)); @@ -326,14 +335,14 @@ public class CoapTransportResource extends AbstractCoapTransportResource { return tokenToSessionIdMap.remove(token); } - private void registerAsyncCoapSession(CoapExchange exchange, TransportProtos.SessionInfoProto sessionInfo, CoapTransportAdaptor coapTransportAdaptor, String token) { + private void registerAsyncCoapSession(CoapExchange exchange, TransportProtos.SessionInfoProto sessionInfo, CoapTransportAdaptor coapTransportAdaptor, DynamicMessage.Builder rpcRequestDynamicMessageBuilder, String token) { tokenToSessionIdMap.putIfAbsent(token, sessionInfo); - transportService.registerAsyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor)); + transportService.registerAsyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor, rpcRequestDynamicMessageBuilder)); transportService.process(sessionInfo, getSessionEventMsg(TransportProtos.SessionEvent.OPEN), null); } - private CoapSessionListener getCoapSessionListener(CoapExchange exchange, CoapTransportAdaptor coapTransportAdaptor) { - return new CoapSessionListener(exchange, coapTransportAdaptor); + private CoapSessionListener getCoapSessionListener(CoapExchange exchange, CoapTransportAdaptor coapTransportAdaptor, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) { + return new CoapSessionListener(exchange, coapTransportAdaptor, rpcRequestDynamicMessageBuilder); } private String getTokenFromRequest(Request request) { @@ -419,10 +428,12 @@ public class CoapTransportResource extends AbstractCoapTransportResource { private final CoapExchange exchange; private final CoapTransportAdaptor coapTransportAdaptor; + private final DynamicMessage.Builder rpcRequestDynamicMessageBuilder; - CoapSessionListener(CoapExchange exchange, CoapTransportAdaptor coapTransportAdaptor) { + CoapSessionListener(CoapExchange exchange, CoapTransportAdaptor coapTransportAdaptor, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) { this.exchange = exchange; this.coapTransportAdaptor = coapTransportAdaptor; + this.rpcRequestDynamicMessageBuilder = rpcRequestDynamicMessageBuilder; } @Override @@ -453,7 +464,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { @Override public void onToDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg msg) { try { - exchange.respond(coapTransportAdaptor.convertToPublish(isConRequest(), msg)); + exchange.respond(coapTransportAdaptor.convertToPublish(isConRequest(), msg, rpcRequestDynamicMessageBuilder)); } catch (AdaptorException e) { log.trace("Failed to reply due to error", e); exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR); @@ -541,9 +552,14 @@ public class CoapTransportResource extends AbstractCoapTransportResource { (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; String deviceTelemetryProtoSchema = protoTransportPayloadConfiguration.getDeviceTelemetryProtoSchema(); String deviceAttributesProtoSchema = protoTransportPayloadConfiguration.getDeviceAttributesProtoSchema(); + String deviceRpcRequestProtoSchema = protoTransportPayloadConfiguration.getDeviceRpcRequestProtoSchema(); + String deviceRpcResponseProtoSchema = protoTransportPayloadConfiguration.getDeviceRpcResponseProtoSchema(); return new TransportConfigurationContainer(false, protoTransportPayloadConfiguration.getTelemetryDynamicMessageDescriptor(deviceTelemetryProtoSchema), - protoTransportPayloadConfiguration.getAttributesDynamicMessageDescriptor(deviceAttributesProtoSchema)); + protoTransportPayloadConfiguration.getAttributesDynamicMessageDescriptor(deviceAttributesProtoSchema), + protoTransportPayloadConfiguration.getRpcResponseDynamicMessageDescriptor(deviceRpcResponseProtoSchema), + protoTransportPayloadConfiguration.getRpcRequestDynamicMessageBuilder(deviceRpcRequestProtoSchema) + ); } } else { throw new AdaptorException("Invalid CoapDeviceTypeConfiguration type: " + coapDeviceTypeConfiguration.getClass().getSimpleName() + "!"); @@ -563,11 +579,15 @@ public class CoapTransportResource extends AbstractCoapTransportResource { private boolean jsonPayload; private Descriptors.Descriptor telemetryMsgDescriptor; private Descriptors.Descriptor attributesMsgDescriptor; + private Descriptors.Descriptor rpcResponseMsgDescriptor; + private DynamicMessage.Builder rpcRequestDynamicMessageBuilder; - public TransportConfigurationContainer(boolean jsonPayload, Descriptors.Descriptor telemetryMsgDescriptor, Descriptors.Descriptor attributesMsgDescriptor) { + public TransportConfigurationContainer(boolean jsonPayload, Descriptors.Descriptor telemetryMsgDescriptor, Descriptors.Descriptor attributesMsgDescriptor, Descriptors.Descriptor rpcResponseMsgDescriptor, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) { this.jsonPayload = jsonPayload; this.telemetryMsgDescriptor = telemetryMsgDescriptor; this.attributesMsgDescriptor = attributesMsgDescriptor; + this.rpcResponseMsgDescriptor = rpcResponseMsgDescriptor; + this.rpcRequestDynamicMessageBuilder = rpcRequestDynamicMessageBuilder; } public TransportConfigurationContainer(boolean jsonPayload) { diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java index a28019c6c0..af4cb021e1 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java @@ -18,26 +18,16 @@ package org.thingsboard.server.transport.coap; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.CoapServer; -import org.eclipse.californium.core.network.CoapEndpoint; -import org.eclipse.californium.core.network.config.NetworkConfig; -import org.eclipse.californium.core.server.resources.Resource; -import org.eclipse.californium.scandium.DTLSConnector; -import org.eclipse.californium.scandium.config.DtlsConnectorConfig; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; +import org.thingsboard.server.coapserver.CoapServerService; import org.thingsboard.server.transport.coap.efento.CoapEfentoTransportResource; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.UnknownHostException; -import java.util.Random; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; @Service("CoapTransportService") @ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')") @@ -50,87 +40,30 @@ public class CoapTransportService { private static final String MEASUREMENTS = "m"; @Autowired - private CoapTransportContext coapTransportContext; - - private TbCoapDtlsCertificateVerifier tbDtlsCertificateVerifier; + private CoapServerService coapServerService; - private CoapServer server; + @Autowired + private CoapTransportContext coapTransportContext; - private ScheduledExecutorService dtlsSessionsExecutor; + private CoapServer coapServer; @PostConstruct public void init() throws UnknownHostException { log.info("Starting CoAP transport..."); - log.info("Starting CoAP transport server"); - - this.server = new CoapServer(); - - CoapEndpoint.Builder capEndpointBuilder = new CoapEndpoint.Builder(); - - if (isDtlsEnabled()) { - TbCoapDtlsSettings dtlsSettings = coapTransportContext.getDtlsSettings(); - DtlsConnectorConfig dtlsConnectorConfig = dtlsSettings.dtlsConnectorConfig(); - DTLSConnector connector = new DTLSConnector(dtlsConnectorConfig); - capEndpointBuilder.setConnector(connector); - if (dtlsConnectorConfig.isClientAuthenticationRequired()) { - tbDtlsCertificateVerifier = (TbCoapDtlsCertificateVerifier) dtlsConnectorConfig.getAdvancedCertificateVerifier(); - dtlsSessionsExecutor = Executors.newSingleThreadScheduledExecutor(); - dtlsSessionsExecutor.scheduleAtFixedRate(this::evictTimeoutSessions, new Random().nextInt((int) getDtlsSessionReportTimeout()), getDtlsSessionReportTimeout(), TimeUnit.MILLISECONDS); - } - } else { - InetAddress addr = InetAddress.getByName(coapTransportContext.getHost()); - InetSocketAddress sockAddr = new InetSocketAddress(addr, coapTransportContext.getPort()); - capEndpointBuilder.setInetSocketAddress(sockAddr); - capEndpointBuilder.setNetworkConfig(NetworkConfig.getStandard()); - } - CoapEndpoint coapEndpoint = capEndpointBuilder.build(); - - server.addEndpoint(coapEndpoint); - - createResources(); - Resource root = this.server.getRoot(); - TbCoapServerMessageDeliverer messageDeliverer = new TbCoapServerMessageDeliverer(root); - this.server.setMessageDeliverer(messageDeliverer); - - server.start(); - log.info("CoAP transport started!"); - } - - private void createResources() { + coapServer = coapServerService.getCoapServer(); CoapResource api = new CoapResource(API); - api.add(new CoapTransportResource(coapTransportContext, getDtlsSessionsMap(), V1)); + api.add(new CoapTransportResource(coapTransportContext, coapServerService, V1)); CoapResource efento = new CoapResource(EFENTO); CoapEfentoTransportResource efentoMeasurementsTransportResource = new CoapEfentoTransportResource(coapTransportContext, MEASUREMENTS); efento.add(efentoMeasurementsTransportResource); - - server.add(api); - server.add(efento); - } - - private boolean isDtlsEnabled() { - return coapTransportContext.getDtlsSettings() != null; - } - - private ConcurrentMap getDtlsSessionsMap() { - return tbDtlsCertificateVerifier != null ? tbDtlsCertificateVerifier.getTbCoapDtlsSessionIdsMap() : null; - } - - private void evictTimeoutSessions() { - tbDtlsCertificateVerifier.evictTimeoutSessions(); - } - - private long getDtlsSessionReportTimeout() { - return tbDtlsCertificateVerifier.getDtlsSessionReportTimeout(); + coapServer.add(api); + coapServer.add(efento); + log.info("CoAP transport started!"); } @PreDestroy public void shutdown() { - if (dtlsSessionsExecutor != null) { - dtlsSessionsExecutor.shutdownNow(); - } - log.info("Stopping CoAP transport!"); - this.server.destroy(); log.info("CoAP transport stopped!"); } } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java index 1d4703ead4..b30c86307b 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.coap.adaptors; import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.coap.Response; import org.thingsboard.server.common.transport.adaptor.AdaptorException; @@ -32,7 +33,7 @@ public interface CoapTransportAdaptor { TransportProtos.GetAttributeRequestMsg convertToGetAttributes(UUID sessionId, Request inbound) throws AdaptorException; - TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound) throws AdaptorException; + TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound, Descriptors.Descriptor rpcResponseMsgDescriptor) throws AdaptorException; TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(UUID sessionId, Request inbound) throws AdaptorException; @@ -42,7 +43,7 @@ public interface CoapTransportAdaptor { Response convertToPublish(boolean isConfirmable, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) throws AdaptorException; - Response convertToPublish(boolean isConfirmable, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) throws AdaptorException; + Response convertToPublish(boolean isConfirmable, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) throws AdaptorException; Response convertToPublish(TransportProtos.ToServerRpcResponseMsg msg) throws AdaptorException; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java index e382b2a3ee..85d35301c3 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java @@ -20,6 +20,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Request; @@ -64,7 +65,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { } @Override - public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound) throws AdaptorException { + public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound, Descriptors.Descriptor rpcResponseMsgDescriptor) throws AdaptorException { Optional requestId = CoapTransportResource.getRequestId(inbound); String payload = validatePayload(sessionId, inbound, false); JsonObject response = new JsonParser().parse(payload).getAsJsonObject(); @@ -95,7 +96,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { } @Override - public Response convertToPublish(boolean isConfirmable, TransportProtos.ToDeviceRpcRequestMsg msg) throws AdaptorException { + public Response convertToPublish(boolean isConfirmable, TransportProtos.ToDeviceRpcRequestMsg msg, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) throws AdaptorException { return getObserveNotification(isConfirmable, JsonConverter.toJson(msg, true)); } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java index d4c53fdc9a..120e49f697 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.coap.adaptors; +import com.google.gson.JsonElement; import com.google.gson.JsonParser; import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; @@ -63,16 +64,16 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor { } @Override - public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound) throws AdaptorException { + public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound, Descriptors.Descriptor rpcResponseMsgDescriptor) throws AdaptorException { Optional requestId = CoapTransportResource.getRequestId(inbound); if (requestId.isEmpty()) { throw new AdaptorException("Request id is missing!"); } else { try { - String payload = TransportProtos.ToDeviceRpcResponseMsg.parseFrom(inbound.getPayload()).getPayload(); - return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.get()) - .setPayload(payload).build(); - } catch (InvalidProtocolBufferException e) { + JsonElement response = new JsonParser().parse(dynamicMsgToJson(inbound.getPayload(), rpcResponseMsgDescriptor)); + return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.orElseThrow(() -> new AdaptorException("Request id is missing!"))) + .setPayload(response.toString()).build(); + } catch (Exception e) { throw new AdaptorException(e); } } @@ -112,8 +113,8 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor { } @Override - public Response convertToPublish(boolean isConfirmable, TransportProtos.ToDeviceRpcRequestMsg msg) throws AdaptorException { - return getObserveNotification(isConfirmable, msg.toByteArray()); + public Response convertToPublish(boolean isConfirmable, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) throws AdaptorException { + return getObserveNotification(isConfirmable, ProtoConverter.convertToRpcRequest(rpcRequest, rpcRequestDynamicMessageBuilder)); } @Override diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java index ee2b3bac20..358ba499d4 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java @@ -35,12 +35,11 @@ import org.thingsboard.server.transport.lwm2m.secure.LwM2mCredentialsSecurityInf import org.thingsboard.server.transport.lwm2m.secure.ReadResultSecurityStore; import org.thingsboard.server.transport.lwm2m.server.LwM2mSessionMsgListener; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContextServer; -import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler; import org.thingsboard.server.transport.lwm2m.utils.TypeServer; import java.io.IOException; import java.security.GeneralSecurityException; -import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.UUID; @@ -70,8 +69,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { @Override public List getAllByEndpoint(String endPoint) { - String endPointKey = endPoint; - ReadResultSecurityStore store = lwM2MCredentialsSecurityInfoValidator.createAndValidateCredentialsSecurityInfo(endPointKey, TypeServer.BOOTSTRAP); + ReadResultSecurityStore store = lwM2MCredentialsSecurityInfoValidator.createAndValidateCredentialsSecurityInfo(endPoint, TypeServer.BOOTSTRAP); if (store.getBootstrapJsonCredential() != null && store.getSecurityMode() < LwM2MSecurityMode.DEFAULT_MODE.code) { /** add value to store from BootstrapJson */ this.setBootstrapConfigScurityInfo(store); @@ -87,7 +85,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { } catch (InvalidConfigurationException e) { log.error("", e); } - return store.getSecurityInfo() == null ? null : Arrays.asList(store.getSecurityInfo()); + return store.getSecurityInfo() == null ? null : Collections.singletonList(store.getSecurityInfo()); } } return null; @@ -166,13 +164,13 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { lwM2MBootstrapConfig.bootstrapServer = new LwM2MServerBootstrap(lwM2MBootstrapConfig.bootstrapServer, profileServerBootstrap); lwM2MBootstrapConfig.lwm2mServer = new LwM2MServerBootstrap(lwM2MBootstrapConfig.lwm2mServer, profileLwm2mServer); String logMsg = String.format("%s: getParametersBootstrap: %s Access connect client with bootstrap server.", LOG_LW2M_INFO, store.getEndPoint()); - context.sendParametersOnThingsboard(context.getTelemetryMsgObject(logMsg), LwM2mTransportHandler.DEVICE_TELEMETRY_TOPIC, sessionInfo); + context.sendParametersOnThingsboardTelemetry(context.getKvLogyToThingsboard(logMsg), sessionInfo); return lwM2MBootstrapConfig; } else { log.error(" [{}] Different values SecurityMode between of client and profile.", store.getEndPoint()); log.error("{} getParametersBootstrap: [{}] Different values SecurityMode between of client and profile.", LOG_LW2M_ERROR, store.getEndPoint()); String logMsg = String.format("%s: getParametersBootstrap: %s Different values SecurityMode between of client and profile.", LOG_LW2M_ERROR, store.getEndPoint()); - context.sendParametersOnThingsboard(context.getTelemetryMsgObject(logMsg), LwM2mTransportHandler.DEVICE_TELEMETRY_TOPIC, sessionInfo); + context.sendParametersOnThingsboardTelemetry(context.getKvLogyToThingsboard(logMsg), sessionInfo); return null; } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapServers.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapServers.java index e3757701cb..b08f1dbcbe 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapServers.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapServers.java @@ -23,5 +23,5 @@ public class LwM2MBootstrapServers { private Integer lifetime = 300; private Integer defaultMinPeriod = 1; private boolean notifIfDisabled = true; - private String binding = "U"; + private String binding = "UQ"; } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mQueuedRequest.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mQueuedRequest.java new file mode 100644 index 0000000000..dfc88a3331 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mQueuedRequest.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server; + +public interface LwM2mQueuedRequest { + void send(); +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java index ffe2f76602..9b2f3cb7eb 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java @@ -26,7 +26,7 @@ import org.eclipse.leshan.server.registration.RegistrationUpdate; import java.util.Collection; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToIdVerFromObjectId; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertPathFromObjectIdToIdVer; @Slf4j public class LwM2mServerListener { @@ -92,7 +92,7 @@ public class LwM2mServerListener { public void onResponse(Observation observation, Registration registration, ObserveResponse response) { if (registration != null) { try { - service.onObservationResponse(registration, convertToIdVerFromObjectId(observation.getPath().toString(), registration), response); + service.onObservationResponse(registration, convertPathFromObjectIdToIdVer(observation.getPath().toString(), registration), response); } catch (Exception e) { log.error("[{}] onResponse", e.toString()); @@ -107,7 +107,7 @@ public class LwM2mServerListener { @Override public void newObservation(Observation observation, Registration registration) { -// log.info("Received newObservation from [{}] endpoint [{}] ", observation.getPath(), registration.getEndpoint()); + log.info("Received newObservation from [{}] endpoint [{}] ", observation.getPath(), registration.getEndpoint()); } }; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java index 20baeba453..0bc78254a6 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.lwm2m.server; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.ResourceType; @@ -74,12 +75,12 @@ public class LwM2mSessionMsgListener implements GenericFutureListener resourceUpdateMsgOpt) { + @Override + public void onResourceUpdate(@NotNull Optional resourceUpdateMsgOpt) { if (ResourceType.LWM2M_MODEL.name().equals(resourceUpdateMsgOpt.get().getResourceType())) { this.service.onResourceUpdate(resourceUpdateMsgOpt); } } - public void onResourceDelete(Optional resourceDeleteMsgOpt) { + @Override + public void onResourceDelete(@NotNull Optional resourceDeleteMsgOpt) { if (ResourceType.LWM2M_MODEL.name().equals(resourceDeleteMsgOpt.get().getResourceType())) { this.service.onResourceDelete(resourceDeleteMsgOpt); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContextServer.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContextServer.java index 170530f084..ed0a1d16e6 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContextServer.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContextServer.java @@ -30,31 +30,33 @@ package org.thingsboard.server.transport.lwm2m.server; * limitations under the License. */ -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.model.DDFFileParser; import org.eclipse.leshan.core.model.DefaultDDFFileValidator; import org.eclipse.leshan.core.model.InvalidDDFFileException; import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.node.codec.CodecException; import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.TransportContext; import org.thingsboard.server.common.transport.TransportResourceCache; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; -import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.lwm2m.LwM2MTransportConfigServer; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.server.adaptors.LwM2MJsonAdaptor; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import static org.thingsboard.server.gen.transport.TransportProtos.KeyValueType.BOOLEAN_V; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_TELEMETRY; @Slf4j @@ -91,7 +93,7 @@ public class LwM2mTransportContextServer extends TransportContext { /** * send to Thingsboard Attribute || Telemetry * - * @param msg - JsonObject: [{name: value}] + * @param msg - JsonObject: [{name: value}] * @return - dummy */ private TransportServiceCallback getPubAckCallbackSendAttrTelemetry(final T msg) { @@ -108,33 +110,29 @@ public class LwM2mTransportContextServer extends TransportContext { }; } - public void sendParametersOnThingsboard(JsonElement msg, String topicName, SessionInfoProto sessionInfo) { - try { - if (topicName.equals(LwM2mTransportHandler.DEVICE_ATTRIBUTES_TOPIC)) { - PostAttributeMsg postAttributeMsg = adaptor.convertToPostAttributes(msg); - TransportServiceCallback call = this.getPubAckCallbackSendAttrTelemetry(postAttributeMsg); - transportService.process(sessionInfo, postAttributeMsg, this.getPubAckCallbackSendAttrTelemetry(call)); - } else if (topicName.equals(LwM2mTransportHandler.DEVICE_TELEMETRY_TOPIC)) { - PostTelemetryMsg postTelemetryMsg = adaptor.convertToPostTelemetry(msg); - TransportServiceCallback call = this.getPubAckCallbackSendAttrTelemetry(postTelemetryMsg); - transportService.process(sessionInfo, postTelemetryMsg, this.getPubAckCallbackSendAttrTelemetry(call)); - } - } catch (AdaptorException e) { - log.error("[{}] Failed to process publish msg [{}]", topicName, e); - log.info("[{}] Closing current session due to invalid publish", topicName); - } + public void sendParametersOnThingsboardAttribute(List result, SessionInfoProto sessionInfo) { + PostAttributeMsg.Builder request = PostAttributeMsg.newBuilder(); + request.addAllKv(result); + PostAttributeMsg postAttributeMsg = request.build(); + TransportServiceCallback call = this.getPubAckCallbackSendAttrTelemetry(postAttributeMsg); + transportService.process(sessionInfo, postAttributeMsg, this.getPubAckCallbackSendAttrTelemetry(call)); } - public JsonObject getTelemetryMsgObject(String logMsg) { - JsonObject telemetries = new JsonObject(); - telemetries.addProperty(LOG_LW2M_TELEMETRY, logMsg); - return telemetries; + public void sendParametersOnThingsboardTelemetry(List result, SessionInfoProto sessionInfo) { + PostTelemetryMsg.Builder request = PostTelemetryMsg.newBuilder(); + TransportProtos.TsKvListProto.Builder builder = TransportProtos.TsKvListProto.newBuilder(); + builder.setTs(System.currentTimeMillis()); + builder.addAllKv(result); + request.addTsKvList(builder.build()); + PostTelemetryMsg postTelemetryMsg = request.build(); + TransportServiceCallback call = this.getPubAckCallbackSendAttrTelemetry(postTelemetryMsg); + transportService.process(sessionInfo, postTelemetryMsg, this.getPubAckCallbackSendAttrTelemetry(call)); } /** * @return - sessionInfo after access connect client */ - public SessionInfoProto getValidateSessionInfo(ValidateDeviceCredentialsResponseMsg msg, long mostSignificantBits, long leastSignificantBits) { + public SessionInfoProto getValidateSessionInfo(TransportProtos.ValidateDeviceCredentialsResponseMsg msg, long mostSignificantBits, long leastSignificantBits) { return SessionInfoProto.newBuilder() .setNodeId(this.getNodeId()) .setSessionIdMSB(mostSignificantBits) @@ -159,4 +157,90 @@ public class LwM2mTransportContextServer extends TransportContext { return null; } } + + /** + * + * @param logMsg - info about Logs + * @return- KeyValueProto for telemetry (Logs) + */ + public List getKvLogyToThingsboard(String logMsg) { + List result = new ArrayList<>(); + result.add(TransportProtos.KeyValueProto.newBuilder() + .setKey(LOG_LW2M_TELEMETRY) + .setType(TransportProtos.KeyValueType.STRING_V) + .setStringV(logMsg).build()); + return result; + } + + /** + * @return - KeyValueProto for attribute/telemetry (change value) + * @throws CodecException - + */ + + public TransportProtos.KeyValueProto getKvAttrTelemetryToThingsboard(ResourceModel.Type resourceType, String resourceName, Object value, boolean isMultiInstances) { + TransportProtos.KeyValueProto.Builder kvProto = TransportProtos.KeyValueProto.newBuilder().setKey(resourceName); + if (isMultiInstances) { + kvProto.setType(TransportProtos.KeyValueType.JSON_V) + .setJsonV((String) value); + } + else { + switch (resourceType) { + case BOOLEAN: + kvProto.setType(BOOLEAN_V).setBoolV((Boolean) value).build(); + break; + case STRING: + case TIME: + case OPAQUE: + case OBJLNK: + kvProto.setType(TransportProtos.KeyValueType.STRING_V).setStringV((String) value); + break; + case INTEGER: + kvProto.setType(TransportProtos.KeyValueType.LONG_V).setLongV((Long) value); + break; + case FLOAT: + kvProto.setType(TransportProtos.KeyValueType.DOUBLE_V).setDoubleV((Double) value); + } + } + return kvProto.build(); + } + + /** + * + * @param currentType - + * @param resourcePath - + * @return + */ + public ResourceModel.Type getResourceModelTypeEqualsKvProtoValueType(ResourceModel.Type currentType, String resourcePath) { + switch (currentType) { + case BOOLEAN: + return ResourceModel.Type.BOOLEAN; + case STRING: + case TIME: + case OPAQUE: + case OBJLNK: + return ResourceModel.Type.STRING; + case INTEGER: + return ResourceModel.Type.INTEGER; + case FLOAT: + return ResourceModel.Type.FLOAT; + default: + } + throw new CodecException("Invalid ResourceModel_Type for resource %s, got %s", resourcePath, currentType); + } + + public Object getValueFromKvProto (TransportProtos.KeyValueProto kv) { + switch (kv.getType()) { + case BOOLEAN_V: + return kv.getBoolV(); + case LONG_V: + return kv.getLongV(); + case DOUBLE_V: + return kv.getDoubleV(); + case STRING_V: + return kv.getStringV(); + case JSON_V: + return kv.getJsonV(); + } + return null; + } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportHandler.java index 0f0707ddf8..86dd5b7960 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportHandler.java @@ -22,15 +22,14 @@ import com.google.gson.JsonSyntaxException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.eclipse.californium.core.network.config.NetworkConfig; +import org.eclipse.leshan.core.attributes.Attribute; +import org.eclipse.leshan.core.attributes.AttributeSet; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; -import org.eclipse.leshan.core.node.LwM2mMultipleResource; -import org.eclipse.leshan.core.node.LwM2mNode; -import org.eclipse.leshan.core.node.LwM2mObject; -import org.eclipse.leshan.core.node.LwM2mObjectInstance; import org.eclipse.leshan.core.node.LwM2mPath; -import org.eclipse.leshan.core.node.LwM2mSingleResource; import org.eclipse.leshan.core.node.codec.CodecException; +import org.eclipse.leshan.core.request.DownlinkRequest; +import org.eclipse.leshan.core.request.WriteAttributesRequest; import org.eclipse.leshan.core.util.Hex; import org.eclipse.leshan.server.californium.LeshanServerBuilder; import org.eclipse.leshan.server.registration.Registration; @@ -44,11 +43,19 @@ import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientProfile; import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.LinkedList; +import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import static org.eclipse.leshan.core.attributes.Attribute.DIMENSION; +import static org.eclipse.leshan.core.attributes.Attribute.MAXIMUM_PERIOD; +import static org.eclipse.leshan.core.attributes.Attribute.MINIMUM_PERIOD; +import static org.eclipse.leshan.core.attributes.Attribute.OBJECT_VERSION; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; @@ -56,8 +63,21 @@ import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPA public class LwM2mTransportHandler { public static final String BASE_DEVICE_API_TOPIC = "v1/devices/me"; + + public static final String CLIENT_LWM2M_SETTINGS = "clientLwM2mSettings"; + public static final String BOOTSTRAP = "bootstrap"; + public static final String SERVERS = "servers"; + public static final String LWM2M_SERVER = "lwm2mServer"; + public static final String BOOTSTRAP_SERVER = "bootstrapServer"; + public static final String OBSERVE_ATTRIBUTE_TELEMETRY = "observeAttr"; public static final String ATTRIBUTE = "attribute"; public static final String TELEMETRY = "telemetry"; + public static final String KEY_NAME = "keyName"; + public static final String OBSERVE = "observe"; + public static final String ATTRIBUTE_LWM2M = "attributeLwm2m"; + public static final String RESOURCE_VALUE = "resValue"; + public static final String RESOURCE_TYPE = "resType"; + private static final String REQUEST = "/request"; private static final String RESPONSE = "/response"; private static final String ATTRIBUTES = "/" + ATTRIBUTE; @@ -70,14 +90,6 @@ public class LwM2mTransportHandler { public static final String DEVICE_TELEMETRY_TOPIC = BASE_DEVICE_API_TOPIC + TELEMETRIES; public static final long DEFAULT_TIMEOUT = 2 * 60 * 1000L; // 2min in ms - public static final String OBSERVE_ATTRIBUTE_TELEMETRY = "observeAttr"; - public static final String CLIENT_LWM2M_SETTINGS = "clientLwM2mSettings"; - public static final String KEY_NAME = "keyName"; - public static final String OBSERVE = "observe"; - public static final String BOOTSTRAP = "bootstrap"; - public static final String SERVERS = "servers"; - public static final String LWM2M_SERVER = "lwm2mServer"; - public static final String BOOTSTRAP_SERVER = "bootstrapServer"; public static final String LOG_LW2M_TELEMETRY = "logLwm2m"; public static final String LOG_LW2M_INFO = "info"; @@ -144,19 +156,19 @@ public class LwM2mTransportHandler { throw new CodecException("Invalid value type for resource %s, type %s", resourcePath, type); } } - - public static LwM2mNode getLvM2mNodeToObject(LwM2mNode content) { - if (content instanceof LwM2mObject) { - return (LwM2mObject) content; - } else if (content instanceof LwM2mObjectInstance) { - return (LwM2mObjectInstance) content; - } else if (content instanceof LwM2mSingleResource) { - return (LwM2mSingleResource) content; - } else if (content instanceof LwM2mMultipleResource) { - return (LwM2mMultipleResource) content; - } - return null; - } +// +// public static LwM2mNode getLvM2mNodeToObject(LwM2mNode content) { +// if (content instanceof LwM2mObject) { +// return (LwM2mObject) content; +// } else if (content instanceof LwM2mObjectInstance) { +// return (LwM2mObjectInstance) content; +// } else if (content instanceof LwM2mSingleResource) { +// return (LwM2mSingleResource) content; +// } else if (content instanceof LwM2mMultipleResource) { +// return (LwM2mMultipleResource) content; +// } +// return null; +// } public static LwM2mClientProfile getNewProfileParameters(JsonObject profilesConfigData, TenantId tenantId) { LwM2mClientProfile lwM2MClientProfile = new LwM2mClientProfile(); @@ -166,6 +178,7 @@ public class LwM2mTransportHandler { lwM2MClientProfile.setPostAttributeProfile(profilesConfigData.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(ATTRIBUTE).getAsJsonArray()); lwM2MClientProfile.setPostTelemetryProfile(profilesConfigData.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(TELEMETRY).getAsJsonArray()); lwM2MClientProfile.setPostObserveProfile(profilesConfigData.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(OBSERVE).getAsJsonArray()); + lwM2MClientProfile.setPostAttributeLwm2mProfile(profilesConfigData.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(ATTRIBUTE_LWM2M).getAsJsonObject()); return lwM2MClientProfile; } @@ -184,6 +197,9 @@ public class LwM2mTransportHandler { * "attribute":["/2/0/1","/3/0/9"], * "telemetry":["/1/0/1","/2/0/1","/6/0/1"], * "observe":["/2/0","/2/0/0","/4/0/2"]} + * "attributeLwm2m": {"/3_1.0": {"ver": "currentTimeTest11"}, + * "/3_1.0/0": {"gt": 17}, + * "/3_1.0/0/9": {"pmax": 45}, "/3_1.2": {ver": "3_1.2"}} */ public static LwM2mClientProfile getLwM2MClientProfileFromThingsboard(DeviceProfile deviceProfile) { if (deviceProfile != null && ((Lwm2mDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration()).getProperties().size() > 0) { @@ -192,7 +208,7 @@ public class LwM2mTransportHandler { ObjectMapper mapper = new ObjectMapper(); String profileStr = mapper.writeValueAsString(profile); JsonObject profileJson = (profileStr != null) ? validateJson(profileStr) : null; - return (getValidateCredentialsBodyFromThingsboard(profileJson)) ? LwM2mTransportHandler.getNewProfileParameters(profileJson, deviceProfile.getTenantId()) : null; + return getValidateCredentialsBodyFromThingsboard(profileJson) ? LwM2mTransportHandler.getNewProfileParameters(profileJson, deviceProfile.getTenantId()) : null; } catch (IOException e) { log.error("", e); } @@ -240,7 +256,10 @@ public class LwM2mTransportHandler { objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(TELEMETRY).isJsonArray() && objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().has(OBSERVE) && !objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(OBSERVE).isJsonNull() && - objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(OBSERVE).isJsonArray()); + objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(OBSERVE).isJsonArray() && + objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().has(ATTRIBUTE_LWM2M) && + !objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(ATTRIBUTE_LWM2M).isJsonNull() && + objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(ATTRIBUTE_LWM2M).isJsonObject()); } private static boolean getValidateBootstrapProfileFromThingsboard(JsonObject objectMsg) { @@ -316,28 +335,30 @@ public class LwM2mTransportHandler { }; } - public static String convertToObjectIdFromIdVer(String key) { + public static String convertPathFromIdVerToObjectId(String pathIdVer) { try { - String[] keyArray = key.split(LWM2M_SEPARATOR_PATH); + String[] keyArray = pathIdVer.split(LWM2M_SEPARATOR_PATH); if (keyArray.length > 1 && keyArray[1].split(LWM2M_SEPARATOR_KEY).length == 2) { keyArray[1] = keyArray[1].split(LWM2M_SEPARATOR_KEY)[0]; return StringUtils.join(keyArray, LWM2M_SEPARATOR_PATH); - } else { - return key; + } + else { + return pathIdVer; } } catch (Exception e) { return null; } } - public static String convertToIdVerFromObjectId(String path, Registration registration) { + public static String convertPathFromObjectIdToIdVer(String path, Registration registration) { String ver = registration.getSupportedObject().get(new LwM2mPath(path).getObjectId()); try { String[] keyArray = path.split(LWM2M_SEPARATOR_PATH); if (keyArray.length > 1) { keyArray[1] = keyArray[1] + LWM2M_SEPARATOR_KEY + ver; return StringUtils.join(keyArray, LWM2M_SEPARATOR_PATH); - } else { + } + else { return path; } } catch (Exception e) { @@ -345,14 +366,6 @@ public class LwM2mTransportHandler { } } - public static Integer validateObjectIdFromKey(String key) { - try { - return Integer.parseInt(key.split(LWM2M_SEPARATOR_PATH)[1].split(LWM2M_SEPARATOR_KEY)[0]); - } catch (Exception e) { - return null; - } - } - public static String validateObjectVerFromKey(String key) { try { return (key.split(LWM2M_SEPARATOR_PATH)[1].split(LWM2M_SEPARATOR_KEY)[1]); @@ -360,4 +373,54 @@ public class LwM2mTransportHandler { return ObjectModel.DEFAULT_VERSION; } } + + /** + * As example: + * a)Write-Attributes/3/0/9?pmin=1 means the Battery Level value will be notified + * to the Server with a minimum interval of 1sec; + * this value is set at theResource level. + * b)Write-Attributes/3/0/9?pmin means the Battery Level will be notified + * to the Server with a minimum value (pmin) given by the default one + * (resource 2 of Object Server ID=1), + * or with another value if this Attribute has been set at another level + * (Object or Object Instance: see section5.1.1). + * c)Write-Attributes/3/0?pmin=10 means that all Resources of Instance 0 of the Object ‘Device (ID:3)’ + * will be notified to the Server with a minimum interval of 10 sec; + * this value is set at the Object Instance level. + * d)Write-Attributes /3/0/9?gt=45&st=10 means the Battery Level will be notified to the Server + * when: + * a.old value is 20 and new value is 35 due to step condition + * b.old value is 45 and new value is 50 due to gt condition + * c.old value is 50 and new value is 40 due to both gt and step conditions + * d.old value is 35 and new value is 20 due to step conditione) + * Write-Attributes /3/0/9?lt=20>=85&st=10 means the Battery Level will be notified to the Server + * when: + * a.old value is 17 and new value is 24 due to lt condition + * b.old value is 75 and new value is 90 due to both gt and step conditions + * String uriQueries = "pmin=10&pmax=60"; + * AttributeSet attributes = AttributeSet.parse(uriQueries); + * WriteAttributesRequest request = new WriteAttributesRequest(target, attributes); + * Attribute gt = new Attribute(GREATER_THAN, Double.valueOf("45")); + * Attribute st = new Attribute(LESSER_THAN, Double.valueOf("10")); + * Attribute pmax = new Attribute(MAXIMUM_PERIOD, "60"); + * Attribute [] attrs = {gt, st}; + */ + public static DownlinkRequest createWriteAttributeRequest(String target, Object params) { + AttributeSet attrSet = new AttributeSet(createWriteAttributes(params)); + return attrSet.getAttributes().size() > 0 ? new WriteAttributesRequest(target, attrSet) : null; + } + + private static Attribute[] createWriteAttributes(Object params) { + List attributeLists = new ArrayList(); + ObjectMapper oMapper = new ObjectMapper(); + Map map = oMapper.convertValue(params, ConcurrentHashMap.class); + map.forEach((k, v) -> { + if (!v.toString().isEmpty() || (v.toString().isEmpty() && OBJECT_VERSION.equals(k))) { + attributeLists.add(new Attribute(k, + (DIMENSION.equals(k) || MINIMUM_PERIOD.equals(k) || MAXIMUM_PERIOD.equals(k)) ? + ((Double) v).longValue() : v)); + } + }); + return (Attribute[]) attributeLists.toArray(Attribute[]::new); + } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java index ad1e542ce1..d717c90418 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java @@ -16,9 +16,8 @@ package org.thingsboard.server.transport.lwm2m.server; import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Response; -import org.eclipse.leshan.core.attributes.Attribute; -import org.eclipse.leshan.core.attributes.AttributeSet; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mPath; @@ -32,8 +31,8 @@ import org.eclipse.leshan.core.request.DownlinkRequest; import org.eclipse.leshan.core.request.ExecuteRequest; import org.eclipse.leshan.core.request.ObserveRequest; import org.eclipse.leshan.core.request.ReadRequest; -import org.eclipse.leshan.core.request.WriteAttributesRequest; import org.eclipse.leshan.core.request.WriteRequest; +import org.eclipse.leshan.core.request.exception.ClientSleepingException; import org.eclipse.leshan.core.response.CancelObservationResponse; import org.eclipse.leshan.core.response.DeleteResponse; import org.eclipse.leshan.core.response.DiscoverResponse; @@ -58,8 +57,6 @@ import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import static org.eclipse.californium.core.coap.CoAP.ResponseCode.isSuccess; -import static org.eclipse.leshan.core.attributes.Attribute.MINIMUM_PERIOD; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEFAULT_TIMEOUT; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_DISCOVER; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_OBSERVE; @@ -72,8 +69,9 @@ import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandle import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.PUT_TYPE_OPER_WRITE_ATTRIBUTES; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.PUT_TYPE_OPER_WRITE_UPDATE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.RESPONSE_CHANNEL; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToIdVerFromObjectId; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToObjectIdFromIdVer; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertPathFromIdVerToObjectId; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertPathFromObjectIdToIdVer; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.createWriteAttributeRequest; @Slf4j @Service @@ -116,13 +114,13 @@ public class LwM2mTransportRequest { */ public void sendAllRequest(Registration registration, String targetIdVer, String typeOper, String contentFormatParam, Observation observation, Object params, long timeoutInMs) { - String target = convertToObjectIdFromIdVer(targetIdVer); + String target = convertPathFromIdVerToObjectId(targetIdVer); LwM2mPath resultIds = new LwM2mPath(target); if (registration != null && resultIds.getObjectId() >= 0) { DownlinkRequest request = null; ContentFormat contentFormat = contentFormatParam != null ? ContentFormat.fromName(contentFormatParam.toUpperCase()) : null; LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); - ResourceModel resource = lwM2MClient.getResourceModel(target); + ResourceModel resource = null; timeoutInMs = timeoutInMs > 0 ? timeoutInMs : DEFAULT_TIMEOUT; switch (typeOper) { case GET_TYPE_OPER_READ: @@ -144,6 +142,7 @@ public class LwM2mTransportRequest { request = new CancelObservationRequest(observation); break; case POST_TYPE_OPER_EXECUTE: + resource = lwM2MClient.getResourceModel(targetIdVer); if (params != null && resource != null && !resource.multiple) { request = new ExecuteRequest(target, (String) this.converter.convertValue(params, resource.type, ResourceModel.Type.STRING, resultIds)); } else { @@ -152,6 +151,7 @@ public class LwM2mTransportRequest { break; case POST_TYPE_OPER_WRITE_REPLACE: // Request to write a String Single-Instance Resource using the TLV content format. + resource = lwM2MClient.getResourceModel(targetIdVer); if (resource != null && contentFormat != null) { // if (contentFormat.equals(ContentFormat.TLV) && !resource.multiple) { if (contentFormat.equals(ContentFormat.TLV)) { @@ -173,55 +173,21 @@ public class LwM2mTransportRequest { } break; case PUT_TYPE_OPER_WRITE_ATTRIBUTES: - /** - * As example: - * a)Write-Attributes/3/0/9?pmin=1 means the Battery Level value will be notified - * to the Server with a minimum interval of 1sec; - * this value is set at theResource level. - * b)Write-Attributes/3/0/9?pmin means the Battery Level will be notified - * to the Server with a minimum value (pmin) given by the default one - * (resource 2 of Object Server ID=1), - * or with another value if this Attribute has been set at another level - * (Object or Object Instance: see section5.1.1). - * c)Write-Attributes/3/0?pmin=10 means that all Resources of Instance 0 of the Object ‘Device (ID:3)’ - * will be notified to the Server with a minimum interval of 10 sec; - * this value is set at the Object Instance level. - * d)Write-Attributes /3/0/9?gt=45&st=10 means the Battery Level will be notified to the Server - * when: - * a.old value is 20 and new value is 35 due to step condition - * b.old value is 45 and new value is 50 due to gt condition - * c.old value is 50 and new value is 40 due to both gt and step conditions - * d.old value is 35 and new value is 20 due to step conditione) - * Write-Attributes /3/0/9?lt=20>=85&st=10 means the Battery Level will be notified to the Server - * when: - * a.old value is 17 and new value is 24 due to lt condition - * b.old value is 75 and new value is 90 due to both gt and step conditions - * String uriQueries = "pmin=10&pmax=60"; - * AttributeSet attributes = AttributeSet.parse(uriQueries); - * WriteAttributesRequest request = new WriteAttributesRequest(target, attributes); - * Attribute gt = new Attribute(GREATER_THAN, Double.valueOf("45")); - * Attribute st = new Attribute(LESSER_THAN, Double.valueOf("10")); - * Attribute pmax = new Attribute(MAXIMUM_PERIOD, "60"); - * Attribute [] attrs = {gt, st}; - */ - Attribute pmin = new Attribute(MINIMUM_PERIOD, Integer.toUnsignedLong(Integer.parseInt("1"))); - Attribute[] attrs = {pmin}; - AttributeSet attrSet = new AttributeSet(attrs); - if (resultIds.isResource()) { - request = new WriteAttributesRequest(resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId(), attrSet); - } else if (resultIds.isObjectInstance()) { - request = new WriteAttributesRequest(resultIds.getObjectId(), resultIds.getObjectInstanceId(), attrSet); - } else if (resultIds.getObjectId() >= 0) { - request = new WriteAttributesRequest(resultIds.getObjectId(), attrSet); - } + request = createWriteAttributeRequest(target, params); break; - default: } if (request != null) { - this.sendRequest(registration, lwM2MClient, request, timeoutInMs); - } - else { + try { + this.sendRequest(registration, lwM2MClient, request, timeoutInMs); + } catch (ClientSleepingException e) { + DownlinkRequest finalRequest = request; + long finalTimeoutInMs = timeoutInMs; + lwM2MClient.getQueuedRequests().add(() -> sendRequest(registration, lwM2MClient, finalRequest, finalTimeoutInMs)); + } catch (Exception e) { + log.error("[{}] [{}] [{}] Failed to send downlink.", registration.getEndpoint(), targetIdVer, typeOper, e); + } + } else { log.error("[{}], [{}] - [{}] error SendRequest", registration.getEndpoint(), typeOper, targetIdVer); } } @@ -238,17 +204,21 @@ public class LwM2mTransportRequest { private void sendRequest(Registration registration, LwM2mClient lwM2MClient, DownlinkRequest request, long timeoutInMs) { leshanServer.send(registration, request, timeoutInMs, (ResponseCallback) response -> { if (!lwM2MClient.isInit()) { - lwM2MClient.initValue(this.serviceImpl, convertToIdVerFromObjectId(request.getPath().toString(), registration)); + lwM2MClient.initValue(this.serviceImpl, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration)); } - if (isSuccess(((Response) response.getCoapResponse()).getCode())) { + if (CoAP.ResponseCode.isSuccess(((Response) response.getCoapResponse()).getCode())) { this.handleResponse(registration, request.getPath().toString(), response, request); if (request instanceof WriteRequest && ((WriteRequest) request).isReplaceRequest()) { + LwM2mNode node = ((WriteRequest) request).getNode(); + Object value = this.converter.convertValue(((LwM2mSingleResource) node).getValue(), + ((LwM2mSingleResource) node).getType(), ResourceModel.Type.STRING, request.getPath()); String msg = String.format("%s: sendRequest Replace: CoapCde - %s Lwm2m code - %d name - %s Resource path - %s value - %s SendRequest to Client", - LOG_LW2M_INFO, ((Response) response.getCoapResponse()).getCode(), response.getCode().getCode(), response.getCode().getName(), request.getPath().toString(), - ((LwM2mSingleResource) ((WriteRequest) request).getNode()).getValue().toString()); + LOG_LW2M_INFO, ((Response) response.getCoapResponse()).getCode(), response.getCode().getCode(), + response.getCode().getName(), request.getPath().toString(), value); serviceImpl.sendLogsToThingsboard(msg, registration); - log.info("[{}] [{}] - [{}] [{}] Update SendRequest[{}]", registration.getEndpoint(), ((Response) response.getCoapResponse()).getCode(), response.getCode(), request.getPath().toString(), - ((LwM2mSingleResource) ((WriteRequest) request).getNode()).getValue()); + log.debug("[{}] [{}] - [{}] [{}] Update SendRequest[{}]", registration.getEndpoint(), + ((Response) response.getCoapResponse()).getCode(), response.getCode(), + request.getPath().toString(), value); } } else { String msg = String.format("%s: sendRequest: CoapCode - %s Lwm2m code - %d name - %s Resource path - %s SendRequest to Client", LOG_LW2M_ERROR, @@ -258,7 +228,7 @@ public class LwM2mTransportRequest { } }, e -> { if (!lwM2MClient.isInit()) { - lwM2MClient.initValue(this.serviceImpl, convertToIdVerFromObjectId(request.getPath().toString(), registration)); + lwM2MClient.initValue(this.serviceImpl, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration)); } String msg = String.format("%s: sendRequest: Resource path - %s msg error - %s SendRequest to Client", LOG_LW2M_ERROR, request.getPath().toString(), e.toString()); @@ -283,7 +253,7 @@ public class LwM2mTransportRequest { case FLOAT: // Double return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Double.parseDouble(value.toString())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Double.parseDouble(value.toString())); case TIME: // Date - Date date = new Date(Long.decode(value.toString())); + Date date = new Date(Long.decode(value.toString())); return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, date) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, date); case OPAQUE: // byte[] value, base64 return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Hex.decodeHex(value.toString().toCharArray())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Hex.decodeHex(value.toString().toCharArray())); @@ -317,7 +287,7 @@ public class LwM2mTransportRequest { * @param response - */ private void sendResponse(Registration registration, String path, LwM2mResponse response, DownlinkRequest request) { - String pathIdVer = convertToIdVerFromObjectId(path, registration); + String pathIdVer = convertPathFromObjectIdToIdVer(path, registration); if (response instanceof ReadResponse) { serviceImpl.onObservationResponse(registration, pathIdVer, (ReadResponse) response); } else if (response instanceof CancelObservationResponse) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportService.java index fb917f1370..4f3cfc44c8 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportService.java @@ -51,6 +51,10 @@ public interface LwM2mTransportService { void onResourceDelete(Optional resourceDeleteMsgOpt); + void onToDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg toDeviceRequest); + + void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg toServerResponse); + void doTrigger(Registration registration, String path); void doDisconnect(TransportProtos.SessionInfoProto sessionInfo); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java index f8d2716808..05f386ad1a 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.lwm2m.server; import com.fasterxml.jackson.core.type.TypeReference; import com.google.common.collect.Sets; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -42,7 +43,6 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.adaptor.AdaptorException; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.common.transport.service.DefaultTransportService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; @@ -52,12 +52,12 @@ import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientProfile; +import org.thingsboard.server.transport.lwm2m.server.client.ResultsAddKeyValueProto; import org.thingsboard.server.transport.lwm2m.server.client.ResultsAnalyzerParameters; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; import javax.annotation.PostConstruct; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashSet; @@ -74,24 +74,22 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.thingsboard.server.common.transport.util.JsonUtils.getJsonObject; +import static org.eclipse.leshan.core.attributes.Attribute.OBJECT_VERSION; +import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.CLIENT_NOT_AUTHORIZED; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEVICE_ATTRIBUTES_REQUEST; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEVICE_ATTRIBUTES_TOPIC; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEVICE_TELEMETRY_TOPIC; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_DISCOVER; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_OBSERVE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_READ; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_ERROR; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_INFO; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_TELEMETRY; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LWM2M_STRATEGY_2; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.POST_TYPE_OPER_EXECUTE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.POST_TYPE_OPER_WRITE_REPLACE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.PUT_TYPE_OPER_WRITE_ATTRIBUTES; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.SERVICE_CHANNEL; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToIdVerFromObjectId; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToObjectIdFromIdVer; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertPathFromIdVerToObjectId; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.getAckCallback; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.validateObjectIdFromKey; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.validateObjectVerFromKey; @Slf4j @@ -146,9 +144,9 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * Next -> Create new LwM2MClient for current session -> setModelClient... * * @param registration - Registration LwM2M Client - * @param previousObsersations - may be null + * @param previousObservations - may be null */ - public void onRegistered(Registration registration, Collection previousObsersations) { + public void onRegistered(Registration registration, Collection previousObservations) { executorRegistered.submit(() -> { try { log.warn("[{}] [{{}] Client: create after Registration", registration.getEndpoint(), registration.getId()); @@ -160,6 +158,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { transportService.registerAsyncSession(sessionInfo, new LwM2mSessionMsgListener(this, sessionInfo)); transportService.process(sessionInfo, DefaultTransportService.getSessionEventMsg(SessionEvent.OPEN), null); transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build(), null); + transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.newBuilder().build(), null); this.initLwM2mFromClientValue(registration, lwM2MClient); this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client create after Registration", registration); } else { @@ -188,6 +187,13 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { LwM2mClient lwM2MClient = this.lwM2mClientContext.getLwM2MClient(sessionInfo); if (lwM2MClient.getDeviceId() == null && lwM2MClient.getProfileId() == null) { initLwM2mClient(lwM2MClient, sessionInfo); + } else { + if (registration.getBindingMode().useQueueMode()) { + LwM2mQueuedRequest request; + while ((request = lwM2MClient.getQueuedRequests().poll()) != null) { + request.send(); + } + } } log.info("Client: [{}] updatedReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType()); @@ -239,8 +245,11 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { } } + @Override public void onSleepingDev(Registration registration) { log.info("[{}] [{}] Received endpoint Sleeping version event", registration.getId(), registration.getEndpoint()); + this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client is sleeping!", registration); + //TODO: associate endpointId with device information. } @@ -296,33 +305,32 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { @Override public void onAttributeUpdate(AttributeUpdateNotificationMsg msg, TransportProtos.SessionInfoProto sessionInfo) { if (msg.getSharedUpdatedCount() > 0) { - JsonElement el = JsonConverter.toJson(msg); - el.getAsJsonObject().entrySet().forEach(de -> { - String pathIdVer = this.getPathAttributeUpdate(sessionInfo, de.getKey()); - String value = de.getValue().getAsString(); + msg.getSharedUpdatedList().forEach(tsKvProto -> { + String pathName = tsKvProto.getKv().getKey(); + String pathIdVer = this.validatePathIntoProfile(sessionInfo, pathName); + Object valueNew = this.lwM2mTransportContextServer.getValueFromKvProto(tsKvProto.getKv()); LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClient(new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB())); - LwM2mClientProfile clientProfile = lwM2mClientContext.getProfile(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); - if (pathIdVer != null && !pathIdVer.isEmpty() && (this.validatePathInAttrProfile(clientProfile, pathIdVer) || this.validatePathInTelemetryProfile(clientProfile, pathIdVer))) { + if (pathIdVer != null) { ResourceModel resourceModel = lwM2MClient.getResourceModel(pathIdVer); if (resourceModel != null && resourceModel.operations.isWritable()) { - lwM2mTransportRequest.sendAllRequest(lwM2MClient.getRegistration(), pathIdVer, POST_TYPE_OPER_WRITE_REPLACE, - ContentFormat.TLV.getName(), null, value, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout()); + this.updateResourcesValueToClient(lwM2MClient, this.getResourceValueFormatKv(lwM2MClient, pathIdVer), valueNew, pathIdVer); } else { - log.error("Resource path - [{}] value - [{}] is not Writable and cannot be updated", pathIdVer, value); + log.error("Resource path - [{}] value - [{}] is not Writable and cannot be updated", pathIdVer, valueNew); String logMsg = String.format("%s: attributeUpdate: Resource path - %s value - %s is not Writable and cannot be updated", - LOG_LW2M_ERROR, pathIdVer, value); + LOG_LW2M_ERROR, pathIdVer, valueNew); this.sendLogsToThingsboard(logMsg, lwM2MClient.getRegistration()); } } else { - log.error("Attribute name - [{}] value - [{}] is not present as attribute in profile and cannot be updated", de.getKey(), value); + log.error("Resource name name - [{}] value - [{}] is not present as attribute/telemetry in profile and cannot be updated", pathName, valueNew); String logMsg = String.format("%s: attributeUpdate: attribute name - %s value - %s is not present as attribute in profile and cannot be updated", - LOG_LW2M_ERROR, de.getKey(), value); + LOG_LW2M_ERROR, pathName, valueNew); this.sendLogsToThingsboard(logMsg, lwM2MClient.getRegistration()); } }); } else if (msg.getSharedDeletedCount() > 0) { log.info("[{}] delete [{}] onAttributeUpdate", msg.getSharedDeletedList(), sessionInfo); } + } /** @@ -355,17 +363,15 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { } /** - * * @param resourceUpdateMsgOpt - */ @Override - public void onResourceUpdate (Optional resourceUpdateMsgOpt) { + public void onResourceUpdate(Optional resourceUpdateMsgOpt) { String idVer = resourceUpdateMsgOpt.get().getResourceKey(); lwM2mClientContext.getLwM2mClients().values().stream().forEach(e -> e.updateResourceModel(idVer, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getModelProvider())); } /** - * * @param resourceDeleteMsgOpt - */ @Override @@ -374,6 +380,14 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { lwM2mClientContext.getLwM2mClients().values().stream().forEach(e -> e.deleteResources(pathIdVer, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getModelProvider())); } + public void onToDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg toDeviceRequest) { + log.info("[{}] toDeviceRpcRequest", toDeviceRequest); + } + + public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg toServerResponse) { + log.info("[{}] toServerRpcResponse", toServerResponse); + } + /** * Trigger Server path = "/1/0/8" *

@@ -417,6 +431,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { */ protected void onAwakeDev(Registration registration) { log.info("[{}] [{}] Received endpoint Awake version event", registration.getId(), registration.getEndpoint()); + this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client is awake!", registration); //TODO: associate endpointId with device information. } @@ -442,32 +457,13 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { } /** - * @param msg - text msg + * @param logMsg - text msg * @param registration - Id of Registration LwM2M Client */ - public void sendLogsToThingsboard(String msg, Registration registration) { - if (msg != null) { - JsonObject telemetries = new JsonObject(); - telemetries.addProperty(LOG_LW2M_TELEMETRY, msg); - this.updateParametersOnThingsboard(telemetries, DEVICE_TELEMETRY_TOPIC, registration); - } - } - - - /** - * // !!! Ok - * Prepare send to Thigsboard callback - Attribute or Telemetry - * - * @param msg - JsonArray: [{name: value}] - * @param topicName - Api Attribute or Telemetry - * @param registration - Id of Registration LwM2M Client - */ - public void updateParametersOnThingsboard(JsonElement msg, String topicName, Registration registration) { + public void sendLogsToThingsboard(String logMsg, Registration registration) { SessionInfoProto sessionInfo = this.getValidateSessionInfo(registration); - if (sessionInfo != null) { - lwM2mTransportContextServer.sendParametersOnThingsboard(msg, topicName, sessionInfo); - } else { - log.error("Client: [{}] updateParametersOnThingsboard [{}] sessionInfo ", registration, null); + if (logMsg != null && sessionInfo != null) { + this.lwM2mTransportContextServer.sendParametersOnThingsboardTelemetry(this.lwM2mTransportContextServer.getKvLogyToThingsboard(logMsg), sessionInfo); } } @@ -485,15 +481,20 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { */ private void initLwM2mFromClientValue(Registration registration, LwM2mClient lwM2MClient) { LwM2mClientProfile lwM2MClientProfile = lwM2mClientContext.getProfile(registration); - Set clientObjects = this.getAllOjectsInClient(registration); - if (clientObjects != null && LWM2M_STRATEGY_2 == LwM2mTransportHandler.getClientOnlyObserveAfterConnect(lwM2MClientProfile)) { - // #2 - lwM2MClient.getPendingRequests().addAll(clientObjects); - clientObjects.forEach(path -> lwM2mTransportRequest.sendAllRequest(registration, path, GET_TYPE_OPER_READ, ContentFormat.TLV.getName(), - null, null, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout())); + Set clientObjects = lwM2mClientContext.getSupportedIdVerInClient(registration); + if (clientObjects != null && clientObjects.size() > 0) { + if (LWM2M_STRATEGY_2 == LwM2mTransportHandler.getClientOnlyObserveAfterConnect(lwM2MClientProfile)) { + // #2 + lwM2MClient.getPendingRequests().addAll(clientObjects); + clientObjects.forEach(path -> lwM2mTransportRequest.sendAllRequest(registration, path, GET_TYPE_OPER_READ, ContentFormat.TLV.getName(), + null, null, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout())); + } + // #1 + this.initReadAttrTelemetryObserveToClient(registration, lwM2MClient, GET_TYPE_OPER_READ, clientObjects); + this.initReadAttrTelemetryObserveToClient(registration, lwM2MClient, GET_TYPE_OPER_OBSERVE, clientObjects); + this.initReadAttrTelemetryObserveToClient(registration, lwM2MClient, PUT_TYPE_OPER_WRITE_ATTRIBUTES, clientObjects); + this.initReadAttrTelemetryObserveToClient(registration, lwM2MClient, GET_TYPE_OPER_DISCOVER, clientObjects); } - // #1 - this.initReadAttrTelemetryObserveToClient(registration, lwM2MClient, GET_TYPE_OPER_OBSERVE); } /** @@ -552,17 +553,20 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * @param registration - Registration LwM2M Client */ private void updateAttrTelemetry(Registration registration, Set paths) { - JsonObject attributes = new JsonObject(); - JsonObject telemetries = new JsonObject(); try { - this.getParametersFromProfile(attributes, telemetries, registration, paths); + ResultsAddKeyValueProto results = getParametersFromProfile(registration, paths); + SessionInfoProto sessionInfo = this.getValidateSessionInfo(registration); + if (results != null && sessionInfo != null) { + if (results.getResultAttributes().size() > 0) { + this.lwM2mTransportContextServer.sendParametersOnThingsboardAttribute(results.getResultAttributes(), sessionInfo); + } + if (results.getResultTelemetries().size() > 0) { + this.lwM2mTransportContextServer.sendParametersOnThingsboardTelemetry(results.getResultTelemetries(), sessionInfo); + } + } } catch (Exception e) { log.error("UpdateAttrTelemetry", e); } - if (attributes.getAsJsonObject().entrySet().size() > 0) - this.updateParametersOnThingsboard(attributes, DEVICE_ATTRIBUTES_TOPIC, registration); - if (telemetries.getAsJsonObject().entrySet().size() > 0) - this.updateParametersOnThingsboard(telemetries, DEVICE_TELEMETRY_TOPIC, registration); } /** @@ -600,41 +604,61 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { /** * Start observe/read: Attr/Telemetry - * #1 - Analyze: - * #1.1 path in resource profile == client resource + * #1 - Analyze: path in resource profile == client resource * * @param registration - */ - private void initReadAttrTelemetryObserveToClient(Registration registration, LwM2mClient lwM2MClient, String typeOper) { + private void initReadAttrTelemetryObserveToClient(Registration registration, LwM2mClient lwM2MClient, + String typeOper, Set clientObjects) { LwM2mClientProfile lwM2MClientProfile = lwM2mClientContext.getProfile(registration); - Set clientInstances = this.getAllInstancesInClient(registration); - Set result; + Set result = null; + ConcurrentHashMap params = null; if (GET_TYPE_OPER_READ.equals(typeOper)) { - result = JacksonUtil.fromString(lwM2MClientProfile.getPostAttributeProfile().toString(), new TypeReference<>() { - }); - result.addAll(JacksonUtil.fromString(lwM2MClientProfile.getPostTelemetryProfile().toString(), new TypeReference<>() { - })); - } else { - result = JacksonUtil.fromString(lwM2MClientProfile.getPostObserveProfile().toString(), new TypeReference<>() { - }); + result = JacksonUtil.fromString(lwM2MClientProfile.getPostAttributeProfile().toString(), + new TypeReference<>() { + }); + result.addAll(JacksonUtil.fromString(lwM2MClientProfile.getPostTelemetryProfile().toString(), + new TypeReference<>() { + })); + } else if (GET_TYPE_OPER_OBSERVE.equals(typeOper)) { + result = JacksonUtil.fromString(lwM2MClientProfile.getPostObserveProfile().toString(), + new TypeReference<>() { + }); + } else if (GET_TYPE_OPER_DISCOVER.equals(typeOper)) { + result = this.getPathForWriteAttributes(lwM2MClientProfile.getPostAttributeLwm2mProfile()).keySet(); + ; + } else if (PUT_TYPE_OPER_WRITE_ATTRIBUTES.equals(typeOper)) { + params = this.getPathForWriteAttributes(lwM2MClientProfile.getPostAttributeLwm2mProfile()); + result = params.keySet(); } - Set pathSend = ConcurrentHashMap.newKeySet(); - result.forEach(target -> { - // #1.1 - String[] resPath = target.split("/"); - String instance = "/" + resPath[1] + "/" + resPath[2]; - if (clientInstances != null && clientInstances.size() > 0 && clientInstances.contains(instance)) { - pathSend.add(target); + if (!result.isEmpty()) { + // #1 + Set pathSend = result.stream().filter(target -> { + return target.split(LWM2M_SEPARATOR_PATH).length < 3 ? + clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1]) : + clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1] + "/" + target.split(LWM2M_SEPARATOR_PATH)[2]); + } + ) + .collect(Collectors.toUnmodifiableSet()); + if (!pathSend.isEmpty()) { + lwM2MClient.getPendingRequests().addAll(pathSend); + ConcurrentHashMap finalParams = params; + pathSend.forEach(target -> lwM2mTransportRequest.sendAllRequest(registration, target, typeOper, ContentFormat.TLV.getName(), + null, finalParams != null ? finalParams.get(target) : null, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout())); + if (GET_TYPE_OPER_OBSERVE.equals(typeOper)) { + lwM2MClient.initValue(this, null); + } } - }); - lwM2MClient.getPendingRequests().addAll(pathSend); - pathSend.forEach(target -> lwM2mTransportRequest.sendAllRequest(registration, target, typeOper, ContentFormat.TLV.getName(), - null, null, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout())); - if (GET_TYPE_OPER_OBSERVE.equals(typeOper)) { - lwM2MClient.initValue(this, null); } } + private ConcurrentHashMap getPathForWriteAttributes(JsonObject objectJson) { + ConcurrentHashMap pathAttributes = new Gson().fromJson(objectJson.toString(), + new TypeToken>() { + }.getType()); + return pathAttributes; + } + /** * Update parameters device in LwM2MClient * If new deviceProfile != old deviceProfile => update deviceProfile @@ -655,96 +679,109 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { } /** - * @param registration - - * @return - all object in client - */ - private Set getAllOjectsInClient(Registration registration) { - Set clientObjects = ConcurrentHashMap.newKeySet(); - Arrays.stream(registration.getObjectLinks()).forEach(url -> { - LwM2mPath pathIds = new LwM2mPath(url.getUrl()); - if (pathIds.isObjectInstance()) { - clientObjects.add("/" + pathIds.getObjectId()); - } - }); - return (clientObjects.size() > 0) ? clientObjects : null; - } - - /** - * @param registration - - * @return all instances in client - */ - private Set getAllInstancesInClient(Registration registration) { - Set clientInstances = ConcurrentHashMap.newKeySet(); - Arrays.stream(registration.getObjectLinks()).forEach(url -> { - LwM2mPath pathIds = new LwM2mPath(url.getUrl()); - if (pathIds.isObjectInstance()) { - clientInstances.add(convertToIdVerFromObjectId(url.getUrl(), registration)); - } - }); - return (clientInstances.size() > 0) ? clientInstances : null; - } - - /** - * @param attributes - new JsonObject - * @param telemetry - new JsonObject + * // * @param attributes - new JsonObject + * // * @param telemetry - new JsonObject + * * @param registration - Registration LwM2M Client * @param path - */ - private void getParametersFromProfile(JsonObject attributes, JsonObject telemetry, Registration registration, Set path) { + private ResultsAddKeyValueProto getParametersFromProfile(Registration registration, Set path) { if (path != null && path.size() > 0) { + ResultsAddKeyValueProto results = new ResultsAddKeyValueProto(); LwM2mClientProfile lwM2MClientProfile = lwM2mClientContext.getProfile(registration); - lwM2MClientProfile.getPostAttributeProfile().forEach(idVer -> { - if (path.contains(idVer.getAsString())) { - this.addParameters(idVer.getAsString(), attributes, registration); + List resultAttributes = new ArrayList<>(); + lwM2MClientProfile.getPostAttributeProfile().forEach(pathIdVer -> { + if (path.contains(pathIdVer.getAsString())) { + TransportProtos.KeyValueProto kvAttr = this.getKvToThingsboard(pathIdVer.getAsString(), registration); + if (kvAttr != null) { + resultAttributes.add(kvAttr); + } } }); - lwM2MClientProfile.getPostTelemetryProfile().forEach(idVer -> { - if (path.contains(idVer.getAsString())) { - this.addParameters(idVer.getAsString(), telemetry, registration); + List resultTelemetries = new ArrayList<>(); + lwM2MClientProfile.getPostTelemetryProfile().forEach(pathIdVer -> { + if (path.contains(pathIdVer.getAsString())) { + TransportProtos.KeyValueProto kvAttr = this.getKvToThingsboard(pathIdVer.getAsString(), registration); + if (kvAttr != null) { + resultTelemetries.add(kvAttr); + } } }); + if (resultAttributes.size() > 0) { + results.setResultAttributes(resultAttributes); + } + if (resultTelemetries.size() > 0) { + results.setResultTelemetries(resultTelemetries); + } + return results; } + return null; } - /** - * @param parameters - JsonObject attributes/telemetry - * @param registration - Registration LwM2M Client - */ - private void addParameters(String path, JsonObject parameters, Registration registration) { - LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); +// public TransportProtos.KeyValueProto getKvToThingsboard(String pathIdVer, Registration registration) { +// ResultsResourceValue resultsResourceValue = getResultsResourceValue(pathIdVer, registration); +// return resultsResourceValue != null ? this.lwM2mTransportContextServer.getKvAttrTelemetryToThingsboard(resultsResourceValue) : null; +// } + + private TransportProtos.KeyValueProto getKvToThingsboard(String pathIdVer, Registration registration) { + LwM2mClient lwM2MClient = this.lwM2mClientContext.getLwM2mClientWithReg(null, registration.getId()); JsonObject names = lwM2mClientContext.getProfiles().get(lwM2MClient.getProfileId()).getPostKeyNameProfile(); - String resName = names.get(path).getAsString(); - if (resName != null && !resName.isEmpty()) { - try { - String resValue = this.getResourceValueToString(lwM2MClient, path); - if (resValue != null) { - parameters.addProperty(resName, resValue); + if (names != null && names.has(pathIdVer)) { + String resourceName = names.get(pathIdVer).getAsString(); + if (resourceName != null && !resourceName.isEmpty()) { + try { + LwM2mResource resourceValue = lwM2MClient != null ? getResourceValueFromLwM2MClient(lwM2MClient, pathIdVer) : null; + if (resourceValue != null) { + ResourceModel.Type currentType = resourceValue.getType(); + ResourceModel.Type expectedType = this.lwM2mTransportContextServer.getResourceModelTypeEqualsKvProtoValueType(currentType, pathIdVer); + Object valueKvProto = null; + if (resourceValue.isMultiInstances()) { + valueKvProto = new JsonObject(); + Object finalvalueKvProto = valueKvProto; + Gson gson = new GsonBuilder().create(); + resourceValue.getValues().forEach((k, v) -> { + Object val = this.converter.convertValue(resourceValue.getValue(), currentType, expectedType, + new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer))); + JsonElement element = gson.toJsonTree(val, val.getClass()); + ((JsonObject) finalvalueKvProto).add(String.valueOf(k), element); + }); + valueKvProto = gson.toJson(valueKvProto); + } else { + valueKvProto = this.converter.convertValue(resourceValue.getValue(), currentType, expectedType, + new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer))); + } + return valueKvProto != null ? this.lwM2mTransportContextServer.getKvAttrTelemetryToThingsboard(currentType, resourceName, valueKvProto, resourceValue.isMultiInstances()) : null; + } + } catch (Exception e) { + log.error("Failed to add parameters.", e); } - } catch (Exception e) { - log.error("Failed to add parameters.", e); } + } else { + log.error("Failed to add parameters. path: [{}], names: [{}]", pathIdVer, names); } + return null; } /** - * @param path - path resource - * @return - value of Resource or null + * @param pathIdVer - path resource + * @return - value of Resource into format KvProto or null */ - private String getResourceValueToString(LwM2mClient lwM2MClient, String path) { - LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(path)); - LwM2mResource resourceValue = this.returnResourceValueFromLwM2MClient(lwM2MClient, path); - return resourceValue == null ? null : - this.converter.convertValue(resourceValue.isMultiInstances() ? resourceValue.getValues() : resourceValue.getValue(), resourceValue.getType(), ResourceModel.Type.STRING, pathIds).toString(); + private Object getResourceValueFormatKv(LwM2mClient lwM2MClient, String pathIdVer) { + LwM2mResource resourceValue = this.getResourceValueFromLwM2MClient(lwM2MClient, pathIdVer); + ResourceModel.Type currentType = resourceValue.getType(); + ResourceModel.Type expectedType = this.lwM2mTransportContextServer.getResourceModelTypeEqualsKvProtoValueType(currentType, pathIdVer); + return this.converter.convertValue(resourceValue.getValue(), currentType, expectedType, + new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer))); } /** * @param lwM2MClient - - * @param path - + * @param path - * @return - return value of Resource by idPath */ - private LwM2mResource returnResourceValueFromLwM2MClient(LwM2mClient lwM2MClient, String path) { + private LwM2mResource getResourceValueFromLwM2MClient(LwM2mClient lwM2MClient, String path) { LwM2mResource resourceValue = null; - if (new LwM2mPath(convertToObjectIdFromIdVer(path)).isResource()) { + if (new LwM2mPath(convertPathFromIdVerToObjectId(path)).isResource()) { resourceValue = lwM2MClient.getResources().get(path).getLwM2mResource(); } return resourceValue; @@ -768,6 +805,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * #3.1 Attribute isChange (add&del) * #3.2 Telemetry isChange (add&del) * #3.3 KeyName isChange (add) + * #3.4 attributeLwm2m isChange (update WrightAttribute: add/update/del) * #4 update * #4.1 add If #3 isChange, then analyze and update Value in Transport form Client and send Value to thingsboard * #4.2 del @@ -778,6 +816,9 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * -- path Attr/Telemetry includes newObserve and does not include oldObserve: send Request observe to Client * #5.3 Observe.del * -- different between newObserve and oldObserve: send Request cancel observe to client + * #6 + * #6.1 - update WriteAttribute + * #6.2 - del WriteAttribute * * @param registrationIds - * @param deviceProfile - @@ -792,6 +833,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { Set telemetrySetOld = this.convertJsonArrayToSet(telemetryOld); JsonArray observeOld = lwM2MClientProfileOld.getPostObserveProfile(); JsonObject keyNameOld = lwM2MClientProfileOld.getPostKeyNameProfile(); + JsonObject attributeLwm2mOld = lwM2MClientProfileOld.getPostAttributeLwm2mProfile(); LwM2mClientProfile lwM2MClientProfileNew = lwM2mClientContext.getProfiles().get(deviceProfile.getUuidId()); JsonArray attributeNew = lwM2MClientProfileNew.getPostAttributeProfile(); @@ -800,32 +842,41 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { Set telemetrySetNew = this.convertJsonArrayToSet(telemetryNew); JsonArray observeNew = lwM2MClientProfileNew.getPostObserveProfile(); JsonObject keyNameNew = lwM2MClientProfileNew.getPostKeyNameProfile(); + JsonObject attributeLwm2mNew = lwM2MClientProfileNew.getPostAttributeLwm2mProfile(); // #3 ResultsAnalyzerParameters sendAttrToThingsboard = new ResultsAnalyzerParameters(); // #3.1 if (!attributeOld.equals(attributeNew)) { - ResultsAnalyzerParameters postAttributeAnalyzer = this.getAnalyzerParameters(new Gson().fromJson(attributeOld, new TypeToken>() { - }.getType()), attributeSetNew); + ResultsAnalyzerParameters postAttributeAnalyzer = this.getAnalyzerParameters(new Gson().fromJson(attributeOld, + new TypeToken>() { + }.getType()), attributeSetNew); sendAttrToThingsboard.getPathPostParametersAdd().addAll(postAttributeAnalyzer.getPathPostParametersAdd()); sendAttrToThingsboard.getPathPostParametersDel().addAll(postAttributeAnalyzer.getPathPostParametersDel()); } // #3.2 if (!telemetryOld.equals(telemetryNew)) { - ResultsAnalyzerParameters postTelemetryAnalyzer = this.getAnalyzerParameters(new Gson().fromJson(telemetryOld, new TypeToken>() { - }.getType()), telemetrySetNew); + ResultsAnalyzerParameters postTelemetryAnalyzer = this.getAnalyzerParameters(new Gson().fromJson(telemetryOld, + new TypeToken>() { + }.getType()), telemetrySetNew); sendAttrToThingsboard.getPathPostParametersAdd().addAll(postTelemetryAnalyzer.getPathPostParametersAdd()); sendAttrToThingsboard.getPathPostParametersDel().addAll(postTelemetryAnalyzer.getPathPostParametersDel()); } // #3.3 if (!keyNameOld.equals(keyNameNew)) { - ResultsAnalyzerParameters keyNameChange = this.getAnalyzerKeyName(new Gson().fromJson(keyNameOld.toString(), new TypeToken>() { + ResultsAnalyzerParameters keyNameChange = this.getAnalyzerKeyName(new Gson().fromJson(keyNameOld.toString(), + new TypeToken>() { }.getType()), new Gson().fromJson(keyNameNew.toString(), new TypeToken>() { }.getType())); sendAttrToThingsboard.getPathPostParametersAdd().addAll(keyNameChange.getPathPostParametersAdd()); } + // #3.4, #6 + if (!attributeLwm2mOld.equals(attributeLwm2mNew)) { + this.getAnalyzerAttributeLwm2m(registrationIds, attributeLwm2mOld, attributeLwm2mNew); + } + // #4.1 add if (sendAttrToThingsboard.getPathPostParametersAdd().size() > 0) { // update value in Resources @@ -859,10 +910,14 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { // send Request observe to Client registrationIds.forEach(registrationId -> { Registration registration = lwM2mClientContext.getRegistration(registrationId); - this.readResourceValueObserve(registration, postObserveAnalyzer.getPathPostParametersAdd(), GET_TYPE_OPER_OBSERVE); + if (postObserveAnalyzer.getPathPostParametersAdd().size() > 0) { + this.readResourceValueObserve(registration, postObserveAnalyzer.getPathPostParametersAdd(), GET_TYPE_OPER_OBSERVE); + } // 5.3 del // send Request cancel observe to Client - this.cancelObserveIsValue(registration, postObserveAnalyzer.getPathPostParametersDel()); + if (postObserveAnalyzer.getPathPostParametersDel().size() > 0) { + this.cancelObserveIsValue(registration, postObserveAnalyzer.getPathPostParametersDel()); + } }); } } @@ -909,7 +964,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { */ private void readResourceValueObserve(Registration registration, Set targets, String typeOper) { targets.forEach(target -> { - LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(target)); + LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(target)); if (pathIds.isResource()) { if (GET_TYPE_OPER_READ.equals(typeOper)) { lwM2mTransportRequest.sendAllRequest(registration, target, typeOper, @@ -922,7 +977,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { }); } - private ResultsAnalyzerParameters getAnalyzerKeyName(ConcurrentMap keyNameOld, ConcurrentMap keyNameNew) { + private ResultsAnalyzerParameters getAnalyzerKeyName(ConcurrentHashMap keyNameOld, ConcurrentHashMap keyNameNew) { ResultsAnalyzerParameters analyzerParameters = new ResultsAnalyzerParameters(); Set paths = keyNameNew.entrySet() .stream() @@ -932,22 +987,91 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { return analyzerParameters; } + /** + * #3.4, #6 + * #6 + * #6.1 - send update WriteAttribute + * #6.2 - send empty WriteAttribute + * + * @param attributeLwm2mOld - + * @param attributeLwm2mNew - + * @return + */ + private void getAnalyzerAttributeLwm2m(Set registrationIds, JsonObject attributeLwm2mOld, JsonObject attributeLwm2mNew) { + ResultsAnalyzerParameters analyzerParameters = new ResultsAnalyzerParameters(); + ConcurrentHashMap lwm2mAttributesOld = new Gson().fromJson(attributeLwm2mOld.toString(), + new TypeToken>() { + }.getType()); + ConcurrentHashMap lwm2mAttributesNew = new Gson().fromJson(attributeLwm2mNew.toString(), + new TypeToken>() { + }.getType()); + Set pathOld = lwm2mAttributesOld.keySet(); + Set pathNew = lwm2mAttributesNew.keySet(); + analyzerParameters.setPathPostParametersAdd(pathNew + .stream().filter(p -> !pathOld.contains(p)).collect(Collectors.toSet())); + analyzerParameters.setPathPostParametersDel(pathOld + .stream().filter(p -> !pathNew.contains(p)).collect(Collectors.toSet())); + Set pathCommon = pathNew + .stream().filter(p -> pathOld.contains(p)).collect(Collectors.toSet()); + Set pathCommonChange = pathCommon + .stream().filter(p -> !lwm2mAttributesOld.get(p).equals(lwm2mAttributesNew.get(p))).collect(Collectors.toSet()); + analyzerParameters.getPathPostParametersAdd().addAll(pathCommonChange); + // #6 + // #6.2 + if (analyzerParameters.getPathPostParametersAdd().size() > 0) { + registrationIds.forEach(registrationId -> { + Registration registration = this.lwM2mClientContext.getRegistration(registrationId); + Set clientObjects = lwM2mClientContext.getSupportedIdVerInClient(registration); + Set pathSend = analyzerParameters.getPathPostParametersAdd().stream().filter(target -> clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1])) + .collect(Collectors.toUnmodifiableSet()); + if (!pathSend.isEmpty()) { + ConcurrentHashMap finalParams = lwm2mAttributesNew; + pathSend.forEach(target -> lwM2mTransportRequest.sendAllRequest(registration, target, PUT_TYPE_OPER_WRITE_ATTRIBUTES, ContentFormat.TLV.getName(), + null, finalParams.get(target), this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout())); + } + }); + } + // #6.2 + if (analyzerParameters.getPathPostParametersDel().size() > 0) { + registrationIds.forEach(registrationId -> { + Registration registration = this.lwM2mClientContext.getRegistration(registrationId); + Set clientObjects = lwM2mClientContext.getSupportedIdVerInClient(registration); + Set pathSend = analyzerParameters.getPathPostParametersDel().stream().filter(target -> clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1])) + .collect(Collectors.toUnmodifiableSet()); + if (!pathSend.isEmpty()) { + pathSend.forEach(target -> { + Map params = (Map) lwm2mAttributesOld.get(target); + params.clear(); + params.put(OBJECT_VERSION, ""); + lwM2mTransportRequest.sendAllRequest(registration, target, PUT_TYPE_OPER_WRITE_ATTRIBUTES, ContentFormat.TLV.getName(), + null, params, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout()); + }); + } + }); + } + + } + private void cancelObserveIsValue(Registration registration, Set paramAnallyzer) { LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); paramAnallyzer.forEach(p -> { - if (this.returnResourceValueFromLwM2MClient(lwM2MClient, p) != null) { - this.setCancelObservationRecourse(registration, convertToObjectIdFromIdVer(p)); + if (this.getResourceValueFromLwM2MClient(lwM2MClient, p) != null) { + this.setCancelObservationRecourse(registration, convertPathFromIdVerToObjectId(p)); } } ); } - private void putDelayedUpdateResourcesClient(LwM2mClient lwM2MClient, Object valueOld, Object valueNew, String path) { + private void updateResourcesValueToClient(LwM2mClient lwM2MClient, Object valueOld, Object valueNew, String path) { if (valueNew != null && (valueOld == null || !valueNew.toString().equals(valueOld.toString()))) { lwM2mTransportRequest.sendAllRequest(lwM2MClient.getRegistration(), path, POST_TYPE_OPER_WRITE_REPLACE, ContentFormat.TLV.getName(), null, valueNew, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout()); } else { - log.error("05 delayError"); + log.error("Failed update resource [{}] [{}]", path, valueNew); + String logMsg = String.format("%s: Failed update resource path - %s value - %s. Value is not changed or bad", + LOG_LW2M_ERROR, path, valueNew); + this.sendLogsToThingsboard(logMsg, lwM2MClient.getRegistration()); + log.info("Failed update resource [{}] [{}]", path, valueNew); } } @@ -967,9 +1091,9 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * @param name - * @return path if path isPresent in postProfile */ - private String getPathAttributeUpdate(TransportProtos.SessionInfoProto sessionInfo, String name) { - String profilePath = this.getPathAttributeUpdateProfile(sessionInfo, name); - return !profilePath.isEmpty() ? profilePath : null; + private String validatePathIntoProfile(TransportProtos.SessionInfoProto sessionInfo, String name) { + String pathIdVer = this.getPresentPathIntoProfile(sessionInfo, name); + return !pathIdVer.isEmpty() ? pathIdVer : null; } /** @@ -979,7 +1103,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * @param name - * @return - */ - private String getPathAttributeUpdateProfile(TransportProtos.SessionInfoProto sessionInfo, String name) { + private String getPresentPathIntoProfile(TransportProtos.SessionInfoProto sessionInfo, String name) { LwM2mClientProfile profile = lwM2mClientContext.getProfile(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); LwM2mClient lwM2mClient = lwM2mClientContext.getLwM2MClient(sessionInfo); return profile.getPostKeyNameProfile().getAsJsonObject().entrySet().stream() @@ -990,40 +1114,49 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { /** * Update resource value on client: if there is a difference in values between the current resource values and the shared attribute values * #1 Get path resource by result attributesResponse - * #1.1 If two names have equal path => last time attribute - * #2.1 if there is a difference in values between the current resource values and the shared attribute values - * => send to client Request Update of value (new value from shared attribute) - * and LwM2MClient.delayedRequests.add(path) - * #2.1 if there is not a difference in values between the current resource values and the shared attribute values * * @param attributesResponse - * @param sessionInfo - */ public void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg attributesResponse, TransportProtos.SessionInfoProto sessionInfo) { try { - LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2MClient(sessionInfo); - attributesResponse.getSharedAttributeListList().forEach(attr -> { - String path = this.getPathAttributeUpdate(sessionInfo, attr.getKv().getKey()); - if (path != null) { - // #1.1 - if (lwM2MClient.getDelayedRequests().containsKey(path) && attr.getTs() > lwM2MClient.getDelayedRequests().get(path).getTs()) { - lwM2MClient.getDelayedRequests().put(path, attr); - } else { - lwM2MClient.getDelayedRequests().put(path, attr); - } - } - }); - // #2.1 - lwM2MClient.getDelayedRequests().forEach((k, v) -> { - ArrayList listV = new ArrayList<>(); - listV.add(v.getKv()); - this.putDelayedUpdateResourcesClient(lwM2MClient, this.getResourceValueToString(lwM2MClient, k), getJsonObject(listV).get(v.getKv().getKey()), k); - }); + List tsKvProtos = attributesResponse.getSharedAttributeListList(); + this.updateAttriuteFromThingsboard(tsKvProtos, sessionInfo); } catch (Exception e) { log.error(String.valueOf(e)); } } + /** + * #1.1 If two names have equal path => last time attribute + * #2.1 if there is a difference in values between the current resource values and the shared attribute values + * => send to client Request Update of value (new value from shared attribute) + * and LwM2MClient.delayedRequests.add(path) + * #2.1 if there is not a difference in values between the current resource values and the shared attribute values + * + * @param tsKvProtos + * @param sessionInfo + */ + public void updateAttriuteFromThingsboard(List tsKvProtos, TransportProtos.SessionInfoProto sessionInfo) { + LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2MClient(sessionInfo); + tsKvProtos.forEach(tsKvProto -> { + String pathIdVer = this.validatePathIntoProfile(sessionInfo, tsKvProto.getKv().getKey()); + if (pathIdVer != null) { + // #1.1 + if (lwM2MClient.getDelayedRequests().containsKey(pathIdVer) && tsKvProto.getTs() > lwM2MClient.getDelayedRequests().get(pathIdVer).getTs()) { + lwM2MClient.getDelayedRequests().put(pathIdVer, tsKvProto); + } else if (!lwM2MClient.getDelayedRequests().containsKey(pathIdVer)) { + lwM2MClient.getDelayedRequests().put(pathIdVer, tsKvProto); + } + } + }); + // #2.1 + lwM2MClient.getDelayedRequests().forEach((pathIdVer, tsKvProto) -> { + this.updateResourcesValueToClient(lwM2MClient, this.getResourceValueFormatKv(lwM2MClient, pathIdVer), + this.lwM2mTransportContextServer.getValueFromKvProto(tsKvProto.getKv()), pathIdVer); + }); + } + /** * @param lwM2MClient - * @return SessionInfoProto - @@ -1140,11 +1273,11 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { return new ArrayList<>(namesIsWritable); } - private boolean validateResourceInModel(LwM2mClient lwM2mClient, String pathKey, boolean isWritable) { - ResourceModel resourceModel = lwM2mClient.getResourceModel(pathKey); - Integer objectId = validateObjectIdFromKey(pathKey); - String objectVer = validateObjectVerFromKey(pathKey); - return resourceModel != null && (isWritable ? + private boolean validateResourceInModel(LwM2mClient lwM2mClient, String pathIdVer, boolean isWritableNotOptional) { + ResourceModel resourceModel = lwM2mClient.getResourceModel(pathIdVer); + Integer objectId = new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer)).getObjectId(); + String objectVer = validateObjectVerFromKey(pathIdVer); + return resourceModel != null && (isWritableNotOptional ? objectId != null && objectVer != null && objectVer.equals(lwM2mClient.getRegistration().getSupportedVersion(objectId)) && resourceModel.operations.isWritable() : objectId != null && objectVer != null && objectVer.equals(lwM2mClient.getRegistration().getSupportedVersion(objectId))); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index bdb783e3ff..5e736d8e0f 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -25,18 +25,21 @@ import org.eclipse.leshan.server.registration.Registration; import org.eclipse.leshan.server.security.SecurityInfo; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; +import org.thingsboard.server.transport.lwm2m.server.LwM2mQueuedRequest; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServiceImpl; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToObjectIdFromIdVer; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertPathFromIdVerToObjectId; @Slf4j @Data @@ -54,6 +57,7 @@ public class LwM2mClient implements Cloneable { private final Map resources; private final Map delayedRequests; private final List pendingRequests; + private final Queue queuedRequests; private boolean init; public Object clone() throws CloneNotSupportedException { @@ -71,6 +75,7 @@ public class LwM2mClient implements Cloneable { this.profileId = profileId; this.sessionId = sessionId; this.init = false; + this.queuedRequests = new ConcurrentLinkedQueue<>(); } public boolean saveResourceValue(String pathRez, LwM2mResource rez, LwM2mModelProvider modelProvider) { @@ -78,7 +83,7 @@ public class LwM2mClient implements Cloneable { this.resources.get(pathRez).setLwM2mResource(rez); return true; } else { - LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(pathRez)); + LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathRez)); ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); if (resourceModel != null) { this.resources.put(pathRez, new ResourceValue(rez, resourceModel)); @@ -103,9 +108,9 @@ public class LwM2mClient implements Cloneable { * @param modelProvider - */ public void deleteResources(String pathIdVer, LwM2mModelProvider modelProvider) { - Set key = getKeysEqualsIdVer(pathIdVer); + Set key = getKeysEqualsIdVer(pathIdVer); key.forEach(pathRez -> { - LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(pathRez.toString())); + LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathRez)); ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); if (resourceModel != null) { this.resources.get(pathRez).setResourceModel(resourceModel); @@ -122,17 +127,17 @@ public class LwM2mClient implements Cloneable { * @param modelProvider - */ public void updateResourceModel(String idVer, LwM2mModelProvider modelProvider) { - Set key = getKeysEqualsIdVer(idVer); - key.forEach(k -> this.saveResourceModel(k.toString(), modelProvider)); + Set key = getKeysEqualsIdVer(idVer); + key.forEach(k -> this.saveResourceModel(k, modelProvider)); } private void saveResourceModel(String pathRez, LwM2mModelProvider modelProvider) { - LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(pathRez)); + LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathRez)); ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); this.resources.get(pathRez).setResourceModel(resourceModel); } - private Set getKeysEqualsIdVer(String idVer) { + private Set getKeysEqualsIdVer(String idVer) { return this.resources.keySet() .stream() .filter(e -> idVer.equals(e.split(LWM2M_SEPARATOR_PATH)[1])) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java index 2aea3fdc3e..358f760fef 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.gen.transport.TransportProtos; import java.util.Map; +import java.util.Set; import java.util.UUID; public interface LwM2mClientContext { @@ -51,4 +52,6 @@ public interface LwM2mClientContext { Map setProfiles(Map profiles); boolean addUpdateProfileParameters(DeviceProfile deviceProfile); + + Set getSupportedIdVerInClient(Registration registration); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index 926d12516b..cca729f5ff 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.lwm2m.server.client; +import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.server.registration.Registration; import org.eclipse.leshan.server.security.EditableSecurityStore; import org.springframework.stereotype.Service; @@ -27,11 +28,14 @@ import org.thingsboard.server.transport.lwm2m.secure.ReadResultSecurityStore; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler; import org.thingsboard.server.transport.lwm2m.utils.TypeServer; +import java.util.Arrays; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.NO_SEC; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertPathFromObjectIdToIdVer; @Service @TbLwM2mTransportComponent @@ -90,7 +94,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { @Override public LwM2mClient updateInSessionsLwM2MClient(Registration registration) { if (this.lwM2mClients.get(registration.getEndpoint()) == null) { - addLwM2mClientToSession(registration.getEndpoint()); + this.addLwM2mClientToSession(registration.getEndpoint()); } LwM2mClient lwM2MClient = lwM2mClients.get(registration.getEndpoint()); lwM2MClient.setRegistration(registration); @@ -169,4 +173,21 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { } return false; } + + /** + * if isVer - ok or default ver=DEFAULT_LWM2M_VERSION + * @param registration - + * @return - all objectIdVer in client + */ + @Override + public Set getSupportedIdVerInClient(Registration registration) { + Set clientObjects = ConcurrentHashMap.newKeySet(); + Arrays.stream(registration.getObjectLinks()).forEach(url -> { + LwM2mPath pathIds = new LwM2mPath(url.getUrl()); + if (!pathIds.isRoot()) { + clientObjects.add(convertPathFromObjectIdToIdVer(url.getUrl(), registration)); + } + }); + return (clientObjects.size() > 0) ? clientObjects : null; + } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java index 1c4042bd1a..19af453f3c 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java @@ -56,6 +56,13 @@ public class LwM2mClientProfile { */ private JsonArray postObserveProfile; + /** + * "attributeLwm2m": {"/3_1.0": {"ver": "currentTimeTest11"}, + * "/3_1.0/0": {"gt": 17}, + * "/3_1.0/0/9": {"pmax": 45}, "/3_1.2": {ver": "3_1.2"}} + */ + private JsonObject postAttributeLwm2mProfile; + public LwM2mClientProfile clone() { LwM2mClientProfile lwM2mClientProfile = new LwM2mClientProfile(); lwM2mClientProfile.postClientLwM2mSettings = this.deepCopy(this.postClientLwM2mSettings, JsonObject.class); @@ -63,6 +70,7 @@ public class LwM2mClientProfile { lwM2mClientProfile.postAttributeProfile = this.deepCopy(this.postAttributeProfile, JsonArray.class); lwM2mClientProfile.postTelemetryProfile = this.deepCopy(this.postTelemetryProfile, JsonArray.class); lwM2mClientProfile.postObserveProfile = this.deepCopy(this.postObserveProfile, JsonArray.class); + lwM2mClientProfile.postAttributeLwm2mProfile = this.deepCopy(this.postAttributeLwm2mProfile, JsonObject.class); return lwM2mClientProfile; } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultsAddKeyValueProto.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultsAddKeyValueProto.java new file mode 100644 index 0000000000..5c7edbe328 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultsAddKeyValueProto.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.client; + +import lombok.Data; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class ResultsAddKeyValueProto { + List resultAttributes; + List resultTelemetries; + + public ResultsAddKeyValueProto() { + this.resultAttributes = new ArrayList<>(); + this.resultTelemetries = new ArrayList<>(); + } + +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultsResourceValue.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultsResourceValue.java new file mode 100644 index 0000000000..36cd8943fb --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultsResourceValue.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.client; + +import lombok.Data; +import org.thingsboard.server.common.data.kv.DataType; + +@Data +public class ResultsResourceValue { + DataType dataType; + Object value; + String resourceName; + + public ResultsResourceValue (DataType dataType, Object value, String resourceName) { + this.dataType = dataType; + this.value = value; + this.resourceName = resourceName; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java index 0faa20952e..dce34c9cf6 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java @@ -18,14 +18,12 @@ package org.thingsboard.server.transport.lwm2m.utils; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.model.ResourceModel.Type; import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.ObjectLink; import org.eclipse.leshan.core.node.codec.CodecException; import org.eclipse.leshan.core.node.codec.LwM2mValueConverter; import org.eclipse.leshan.core.util.Hex; import org.eclipse.leshan.core.util.StringUtils; -import javax.xml.datatype.DatatypeConfigurationException; -import javax.xml.datatype.DatatypeFactory; -import javax.xml.datatype.XMLGregorianCalendar; import java.math.BigInteger; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -111,15 +109,16 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter { case INTEGER: log.debug("Trying to convert long value {} to date", value); /** let's assume we received the millisecond since 1970/1/1 */ - return new Date((Long) value); + return new Date(((Number) value).longValue() * 1000L); case STRING: log.debug("Trying to convert string value {} to date", value); /** let's assume we received an ISO 8601 format date */ try { - DatatypeFactory datatypeFactory = DatatypeFactory.newInstance(); - XMLGregorianCalendar cal = datatypeFactory.newXMLGregorianCalendar((String) value); - return cal.toGregorianCalendar().getTime(); - } catch (DatatypeConfigurationException | IllegalArgumentException e) { + return new Date(Long.decode(value.toString())); +// DatatypeFactory datatypeFactory = DatatypeFactory.newInstance(); +// XMLGregorianCalendar cal = datatypeFactory.newXMLGregorianCalendar((String) value); +// return cal.toGregorianCalendar().getTime(); + } catch (IllegalArgumentException e) { log.debug("Unable to convert string to date", e); throw new CodecException("Unable to convert string (%s) to date for resource %s", value, resourcePath); @@ -147,6 +146,8 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter { return formatter.format(new Date(timeValue)); case OPAQUE: return Hex.encodeHexString((byte[])value); + case OBJLNK: + return ObjectLink.decodeFromString((String) value); default: break; } @@ -164,10 +165,14 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter { } } break; + case OBJLNK: + if (currentType == Type.STRING) { + return ObjectLink.fromPath(value.toString()); + } default: } throw new CodecException("Invalid value type for resource %s, expected %s, got %s", resourcePath, expectedType, currentType); } -} + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index 1c0801c64a..64efb5f036 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -55,7 +55,7 @@ import java.util.concurrent.TimeUnit; */ @Slf4j @Component("MqttSslHandlerProvider") -@ConditionalOnExpression("'${transport.type:null}'=='null' || ('${transport.type}'=='local' && '${transport.mqtt.enabled}'=='true')") +@ConditionalOnExpression("'${transport.mqtt.enabled}'=='true'") @ConditionalOnProperty(prefix = "transport.mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false) public class MqttSslHandlerProvider { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java index 8dfec2003c..d42e7d85e8 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.mqtt.adaptors; +import com.google.gson.JsonElement; import com.google.gson.JsonParser; import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; @@ -57,9 +58,9 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { public TransportProtos.PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; byte[] bytes = toBytes(inbound.payload()); - Descriptors.Descriptor attributesDynamicMessage = getDescriptor(deviceSessionCtx.getAttributesDynamicMessageDescriptor()); + Descriptors.Descriptor attributesDynamicMessageDescriptor = getDescriptor(deviceSessionCtx.getAttributesDynamicMessageDescriptor()); try { - return JsonConverter.convertToAttributesProto(new JsonParser().parse(dynamicMsgToJson(bytes, attributesDynamicMessage))); + return JsonConverter.convertToAttributesProto(new JsonParser().parse(dynamicMsgToJson(bytes, attributesDynamicMessageDescriptor))); } catch (Exception e) { throw new AdaptorException(e); } @@ -79,8 +80,8 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { byte[] bytes = toBytes(inbound.payload()); String topicName = inbound.variableHeader().topicName(); - int requestId = getRequestId(topicName, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); try { + int requestId = getRequestId(topicName, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); return ProtoConverter.convertToGetAttributeRequestMessage(bytes, requestId); } catch (InvalidProtocolBufferException e) { log.warn("Failed to decode get attributes request", e); @@ -90,10 +91,15 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { @Override public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg) throws AdaptorException { + DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; + String topicName = mqttMsg.variableHeader().topicName(); byte[] bytes = toBytes(mqttMsg.payload()); + Descriptors.Descriptor rpcResponseDynamicMessageDescriptor = getDescriptor(deviceSessionCtx.getRpcResponseDynamicMessageDescriptor()); try { - return TransportProtos.ToDeviceRpcResponseMsg.parseFrom(bytes); - } catch (RuntimeException | InvalidProtocolBufferException e) { + int requestId = getRequestId(topicName, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC); + JsonElement response = new JsonParser().parse(dynamicMsgToJson(bytes, rpcResponseDynamicMessageDescriptor)); + return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setPayload(response.toString()).build(); + } catch (Exception e) { log.warn("Failed to decode Rpc response", e); throw new AdaptorException(e); } @@ -137,8 +143,10 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { } @Override - public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), ProtoConverter.convertToRpcRequest(rpcRequest))); + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) throws AdaptorException { + DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; + DynamicMessage.Builder rpcRequestDynamicMessageBuilder = deviceSessionCtx.getRpcRequestDynamicMessageBuilder(); + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), ProtoConverter.convertToRpcRequest(rpcRequest, rpcRequestDynamicMessageBuilder))); } @Override diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java index efc5c057ed..3804d96cf6 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.mqtt.session; import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; import io.netty.channel.ChannelHandlerContext; import lombok.Getter; import lombok.Setter; @@ -60,6 +61,8 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { private volatile TransportPayloadType payloadType = TransportPayloadType.JSON; private volatile Descriptors.Descriptor attributesDynamicMessageDescriptor; private volatile Descriptors.Descriptor telemetryDynamicMessageDescriptor; + private volatile Descriptors.Descriptor rpcResponseDynamicMessageDescriptor; + private volatile DynamicMessage.Builder rpcRequestDynamicMessageBuilder; @Getter @Setter @@ -102,6 +105,14 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { return attributesDynamicMessageDescriptor; } + public Descriptors.Descriptor getRpcResponseDynamicMessageDescriptor() { + return rpcResponseDynamicMessageDescriptor; + } + + public DynamicMessage.Builder getRpcRequestDynamicMessageBuilder() { + return rpcRequestDynamicMessageBuilder; + } + @Override public void setDeviceProfile(DeviceProfile deviceProfile) { super.setDeviceProfile(deviceProfile); @@ -136,5 +147,7 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { ProtoTransportPayloadConfiguration protoTransportPayloadConfig = (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; telemetryDynamicMessageDescriptor = protoTransportPayloadConfig.getTelemetryDynamicMessageDescriptor(protoTransportPayloadConfig.getDeviceTelemetryProtoSchema()); attributesDynamicMessageDescriptor = protoTransportPayloadConfig.getAttributesDynamicMessageDescriptor(protoTransportPayloadConfig.getDeviceAttributesProtoSchema()); + rpcResponseDynamicMessageDescriptor = protoTransportPayloadConfig.getRpcResponseDynamicMessageDescriptor(protoTransportPayloadConfig.getDeviceRpcResponseProtoSchema()); + rpcRequestDynamicMessageBuilder = protoTransportPayloadConfig.getRpcRequestDynamicMessageBuilder(protoTransportPayloadConfig.getDeviceRpcRequestProtoSchema()); } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java index 42c99bc428..d0d471a784 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java @@ -31,4 +31,8 @@ public class AdaptorException extends Exception { super(cause); } + public AdaptorException(String message, Exception cause) { + super(message, cause); + } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java index 6155dcac7a..9b4897bad5 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java @@ -74,12 +74,16 @@ public class JsonConverter { private static int maxStringValueLength = 0; - public static PostTelemetryMsg convertToTelemetryProto(JsonElement jsonElement) throws JsonSyntaxException { + public static PostTelemetryMsg convertToTelemetryProto(JsonElement jsonElement, long ts) throws JsonSyntaxException { PostTelemetryMsg.Builder builder = PostTelemetryMsg.newBuilder(); - convertToTelemetry(jsonElement, System.currentTimeMillis(), null, builder); + convertToTelemetry(jsonElement, ts, null, builder); return builder.build(); } + public static PostTelemetryMsg convertToTelemetryProto(JsonElement jsonElement) throws JsonSyntaxException { + return convertToTelemetryProto(jsonElement, System.currentTimeMillis()); + } + private static void convertToTelemetry(JsonElement jsonElement, long systemTs, Map> result, PostTelemetryMsg.Builder builder) { if (jsonElement.isJsonObject()) { parseObject(systemTs, result, builder, jsonElement.getAsJsonObject()); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java index ed440ebcfe..620728cda4 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java @@ -15,10 +15,13 @@ */ package org.thingsboard.server.common.transport.adaptor; +import com.google.gson.Gson; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import com.google.gson.JsonPrimitive; +import com.google.protobuf.DynamicMessage; import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; import lombok.extern.slf4j.Slf4j; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -34,6 +37,7 @@ import java.util.List; @Slf4j public class ProtoConverter { + public static final Gson GSON = new Gson(); public static final JsonParser JSON_PARSER = new JsonParser(); public static TransportProtos.PostTelemetryMsg convertToTelemetryProto(byte[] payload) throws InvalidProtocolBufferException, IllegalArgumentException { @@ -170,26 +174,20 @@ public class ProtoConverter { return kvList; } - public static byte[] convertToRpcRequest(TransportProtos.ToDeviceRpcRequestMsg toDeviceRpcRequestMsg) { - TransportProtos.ToDeviceRpcRequestMsg.Builder toDeviceRpcRequestMsgBuilder = toDeviceRpcRequestMsg.newBuilderForType(); - toDeviceRpcRequestMsgBuilder.mergeFrom(toDeviceRpcRequestMsg); - toDeviceRpcRequestMsgBuilder.setParams(parseParams(toDeviceRpcRequestMsg)); - TransportProtos.ToDeviceRpcRequestMsg result = toDeviceRpcRequestMsgBuilder.build(); - return result.toByteArray(); - } - - private static String parseParams(TransportProtos.ToDeviceRpcRequestMsg toDeviceRpcRequestMsg) { + public static byte[] convertToRpcRequest(TransportProtos.ToDeviceRpcRequestMsg toDeviceRpcRequestMsg, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) throws AdaptorException { + rpcRequestDynamicMessageBuilder.clear(); + JsonObject rpcRequestJson = new JsonObject(); + rpcRequestJson.addProperty("method", toDeviceRpcRequestMsg.getMethodName()); + rpcRequestJson.addProperty("requestId", toDeviceRpcRequestMsg.getRequestId()); String params = toDeviceRpcRequestMsg.getParams(); - JsonElement jsonElementParams = JSON_PARSER.parse(params); - if (!jsonElementParams.isJsonPrimitive()) { - return params; - } else { - JsonPrimitive primitiveParams = jsonElementParams.getAsJsonPrimitive(); - if (jsonElementParams.getAsJsonPrimitive().isString()) { - return primitiveParams.getAsString(); - } else { - return params; - } + try { + JsonElement paramsElement = JSON_PARSER.parse(params); + rpcRequestJson.add("params", paramsElement); + JsonFormat.parser().ignoringUnknownFields().merge(GSON.toJson(rpcRequestJson), rpcRequestDynamicMessageBuilder); + DynamicMessage dynamicRpcRequest = rpcRequestDynamicMessageBuilder.build(); + return dynamicRpcRequest.toByteArray(); + } catch (Exception e) { + throw new AdaptorException("Failed to convert ToDeviceRpcRequestMsg to Dynamic Rpc request message due to: ", e); } } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigServer.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigServer.java index 5603721474..cb2af9eb3f 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigServer.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigServer.java @@ -207,6 +207,8 @@ public class LwM2MTransportConfigServer { FULL_FILE_PATH = Paths.get(BASE_DIR_PATH.replaceAll("bin$", "")); } else if (BASE_DIR_PATH.endsWith("conf")) { FULL_FILE_PATH = Paths.get(BASE_DIR_PATH.replaceAll("conf$", "")); + } else if (BASE_DIR_PATH.endsWith("application")) { + FULL_FILE_PATH = Paths.get(BASE_DIR_PATH.substring(0, BASE_DIR_PATH.length() - "application".length())); } else { FULL_FILE_PATH = Paths.get(BASE_DIR_PATH); } diff --git a/dao/pom.xml b/dao/pom.xml index 9ba3033970..cc25e1a13c 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -227,10 +227,6 @@ org.elasticsearch.client rest - - org.eclipse.leshan - leshan-core - diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 9af4dc4749..9dff668f2e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -40,7 +40,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; -import org.thingsboard.server.common.data.query.AlarmDataPageLink; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -327,16 +326,6 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return alarmSeverities.stream().min(AlarmSeverity::compareTo).orElse(null); } - private void deleteRelation(TenantId tenantId, EntityRelation alarmRelation) { - log.debug("Deleting Alarm relation: {}", alarmRelation); - relationService.deleteRelation(tenantId, alarmRelation); - } - - private void createRelation(TenantId tenantId, EntityRelation alarmRelation) { - log.debug("Creating Alarm relation: {}", alarmRelation); - relationService.saveRelation(tenantId, alarmRelation); - } - private Alarm merge(Alarm existing, Alarm alarm) { if (alarm.getStartTs() > existing.getEndTs()) { existing.setEndTs(alarm.getStartTs()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java index e99be2646d..130d86124f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.asset.AssetInfo; 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.page.TimePageLink; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.TenantEntityDao; @@ -167,4 +168,24 @@ public interface AssetDao extends Dao, TenantEntityDao { */ ListenableFuture> findTenantAssetTypesAsync(UUID tenantId); + /** + * Find assets by tenantId, edgeId and page link. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param pageLink the page link + * @return the list of asset objects + */ + PageData findAssetsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink); + + /** + * Find assets by tenantId, edgeId, type and page link. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param type the type + * @param pageLink the page link + * @return the list of asset objects + */ + PageData findAssetsByTenantIdAndEdgeIdAndType(UUID tenantId, UUID edgeId, String type, PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index 57a928ee0c..97bd5f37a9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.asset; +import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -37,24 +38,28 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; +import org.thingsboard.server.common.data.edge.Edge; 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.EntityId; 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.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.entity.AbstractEntityService; -import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantDao; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -75,7 +80,6 @@ import static org.thingsboard.server.dao.service.Validator.validateString; public class BaseAssetService extends AbstractEntityService implements AssetService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; public static final String INCORRECT_ASSET_ID = "Incorrect assetId "; public static final String TB_SERVICE_QUEUE = "TbServiceQueue"; @@ -89,9 +93,6 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ @Autowired private CustomerDao customerDao; - @Autowired - private EntityViewService entityViewService; - @Autowired private CacheManager cacheManager; @@ -324,6 +325,63 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ }, MoreExecutors.directExecutor()); } + @Override + public Asset assignAssetToEdge(TenantId tenantId, AssetId assetId, EdgeId edgeId) { + Asset asset = findAssetById(tenantId, assetId); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null) { + throw new DataValidationException("Can't assign asset to non-existent edge!"); + } + if (!edge.getTenantId().getId().equals(asset.getTenantId().getId())) { + throw new DataValidationException("Can't assign asset to edge from different tenant!"); + } + try { + createRelation(tenantId, new EntityRelation(edgeId, assetId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to create asset relation. Edge Id: [{}]", assetId, edgeId); + throw new RuntimeException(e); + } + return asset; + } + + @Override + public Asset unassignAssetFromEdge(TenantId tenantId, AssetId assetId, EdgeId edgeId) { + Asset asset = findAssetById(tenantId, assetId); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null) { + throw new DataValidationException("Can't unassign asset from non-existent edge!"); + } + + checkAssignedEntityViewsToEdge(tenantId, assetId, edgeId); + + try { + deleteRelation(tenantId, new EntityRelation(edgeId, assetId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to delete asset relation. Edge Id: [{}]", assetId, edgeId); + throw new RuntimeException(e); + } + return asset; + } + + @Override + public PageData findAssetsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink) { + log.trace("Executing findAssetsByTenantIdAndEdgeId, tenantId [{}], edgeId [{}], pageLink [{}]", tenantId, edgeId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + validatePageLink(pageLink); + return assetDao.findAssetsByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink); + } + + @Override + public PageData findAssetsByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink) { + log.trace("Executing findAssetsByTenantIdAndEdgeIdAndType, tenantId [{}], edgeId [{}], type [{}] pageLink [{}]", tenantId, edgeId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return assetDao.findAssetsByTenantIdAndEdgeIdAndType(tenantId.getId(), edgeId.getId(), type, pageLink); + } + private DataValidator assetValidator = new DataValidator() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index 7bf13871c5..2fdee60313 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -295,6 +295,22 @@ public class AuditLogServiceImpl implements AuditLogService { actionData.put("startTs", extractParameter(Long.class, 1, additionalInfo)); actionData.put("endTs", extractParameter(Long.class, 2, additionalInfo)); break; + case ASSIGNED_TO_EDGE: + strEntityId = extractParameter(String.class, 0, additionalInfo); + String strEdgeId = extractParameter(String.class, 1, additionalInfo); + String strEdgeName = extractParameter(String.class, 2, additionalInfo); + actionData.put("entityId", strEntityId); + actionData.put("assignedEdgeId", strEdgeId); + actionData.put("assignedEdgeName", strEdgeName); + break; + case UNASSIGNED_FROM_EDGE: + strEntityId = extractParameter(String.class, 0, additionalInfo); + strEdgeId = extractParameter(String.class, 1, additionalInfo); + strEdgeName = extractParameter(String.class, 2, additionalInfo); + actionData.put("entityId", strEntityId); + actionData.put("unassignedEdgeId", strEdgeId); + actionData.put("unassignedEdgeName", strEdgeName); + break; } return actionData; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java index dc1446ff3a..c00c5fbe56 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java @@ -106,16 +106,16 @@ public class BaseComponentDescriptorService implements ComponentDescriptorServic @Override protected void validateDataImpl(TenantId tenantId, ComponentDescriptor plugin) { if (plugin.getType() == null) { - throw new DataValidationException("Component type should be specified!."); + throw new DataValidationException("Component type should be specified!"); } if (plugin.getScope() == null) { - throw new DataValidationException("Component scope should be specified!."); + throw new DataValidationException("Component scope should be specified!"); } if (StringUtils.isEmpty(plugin.getName())) { - throw new DataValidationException("Component name should be specified!."); + throw new DataValidationException("Component name should be specified!"); } if (StringUtils.isEmpty(plugin.getClazz())) { - throw new DataValidationException("Component clazz should be specified!."); + throw new DataValidationException("Component clazz should be specified!"); } } }; diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index 2ca59b7804..8f13900a6a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -125,6 +125,7 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom entityViewService.unassignCustomerEntityViews(customer.getTenantId(), customerId); assetService.unassignCustomerAssets(customer.getTenantId(), customerId); deviceService.unassignCustomerDevices(customer.getTenantId(), customerId); + edgeService.unassignCustomerEdges(customer.getTenantId(), customerId); userService.deleteCustomerUsers(customer.getTenantId(), customerId); deleteEntityRelations(tenantId, customerId); customerDao.removeById(tenantId, customerId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java index 48b234f1b2..22a4567198 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java @@ -46,4 +46,14 @@ public interface DashboardInfoDao extends Dao { */ PageData findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink); + /** + * Find dashboards by tenantId, edgeId and page link. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param pageLink the page link + * @return the list of dashboard objects + */ + PageData findDashboardsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 84b2afc089..456132a443 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -26,15 +26,19 @@ import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EdgeId; 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.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.customer.CustomerDao; +import org.thingsboard.server.dao.edge.EdgeDao; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @@ -43,8 +47,6 @@ import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantDao; -import java.util.concurrent.ExecutionException; - import static org.thingsboard.server.dao.service.Validator.validateId; @Service @@ -64,6 +66,9 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Autowired private CustomerDao customerDao; + + @Autowired + private EdgeDao edgeDao; @Autowired @Lazy @@ -117,7 +122,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb if (dashboard.addAssignedCustomer(customer)) { try { createRelation(tenantId, new EntityRelation(customerId, dashboardId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.DASHBOARD)); - } catch (ExecutionException | InterruptedException e) { + } catch (Exception e) { log.warn("[{}] Failed to create dashboard relation. Customer Id: [{}]", dashboardId, customerId); throw new RuntimeException(e); } @@ -137,7 +142,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb if (dashboard.removeAssignedCustomer(customer)) { try { deleteRelation(tenantId, new EntityRelation(customerId, dashboardId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.DASHBOARD)); - } catch (ExecutionException | InterruptedException e) { + } catch (Exception e) { log.warn("[{}] Failed to delete dashboard relation. Customer Id: [{}]", dashboardId, customerId); throw new RuntimeException(e); } @@ -156,16 +161,6 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb } } - private void deleteRelation(TenantId tenantId, EntityRelation dashboardRelation) throws ExecutionException, InterruptedException { - log.debug("Deleting Dashboard relation: {}", dashboardRelation); - relationService.deleteRelationAsync(tenantId, dashboardRelation).get(); - } - - private void createRelation(TenantId tenantId, EntityRelation dashboardRelation) throws ExecutionException, InterruptedException { - log.debug("Creating Dashboard relation: {}", dashboardRelation); - relationService.saveRelationAsync(tenantId, dashboardRelation).get(); - } - @Override public void deleteDashboard(TenantId tenantId, DashboardId dashboardId) { log.trace("Executing deleteDashboard [{}]", dashboardId); @@ -220,6 +215,50 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb new CustomerDashboardsUpdater(customer).removeEntities(tenantId, customer); } + @Override + public Dashboard assignDashboardToEdge(TenantId tenantId, DashboardId dashboardId, EdgeId edgeId) { + Dashboard dashboard = findDashboardById(tenantId, dashboardId); + Edge edge = edgeDao.findById(tenantId, edgeId.getId()); + if (edge == null) { + throw new DataValidationException("Can't assign dashboard to non-existent edge!"); + } + if (!edge.getTenantId().equals(dashboard.getTenantId())) { + throw new DataValidationException("Can't assign dashboard to edge from different tenant!"); + } + try { + createRelation(tenantId, new EntityRelation(edgeId, dashboardId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to create dashboard relation. Edge Id: [{}]", dashboardId, edgeId); + throw new RuntimeException(e); + } + return dashboard; + } + + @Override + public Dashboard unassignDashboardFromEdge(TenantId tenantId, DashboardId dashboardId, EdgeId edgeId) { + Dashboard dashboard = findDashboardById(tenantId, dashboardId); + Edge edge = edgeDao.findById(tenantId, edgeId.getId()); + if (edge == null) { + throw new DataValidationException("Can't unassign dashboard from non-existent edge!"); + } + try { + deleteRelation(tenantId, new EntityRelation(edgeId, dashboardId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to delete dashboard relation. Edge Id: [{}]", dashboardId, edgeId); + throw new RuntimeException(e); + } + return dashboard; + } + + @Override + public PageData findDashboardsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink) { + log.trace("Executing findDashboardsByTenantIdAndEdgeId, tenantId [{}], edgeId [{}], pageLink [{}]", tenantId, edgeId, pageLink); + Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + Validator.validatePageLink(pageLink); + return dashboardInfoDao.findDashboardsByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink); + } + private DataValidator dashboardValidator = new DataValidator() { @Override @@ -259,9 +298,9 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb deleteDashboard(tenantId, new DashboardId(entity.getUuidId())); } }; - + private class CustomerDashboardsUnassigner extends PaginatedRemover { - + private Customer customer; CustomerDashboardsUnassigner(Customer customer) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java index 7f8d512c70..784eb4e229 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntitySubtype; 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.page.TimePageLink; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.TenantEntityDao; @@ -215,4 +216,26 @@ public interface DeviceDao extends Dao, TenantEntityDao { * @return the list of device objects */ PageData findDevicesByTenantIdAndProfileId(UUID tenantId, UUID profileId, PageLink pageLink); + + + /** + * Find devices by tenantId, edgeId and page link. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param pageLink the page link + * @return the list of device objects + */ + PageData findDevicesByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink); + + /** + * Find devices by tenantId, edgeId, type and page link. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param type the type + * @param pageLink the page link + * @return the list of device objects + */ + PageData findDevicesByTenantIdAndEdgeIdAndType(UUID tenantId, UUID edgeId, String type, PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 9f85dd1cb5..7833c80ed5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.dao.device; +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; import com.squareup.wire.Syntax; import com.squareup.wire.schema.Field; import com.squareup.wire.schema.Location; @@ -89,6 +91,8 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D private static final Location LOCATION = new Location("", "", -1, -1); private static final String ATTRIBUTES_PROTO_SCHEMA = "attributes proto schema"; private static final String TELEMETRY_PROTO_SCHEMA = "telemetry proto schema"; + private static final String RPC_REQUEST_PROTO_SCHEMA = "rpc request proto schema"; + private static final String RPC_RESPONSE_PROTO_SCHEMA = "rpc response proto schema"; private static String invalidSchemaProvidedMessage(String schemaName) { return "[Transport Configuration] invalid " + schemaName + " provided!"; @@ -362,9 +366,10 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D if (transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) { MqttDeviceProfileTransportConfiguration mqttTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; if (mqttTransportConfiguration.getTransportPayloadTypeConfiguration() instanceof ProtoTransportPayloadConfiguration) { - ProtoTransportPayloadConfiguration protoTransportPayloadTypeConfiguration = + ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = (ProtoTransportPayloadConfiguration) mqttTransportConfiguration.getTransportPayloadTypeConfiguration(); - validateProtoSchemas(protoTransportPayloadTypeConfiguration); + validateProtoSchemas(protoTransportPayloadConfiguration); + validateRpcRequestDynamicMessageFields(protoTransportPayloadConfiguration); } } else if (transportConfiguration instanceof CoapDeviceProfileTransportConfiguration) { CoapDeviceProfileTransportConfiguration coapDeviceProfileTransportConfiguration = (CoapDeviceProfileTransportConfiguration) transportConfiguration; @@ -375,6 +380,7 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D if (transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration) { ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; validateProtoSchemas(protoTransportPayloadConfiguration); + validateRpcRequestDynamicMessageFields(protoTransportPayloadConfiguration); } } } @@ -431,6 +437,8 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D try { validateTransportProtoSchema(protoTransportPayloadTypeConfiguration.getDeviceAttributesProtoSchema(), ATTRIBUTES_PROTO_SCHEMA); validateTransportProtoSchema(protoTransportPayloadTypeConfiguration.getDeviceTelemetryProtoSchema(), TELEMETRY_PROTO_SCHEMA); + validateTransportProtoSchema(protoTransportPayloadTypeConfiguration.getDeviceRpcRequestProtoSchema(), RPC_REQUEST_PROTO_SCHEMA); + validateTransportProtoSchema(protoTransportPayloadTypeConfiguration.getDeviceRpcResponseProtoSchema(), RPC_RESPONSE_PROTO_SCHEMA); } catch (Exception exception) { throw new DataValidationException(exception.getMessage()); } @@ -553,6 +561,48 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D }; + private void validateRpcRequestDynamicMessageFields(ProtoTransportPayloadConfiguration protoTransportPayloadTypeConfiguration) { + DynamicMessage.Builder rpcRequestDynamicMessageBuilder = protoTransportPayloadTypeConfiguration.getRpcRequestDynamicMessageBuilder(protoTransportPayloadTypeConfiguration.getDeviceRpcRequestProtoSchema()); + Descriptors.Descriptor rpcRequestDynamicMessageDescriptor = rpcRequestDynamicMessageBuilder.getDescriptorForType(); + if (rpcRequestDynamicMessageDescriptor == null) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " Failed to get rpcRequestDynamicMessageDescriptor!"); + } else { + if (CollectionUtils.isEmpty(rpcRequestDynamicMessageDescriptor.getFields()) || rpcRequestDynamicMessageDescriptor.getFields().size() != 3) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " " + rpcRequestDynamicMessageDescriptor.getName() + " message should always contains 3 fields: method, requestId and params!"); + } + Descriptors.FieldDescriptor methodFieldDescriptor = rpcRequestDynamicMessageDescriptor.findFieldByName("method"); + if (methodFieldDescriptor == null) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " Failed to get field descriptor for field: method!"); + } else { + if (!Descriptors.FieldDescriptor.Type.STRING.equals(methodFieldDescriptor.getType())) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " Field 'method' has invalid data type. Only string type is supported!"); + } + if (methodFieldDescriptor.isRepeated()) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " Field 'method' has invalid label!"); + } + } + Descriptors.FieldDescriptor requestIdFieldDescriptor = rpcRequestDynamicMessageDescriptor.findFieldByName("requestId"); + if (requestIdFieldDescriptor == null) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " Failed to get field descriptor for field: requestId!"); + } else { + if (!Descriptors.FieldDescriptor.Type.INT32.equals(requestIdFieldDescriptor.getType())) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " Field 'requestId' has invalid data type. Only int32 type is supported!"); + } + if (requestIdFieldDescriptor.isRepeated()) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " Field 'requestId' has invalid label!"); + } + } + Descriptors.FieldDescriptor paramsFieldDescriptor = rpcRequestDynamicMessageDescriptor.findFieldByName("params"); + if (paramsFieldDescriptor == null) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + " Failed to get field descriptor for field: params!"); + } else { + if (paramsFieldDescriptor.isRepeated()) { + throw new DataValidationException(invalidSchemaProvidedMessage(RPC_REQUEST_PROTO_SCHEMA) + "Field 'params' has invalid label!"); + } + } + } + } + private PaginatedRemover tenantDeviceProfilesRemover = new PaginatedRemover() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index d1d76b1e95..73eff3b5e6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Firmware; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.device.data.CoapDeviceTransportConfiguration; @@ -49,15 +50,19 @@ import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConf import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.MqttDeviceTransportConfiguration; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; @@ -66,7 +71,6 @@ import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; import org.thingsboard.server.dao.entity.AbstractEntityService; -import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.firmware.FirmwareService; @@ -102,6 +106,8 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; public static final String INCORRECT_DEVICE_ID = "Incorrect deviceId "; + public static final String INCORRECT_EDGE_ID = "Incorrect edgeId "; + @Autowired private DeviceDao deviceDao; @@ -117,9 +123,6 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @Autowired private DeviceProfileService deviceProfileService; - @Autowired - private EntityViewService entityViewService; - @Autowired private CacheManager cacheManager; @@ -560,6 +563,68 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return savedDevice; } + @Override + public Device assignDeviceToEdge(TenantId tenantId, DeviceId deviceId, EdgeId edgeId) { + Device device = findDeviceById(tenantId, deviceId); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null) { + throw new DataValidationException("Can't assign device to non-existent edge!"); + } + if (!edge.getTenantId().getId().equals(device.getTenantId().getId())) { + throw new DataValidationException("Can't assign device to edge from different tenant!"); + } + try { + createRelation(tenantId, new EntityRelation(edgeId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to create device relation. Edge Id: [{}]", deviceId, edgeId); + throw new RuntimeException(e); + } + return device; + } + + @Override + public Device unassignDeviceFromEdge(TenantId tenantId, DeviceId deviceId, EdgeId edgeId) { + Device device = findDeviceById(tenantId, deviceId); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null) { + throw new DataValidationException("Can't unassign device from non-existent edge!"); + } + + checkAssignedEntityViewsToEdge(tenantId, deviceId, edgeId); + + try { + deleteRelation(tenantId, new EntityRelation(edgeId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to delete device relation. Edge Id: [{}]", deviceId, edgeId); + throw new RuntimeException(e); + } + return device; + } + + @Override + public PageData findDevicesByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink) { + log.trace("Executing findDevicesByTenantIdAndEdgeId, tenantId [{}], edgeId [{}], pageLink [{}]", tenantId, edgeId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + validatePageLink(pageLink); + return deviceDao.findDevicesByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink); + } + + @Override + public PageData findDevicesByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink) { + log.trace("Executing findDevicesByTenantIdAndEdgeIdAndType, tenantId [{}], edgeId [{}], type [{}] pageLink [{}]", tenantId, edgeId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return deviceDao.findDevicesByTenantIdAndEdgeIdAndType(tenantId.getId(), edgeId.getId(), type, pageLink); + } + + @Override + public long countByTenantId(TenantId tenantId) { + return deviceDao.countByTenantId(tenantId); + } + private DataValidator deviceValidator = new DataValidator() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java b/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java new file mode 100644 index 0000000000..20e4fbf6ce --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; + +@Service +@Slf4j +public class BaseEdgeEventService implements EdgeEventService { + + @Autowired + private EdgeEventDao edgeEventDao; + + @Override + public ListenableFuture saveAsync(EdgeEvent edgeEvent) { + edgeEventValidator.validate(edgeEvent, EdgeEvent::getTenantId); + return edgeEventDao.saveAsync(edgeEvent); + } + + @Override + public PageData findEdgeEvents(TenantId tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate) { + return edgeEventDao.findEdgeEvents(tenantId.getId(), edgeId, pageLink, withTsUpdate); + } + + private DataValidator edgeEventValidator = + new DataValidator() { + @Override + protected void validateDataImpl(TenantId tenantId, EdgeEvent edgeEvent) { + if (edgeEvent.getEdgeId() == null) { + throw new DataValidationException("Edge id should be specified!"); + } + if (edgeEvent.getAction() == null) { + throw new DataValidationException("Edge Event action should be specified!"); + } + } + }; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java new file mode 100644 index 0000000000..d254c5ee9a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java @@ -0,0 +1,179 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeInfo; +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.dao.Dao; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * The Interface EdgeDao. + * + */ +public interface EdgeDao extends Dao { + + /** + * Save or update edge object + * + * @param edge the edge object + * @return saved edge object + */ + Edge save(TenantId tenantId, Edge edge); + + /** + * Find edge info by id. + * + * @param tenantId the tenant id + * @param edgeId the edge id + * @return the edge info object + */ + EdgeInfo findEdgeInfoById(TenantId tenantId, UUID edgeId); + + /** + * Find edges by tenantId and page link. + * + * @param tenantId the tenantId + * @param pageLink the page link + * @return the list of edge objects + */ + PageData findEdgesByTenantId(UUID tenantId, PageLink pageLink); + + /** + * Find edges by tenantId, type and page link. + * + * @param tenantId the tenantId + * @param type the type + * @param pageLink the page link + * @return the list of edge objects + */ + PageData findEdgesByTenantIdAndType(UUID tenantId, String type, PageLink pageLink); + + /** + * Find edges by tenantId and edges Ids. + * + * @param tenantId the tenantId + * @param edgeIds the edge Ids + * @return the list of edge objects + */ + ListenableFuture> findEdgesByTenantIdAndIdsAsync(UUID tenantId, List edgeIds); + + /** + * Find edges by tenantId, customerId and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param pageLink the page link + * @return the list of edge objects + */ + PageData findEdgesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink); + + /** + * Find edges by tenantId, customerId, type and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param type the type + * @param pageLink the page link + * @return the list of edge objects + */ + PageData findEdgesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink); + + /** + * Find edge infos by tenantId, customerId and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param pageLink the page link + * @return the list of edge info objects + */ + PageData findEdgeInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink); + + /** + * Find edge infos by tenantId, customerId, type and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param type the type + * @param pageLink the page link + * @return the list of edge info objects + */ + PageData findEdgeInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink); + + /** + * Find edges by tenantId, customerId and edges Ids. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param edgeIds the edge Ids + * @return the list of edge objects + */ + ListenableFuture> findEdgesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List edgeIds); + + /** + * Find edges by tenantId and edge name. + * + * @param tenantId the tenantId + * @param name the edge name + * @return the optional edge object + */ + Optional findEdgeByTenantIdAndName(UUID tenantId, String name); + + /** + * Find tenants edge types. + * + * @return the list of tenant edge type objects + */ + ListenableFuture> findTenantEdgeTypesAsync(UUID tenantId); + + /** + * Find edge by routing Key. + * + * @param routingKey the edge routingKey + * @return the optional edge object + */ + Optional findByRoutingKey(UUID tenantId, String routingKey); + + PageData findEdgeInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink); + + PageData findEdgeInfosByTenantId(UUID tenantId, PageLink pageLink); + + /** + * Find edges by tenantId and ruleChainId. + * + * @param tenantId the tenantId + * @param ruleChainId the ruleChainId + * @return the list of rule chain objects + */ + ListenableFuture> findEdgesByTenantIdAndRuleChainId(UUID tenantId, UUID ruleChainId); + + /** + * Find edges by tenantId and dashboardId. + * + * @param tenantId the tenantId + * @param dashboardId the dashboardId + * @return the list of rule chain objects + */ + ListenableFuture> findEdgesByTenantIdAndDashboardId(UUID tenantId, UUID dashboardId); +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java new file mode 100644 index 0000000000..392e7276b1 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.dao.Dao; + +import java.util.UUID; + +/** + * The Interface EdgeEventDao. + */ +public interface EdgeEventDao extends Dao { + + /** + * Save or update edge event object async + * + * @param edgeEvent the event object + * @return saved edge event object future + */ + ListenableFuture saveAsync(EdgeEvent edgeEvent); + + + /** + * Find edge events by tenantId, edgeId and pageLink. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param pageLink the pageLink + * @return the event list + */ + PageData findEdgeEvents(UUID tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java new file mode 100644 index 0000000000..8401a43d21 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -0,0 +1,679 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.base.Function; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpHost; +import org.apache.http.conn.ssl.DefaultHostnameVerifier; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeInfo; +import org.thingsboard.server.common.data.edge.EdgeSearchQuery; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.IdBased; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; +import org.thingsboard.server.dao.customer.CustomerDao; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +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.service.PaginatedRemover; +import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.dao.tenant.TenantDao; +import org.thingsboard.server.dao.user.UserService; + +import javax.annotation.Nullable; +import javax.annotation.PostConstruct; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.apache.commons.lang3.StringUtils.isNotEmpty; +import static org.thingsboard.server.common.data.CacheConstants.EDGE_CACHE; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; +import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validateIds; +import static org.thingsboard.server.dao.service.Validator.validatePageLink; +import static org.thingsboard.server.dao.service.Validator.validateString; + +@Service +@Slf4j +public class EdgeServiceImpl extends AbstractEntityService implements EdgeService { + + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; + public static final String INCORRECT_EDGE_ID = "Incorrect edgeId "; + + private static final ObjectMapper mapper = new ObjectMapper(); + + private static final int DEFAULT_LIMIT = 100; + + private RestTemplate restTemplate; + + private static final String EDGE_LICENSE_SERVER_ENDPOINT = "https://license.thingsboard.io"; + + @Autowired + private EdgeDao edgeDao; + + @Autowired + private TenantDao tenantDao; + + @Autowired + private CustomerDao customerDao; + + @Autowired + private UserService userService; + + @Autowired + private CacheManager cacheManager; + + @Autowired + private RuleChainService ruleChainService; + + @Autowired + private RelationService relationService; + + @Value("${edges.enabled:false}") + private boolean edgesEnabled; + + @PostConstruct + public void init() { + if (edgesEnabled) { + initRestTemplate(); + } + } + + @Override + public Edge findEdgeById(TenantId tenantId, EdgeId edgeId) { + log.trace("Executing findEdgeById [{}]", edgeId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + return edgeDao.findById(tenantId, edgeId.getId()); + } + + @Override + public EdgeInfo findEdgeInfoById(TenantId tenantId, EdgeId edgeId) { + log.trace("Executing findEdgeInfoById [{}]", edgeId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + return edgeDao.findEdgeInfoById(tenantId, edgeId.getId()); + } + + @Override + public ListenableFuture findEdgeByIdAsync(TenantId tenantId, EdgeId edgeId) { + log.trace("Executing findEdgeById [{}]", edgeId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + return edgeDao.findByIdAsync(tenantId, edgeId.getId()); + } + + @Cacheable(cacheNames = EDGE_CACHE, key = "{#tenantId, #name}") + @Override + public Edge findEdgeByTenantIdAndName(TenantId tenantId, String name) { + log.trace("Executing findEdgeByTenantIdAndName [{}][{}]", tenantId, name); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Optional edgeOpt = edgeDao.findEdgeByTenantIdAndName(tenantId.getId(), name); + return edgeOpt.orElse(null); + } + + @Override + public Optional findEdgeByRoutingKey(TenantId tenantId, String routingKey) { + log.trace("Executing findEdgeByRoutingKey [{}]", routingKey); + Validator.validateString(routingKey, "Incorrect edge routingKey for search request."); + return edgeDao.findByRoutingKey(tenantId.getId(), routingKey); + } + + @CacheEvict(cacheNames = EDGE_CACHE, key = "{#edge.tenantId, #edge.name}") + @Override + public Edge saveEdge(Edge edge) { + log.trace("Executing saveEdge [{}]", edge); + edgeValidator.validate(edge, Edge::getTenantId); + try { + return edgeDao.save(edge.getTenantId(), edge); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null + && e.getConstraintName().equalsIgnoreCase("edge_name_unq_key")) { + throw new DataValidationException("Edge with such name already exists!"); + } else { + throw t; + } + } + } + + @Override + public Edge assignEdgeToCustomer(TenantId tenantId, EdgeId edgeId, CustomerId customerId) { + log.trace("[{}] Executing assignEdgeToCustomer [{}][{}]", tenantId, edgeId, customerId); + Edge edge = findEdgeById(tenantId, edgeId); + edge.setCustomerId(customerId); + return saveEdge(edge); + } + + @Override + public Edge unassignEdgeFromCustomer(TenantId tenantId, EdgeId edgeId) { + log.trace("[{}] Executing unassignEdgeFromCustomer [{}]", tenantId, edgeId); + Edge edge = findEdgeById(tenantId, edgeId); + edge.setCustomerId(null); + return saveEdge(edge); + } + + @Override + public void deleteEdge(TenantId tenantId, EdgeId edgeId) { + log.trace("Executing deleteEdge [{}]", edgeId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + + Edge edge = edgeDao.findById(tenantId, edgeId.getId()); + + List list = new ArrayList<>(); + list.add(edge.getTenantId()); + list.add(edge.getName()); + Cache cache = cacheManager.getCache(EDGE_CACHE); + cache.evict(list); + + deleteEntityRelations(tenantId, edgeId); + + edgeDao.removeById(tenantId, edgeId.getId()); + } + + @Override + public PageData findEdgesByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findEdgesByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validatePageLink(pageLink); + return edgeDao.findEdgesByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findEdgesByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { + log.trace("Executing findEdgesByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return edgeDao.findEdgesByTenantIdAndType(tenantId.getId(), type, pageLink); + } + + @Override + public PageData findEdgeInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) { + log.trace("Executing findEdgeInfosByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return edgeDao.findEdgeInfosByTenantIdAndType(tenantId.getId(), type, pageLink); + } + + @Override + public PageData findEdgeInfosByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findEdgeInfosByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validatePageLink(pageLink); + return edgeDao.findEdgeInfosByTenantId(tenantId.getId(), pageLink); + } + + @Override + public ListenableFuture> findEdgesByTenantIdAndIdsAsync(TenantId tenantId, List edgeIds) { + log.trace("Executing findEdgesByTenantIdAndIdsAsync, tenantId [{}], edgeIds [{}]", tenantId, edgeIds); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateIds(edgeIds, "Incorrect edgeIds " + edgeIds); + return edgeDao.findEdgesByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(edgeIds)); + } + + @Override + public void deleteEdgesByTenantId(TenantId tenantId) { + log.trace("Executing deleteEdgesByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + tenantEdgesRemover.removeEntities(tenantId, tenantId); + } + + @Override + public PageData findEdgesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { + log.trace("Executing findEdgesByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validatePageLink(pageLink); + return edgeDao.findEdgesByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); + } + + @Override + public PageData findEdgesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { + log.trace("Executing findEdgesByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return edgeDao.findEdgesByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); + } + + @Override + public PageData findEdgeInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) { + log.trace("Executing findEdgeInfosByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validatePageLink(pageLink); + return edgeDao.findEdgeInfosByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); + } + + @Override + public PageData findEdgeInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) { + log.trace("Executing findEdgeInfosByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return edgeDao.findEdgeInfosByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); + } + + @Override + public ListenableFuture> findEdgesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List edgeIds) { + log.trace("Executing findEdgesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], edgeIds [{}]", tenantId, customerId, edgeIds); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateIds(edgeIds, "Incorrect edgeIds " + edgeIds); + return edgeDao.findEdgesByTenantIdCustomerIdAndIdsAsync(tenantId.getId(), + customerId.getId(), toUUIDs(edgeIds)); + } + + @Override + public void unassignCustomerEdges(TenantId tenantId, CustomerId customerId) { + log.trace("Executing unassignCustomerEdges, tenantId [{}], customerId [{}]", tenantId, customerId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + customerEdgeUnassigner.removeEntities(tenantId, customerId); + } + + @Override + public ListenableFuture> findEdgesByQuery(TenantId tenantId, EdgeSearchQuery query) { + log.trace("[{}] Executing findEdgesByQuery [{}]", tenantId, query); + ListenableFuture> relations = relationService.findByQuery(tenantId, query.toEntitySearchQuery()); + ListenableFuture> edges = Futures.transformAsync(relations, r -> { + EntitySearchDirection direction = query.toEntitySearchQuery().getParameters().getDirection(); + List> futures = new ArrayList<>(); + for (EntityRelation relation : r) { + EntityId entityId = direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom(); + if (entityId.getEntityType() == EntityType.EDGE) { + futures.add(findEdgeByIdAsync(tenantId, new EdgeId(entityId.getId()))); + } + } + return Futures.successfulAsList(futures); + }, MoreExecutors.directExecutor()); + + edges = Futures.transform(edges, new Function, List>() { + @Nullable + @Override + public List apply(@Nullable List edgeList) { + return edgeList == null ? Collections.emptyList() : edgeList.stream().filter(edge -> query.getEdgeTypes().contains(edge.getType())).collect(Collectors.toList()); + } + }, MoreExecutors.directExecutor()); + + return edges; + } + + @Override + public ListenableFuture> findEdgeTypesByTenantId(TenantId tenantId) { + log.trace("Executing findEdgeTypesByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + ListenableFuture> tenantEdgeTypes = edgeDao.findTenantEdgeTypesAsync(tenantId.getId()); + return Futures.transform(tenantEdgeTypes, + edgeTypes -> { + edgeTypes.sort(Comparator.comparing(EntitySubtype::getType)); + return edgeTypes; + }, MoreExecutors.directExecutor()); + } + + @Override + public void assignDefaultRuleChainsToEdge(TenantId tenantId, EdgeId edgeId) { + log.trace("Executing assignDefaultRuleChainsToEdge, tenantId [{}], edgeId [{}]", tenantId, edgeId); + ListenableFuture> future = ruleChainService.findAutoAssignToEdgeRuleChainsByTenantId(tenantId); + Futures.addCallback(future, new FutureCallback>() { + @Override + public void onSuccess(List ruleChains) { + if (ruleChains != null && !ruleChains.isEmpty()) { + for (RuleChain ruleChain : ruleChains) { + ruleChainService.assignRuleChainToEdge(tenantId, ruleChain.getId(), edgeId); + } + } + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] can't find default edge rule chains [{}]", tenantId.getId(), edgeId.getId(), t); + } + }, MoreExecutors.directExecutor()); + } + + @Override + public ListenableFuture> findEdgesByTenantIdAndRuleChainId(TenantId tenantId, RuleChainId ruleChainId) { + log.trace("Executing findEdgesByTenantIdAndRuleChainId, tenantId [{}], ruleChainId [{}]", tenantId, ruleChainId); + Validator.validateId(tenantId, "Incorrect tenantId " + tenantId); + Validator.validateId(ruleChainId, "Incorrect ruleChainId " + ruleChainId); + return edgeDao.findEdgesByTenantIdAndRuleChainId(tenantId.getId(), ruleChainId.getId()); + } + + @Override + public ListenableFuture> findEdgesByTenantIdAndDashboardId(TenantId tenantId, DashboardId dashboardId) { + log.trace("Executing findEdgesByTenantIdAndDashboardId, tenantId [{}], dashboardId [{}]", tenantId, dashboardId); + Validator.validateId(tenantId, "Incorrect tenantId " + tenantId); + Validator.validateId(dashboardId, "Incorrect dashboardId " + dashboardId); + return edgeDao.findEdgesByTenantIdAndDashboardId(tenantId.getId(), dashboardId.getId()); + } + + private DataValidator edgeValidator = + new DataValidator() { + + @Override + protected void validateCreate(TenantId tenantId, Edge edge) { + } + + @Override + protected void validateUpdate(TenantId tenantId, Edge edge) { + } + + @Override + protected void validateDataImpl(TenantId tenantId, Edge edge) { + if (StringUtils.isEmpty(edge.getType())) { + throw new DataValidationException("Edge type should be specified!"); + } + if (StringUtils.isEmpty(edge.getName())) { + throw new DataValidationException("Edge name should be specified!"); + } + if (StringUtils.isEmpty(edge.getSecret())) { + throw new DataValidationException("Edge secret should be specified!"); + } + if (StringUtils.isEmpty(edge.getRoutingKey())) { + throw new DataValidationException("Edge routing key should be specified!"); + } + if (StringUtils.isEmpty(edge.getEdgeLicenseKey())) { + throw new DataValidationException("Edge license key should be specified!"); + } + if (StringUtils.isEmpty(edge.getCloudEndpoint())) { + throw new DataValidationException("Cloud endpoint should be specified!"); + } + if (edge.getTenantId() == null) { + throw new DataValidationException("Edge should be assigned to tenant!"); + } else { + Tenant tenant = tenantDao.findById(edge.getTenantId(), edge.getTenantId().getId()); + if (tenant == null) { + throw new DataValidationException("Edge is referencing to non-existent tenant!"); + } + } + if (edge.getCustomerId() == null) { + edge.setCustomerId(new CustomerId(NULL_UUID)); + } else if (!edge.getCustomerId().getId().equals(NULL_UUID)) { + Customer customer = customerDao.findById(edge.getTenantId(), edge.getCustomerId().getId()); + if (customer == null) { + throw new DataValidationException("Can't assign edge to non-existent customer!"); + } + if (!customer.getTenantId().getId().equals(edge.getTenantId().getId())) { + throw new DataValidationException("Can't assign edge to customer from different tenant!"); + } + } + } + }; + + private PaginatedRemover tenantEdgesRemover = + new PaginatedRemover() { + + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return edgeDao.findEdgesByTenantId(id.getId(), pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, Edge entity) { + deleteEdge(tenantId, new EdgeId(entity.getUuidId())); + } + }; + + private PaginatedRemover customerEdgeUnassigner = new PaginatedRemover() { + + @Override + protected PageData findEntities(TenantId tenantId, CustomerId id, PageLink pageLink) { + return edgeDao.findEdgesByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, Edge entity) { + unassignEdgeFromCustomer(tenantId, new EdgeId(entity.getUuidId())); + } + }; + + @Override + public ListenableFuture> findRelatedEdgeIdsByEntityId(TenantId tenantId, EntityId entityId) { + // TODO: voba - rewrite 'find' to use native SQL queries instead of fetching relations + + log.trace("[{}] Executing findRelatedEdgeIdsByEntityId [{}]", tenantId, entityId); + if (EntityType.TENANT.equals(entityId.getEntityType()) || + EntityType.CUSTOMER.equals(entityId.getEntityType()) || + EntityType.DEVICE_PROFILE.equals(entityId.getEntityType())) { + List result = new ArrayList<>(); + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + if (EntityType.TENANT.equals(entityId.getEntityType()) || + EntityType.DEVICE_PROFILE.equals(entityId.getEntityType())) { + pageData = findEdgesByTenantId(tenantId, pageLink); + } else { + pageData = findEdgesByTenantIdAndCustomerId(tenantId, new CustomerId(entityId.getId()), pageLink); + } + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + for (Edge edge : pageData.getData()) { + result.add(edge.getId()); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + return Futures.immediateFuture(result); + } else { + switch (entityId.getEntityType()) { + case DEVICE: + case ASSET: + case ENTITY_VIEW: + ListenableFuture> originatorEdgeRelationsFuture = + relationService.findByToAndTypeAsync(tenantId, entityId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE); + return Futures.transform(originatorEdgeRelationsFuture, originatorEdgeRelations -> { + if (originatorEdgeRelations != null && originatorEdgeRelations.size() > 0 && + originatorEdgeRelations.get(0).getFrom() != null) { + return Collections.singletonList(new EdgeId(originatorEdgeRelations.get(0).getFrom().getId())); + } else { + return Collections.emptyList(); + } + }, MoreExecutors.directExecutor()); + case DASHBOARD: + return convertToEdgeIds(findEdgesByTenantIdAndDashboardId(tenantId, new DashboardId(entityId.getId()))); + case RULE_CHAIN: + return convertToEdgeIds(findEdgesByTenantIdAndRuleChainId(tenantId, new RuleChainId(entityId.getId()))); + case USER: + User userById = userService.findUserById(tenantId, new UserId(entityId.getId())); + if (userById == null) { + return Futures.immediateFuture(Collections.emptyList()); + } + List result = new ArrayList<>(); + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + if (userById.getCustomerId() == null || userById.getCustomerId().isNullUid()) { + pageData = findEdgesByTenantId(tenantId, pageLink); + } else { + pageData = findEdgesByTenantIdAndCustomerId(tenantId, new CustomerId(entityId.getId()), pageLink); + } + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + result.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + return convertToEdgeIds(Futures.immediateFuture(result)); + default: + return Futures.immediateFuture(Collections.emptyList()); + } + } + } + + private ListenableFuture> convertToEdgeIds(ListenableFuture> future) { + return Futures.transform(future, edges -> { + if (edges != null && !edges.isEmpty()) { + return edges.stream().map(IdBased::getId).collect(Collectors.toList()); + } else { + return Collections.emptyList(); + } + }, MoreExecutors.directExecutor()); + } + + @Override + public Object checkInstance(Object request) { + return this.restTemplate.postForEntity(EDGE_LICENSE_SERVER_ENDPOINT + "/api/license/checkInstance", request, Object.class, new Object[0]); + } + + @Override + public Object activateInstance(String edgeLicenseSecret, String releaseDate) { + Map params = new HashMap<>(); + params.put("licenseSecret", edgeLicenseSecret); + params.put("releaseDate", releaseDate); + return this.restTemplate.postForEntity(EDGE_LICENSE_SERVER_ENDPOINT + "/api/license/activateInstance?licenseSecret={licenseSecret}&releaseDate={releaseDate}", (Object) null, Object.class, params); + } + + @Override + public String findMissingToRelatedRuleChains(TenantId tenantId, EdgeId edgeId) { + List edgeRuleChains = findEdgeRuleChains(tenantId, edgeId); + List edgeRuleChainIds = edgeRuleChains.stream().map(IdBased::getId).collect(Collectors.toList()); + ObjectNode result = mapper.createObjectNode(); + for (RuleChain edgeRuleChain : edgeRuleChains) { + List connectionInfos = + ruleChainService.loadRuleChainMetaData(edgeRuleChain.getTenantId(), edgeRuleChain.getId()).getRuleChainConnections(); + if (connectionInfos != null && !connectionInfos.isEmpty()) { + List connectedRuleChains = + connectionInfos.stream().map(RuleChainConnectionInfo::getTargetRuleChainId).collect(Collectors.toList()); + List missingRuleChains = new ArrayList<>(); + for (RuleChainId connectedRuleChain : connectedRuleChains) { + if (!edgeRuleChainIds.contains(connectedRuleChain)) { + RuleChain ruleChainById = ruleChainService.findRuleChainById(tenantId, connectedRuleChain); + missingRuleChains.add(ruleChainById.getName()); + } + } + if (!missingRuleChains.isEmpty()) { + ArrayNode array = mapper.createArrayNode(); + for (String missingRuleChain : missingRuleChains) { + array.add(missingRuleChain); + } + result.set(edgeRuleChain.getName(), array); + } + } + } + return result.toString(); + } + + private List findEdgeRuleChains(TenantId tenantId, EdgeId edgeId) { + List result = new ArrayList<>(); + PageLink pageLink = new PageLink(DEFAULT_LIMIT); + PageData pageData; + do { + pageData = ruleChainService.findRuleChainsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + result.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + return result; + } + + private void initRestTemplate() { + boolean jdkHttpClientEnabled = isNotEmpty(System.getProperty("tb.proxy.jdk")) && System.getProperty("tb.proxy.jdk").equalsIgnoreCase("true"); + boolean systemProxyEnabled = isNotEmpty(System.getProperty("tb.proxy.system")) && System.getProperty("tb.proxy.system").equalsIgnoreCase("true"); + boolean proxyEnabled = isNotEmpty(System.getProperty("tb.proxy.host")) && isNotEmpty(System.getProperty("tb.proxy.port")); + if (jdkHttpClientEnabled) { + log.warn("Going to use plain JDK Http Client!"); + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + if (proxyEnabled) { + log.warn("Going to use Proxy Server: [{}:{}]", System.getProperty("tb.proxy.host"), System.getProperty("tb.proxy.port")); + factory.setProxy(new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(System.getProperty("tb.proxy.host"), Integer.parseInt(System.getProperty("tb.proxy.port"))))); + } + + this.restTemplate = new RestTemplate(new SimpleClientHttpRequestFactory()); + } else { + CloseableHttpClient httpClient; + HttpComponentsClientHttpRequestFactory requestFactory; + if (systemProxyEnabled) { + log.warn("Going to use System Proxy Server!"); + httpClient = HttpClients.createSystem(); + requestFactory = new HttpComponentsClientHttpRequestFactory(); + requestFactory.setHttpClient(httpClient); + this.restTemplate = new RestTemplate(requestFactory); + } else if (proxyEnabled) { + log.warn("Going to use Proxy Server: [{}:{}]", System.getProperty("tb.proxy.host"), System.getProperty("tb.proxy.port")); + httpClient = HttpClients.custom().setSSLHostnameVerifier(new DefaultHostnameVerifier()).setProxy(new HttpHost(System.getProperty("tb.proxy.host"), Integer.parseInt(System.getProperty("tb.proxy.port")), "https")).build(); + requestFactory = new HttpComponentsClientHttpRequestFactory(); + requestFactory.setHttpClient(httpClient); + this.restTemplate = new RestTemplate(requestFactory); + } else { + httpClient = HttpClients.custom().setSSLHostnameVerifier(new DefaultHostnameVerifier()).build(); + requestFactory = new HttpComponentsClientHttpRequestFactory(); + requestFactory.setHttpClient(httpClient); + this.restTemplate = new RestTemplate(requestFactory); + } + } + + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java index 5ee19dce75..0c80195d9b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java @@ -18,18 +18,45 @@ package org.thingsboard.server.dao.entity; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; +import java.util.List; import java.util.Optional; @Slf4j public abstract class AbstractEntityService { + public static final String INCORRECT_EDGE_ID = "Incorrect edgeId "; + public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; + @Autowired protected RelationService relationService; + @Autowired + protected EntityViewService entityViewService; + + @Autowired(required = false) + protected EdgeService edgeService; + + protected void createRelation(TenantId tenantId, EntityRelation relation) { + log.debug("Creating relation: {}", relation); + relationService.saveRelation(tenantId, relation); + } + + protected void deleteRelation(TenantId tenantId, EntityRelation relation) { + log.debug("Deleting relation: {}", relation); + relationService.deleteRelation(tenantId, relation); + } + protected void deleteEntityRelations(TenantId tenantId, EntityId entityId) { log.trace("Executing deleteEntityRelations [{}]", entityId); relationService.deleteEntityRelations(tenantId, entityId); @@ -44,4 +71,23 @@ public abstract class AbstractEntityService { return Optional.empty(); } } + + protected void checkAssignedEntityViewsToEdge(TenantId tenantId, EntityId entityId, EdgeId edgeId) { + try { + List entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId).get(); + if (entityViews != null && !entityViews.isEmpty()) { + EntityView entityView = entityViews.get(0); + // TODO: voba - refactor this blocking operation in 3.3+ + Boolean relationExists = relationService.checkRelation(tenantId,edgeId, entityView.getId(), + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE).get(); + if (relationExists) { + throw new DataValidationException("Can't unassign device/asset from edge that is related to entity view and entity view is assigned to edge!"); + } + } + } catch (Exception e) { + log.error("[{}] Exception while finding entity views for entityId [{}]", tenantId, entityId, e); + throw new RuntimeException("Exception while finding entity views for entityId [" + entityId + "]", e); + } + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 541b7d4ec2..5dc9cf4eed 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.RuleChainId; @@ -148,6 +149,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe case RULE_CHAIN: hasName = ruleChainService.findRuleChainByIdAsync(tenantId, new RuleChainId(entityId.getId())); break; + case EDGE: + hasName = edgeService.findEdgeByIdAsync(tenantId, new EdgeId(entityId.getId())); + break; default: throw new IllegalStateException("Not Implemented!"); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java index 49e6e84bd9..c4a9e0d871 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java @@ -106,8 +106,8 @@ public interface EntityViewDao extends Dao { * @return the list of entity view objects */ PageData findEntityViewsByTenantIdAndCustomerId(UUID tenantId, - UUID customerId, - PageLink pageLink); + UUID customerId, + PageLink pageLink); /** * Find entity view infos by tenantId, customerId and page link. @@ -129,9 +129,9 @@ public interface EntityViewDao extends Dao { * @return the list of entity view objects */ PageData findEntityViewsByTenantIdAndCustomerIdAndType(UUID tenantId, - UUID customerId, - String type, - PageLink pageLink); + UUID customerId, + String type, + PageLink pageLink); /** * Find entity view infos by tenantId, customerId, type and page link. @@ -153,4 +153,30 @@ public interface EntityViewDao extends Dao { */ ListenableFuture> findTenantEntityViewTypesAsync(UUID tenantId); + /** + * Find entity views by tenantId, edgeId and page link. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param pageLink the page link + * @return the list of entity view objects + */ + PageData findEntityViewsByTenantIdAndEdgeId(UUID tenantId, + UUID edgeId, + PageLink pageLink); + + /** + * Find entity views by tenantId, edgeId, type and page link. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param type the type + * @param pageLink the page link + * @return the list of entity view objects + */ + PageData findEntityViewsByTenantIdAndEdgeIdAndType(UUID tenantId, + UUID edgeId, + String type, + PageLink pageLink); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index 5b6ae32437..304b04a33d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -35,15 +35,19 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; 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.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; @@ -58,6 +62,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Optional; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.CacheConstants.ENTITY_VIEW_CACHE; @@ -77,6 +82,7 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; public static final String INCORRECT_ENTITY_VIEW_ID = "Incorrect entityViewId "; + public static final String INCORRECT_EDGE_ID = "Incorrect edgeId "; @Autowired private EntityViewDao entityViewDao; @@ -328,6 +334,73 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti }, MoreExecutors.directExecutor()); } + @CacheEvict(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityViewId}") + @Override + public EntityView assignEntityViewToEdge(TenantId tenantId, EntityViewId entityViewId, EdgeId edgeId) { + EntityView entityView = findEntityViewById(tenantId, entityViewId); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null) { + throw new DataValidationException("Can't assign entityView to non-existent edge!"); + } + if (!edge.getTenantId().getId().equals(entityView.getTenantId().getId())) { + throw new DataValidationException("Can't assign entityView to edge from different tenant!"); + } + + try { + Boolean relationExists = relationService.checkRelation(tenantId, edgeId, entityView.getEntityId(), + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE).get(); + if (!relationExists) { + throw new DataValidationException("Can't assign entity view to edge because related device/asset doesn't assigned to edge!"); + } + } catch (ExecutionException | InterruptedException e) { + log.error("Exception during relation check", e); + throw new RuntimeException("Exception during relation check", e); + } + + try { + createRelation(tenantId, new EntityRelation(edgeId, entityViewId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to create entityView relation. Edge Id: [{}]", entityViewId, edgeId); + throw new RuntimeException(e); + } + return entityView; + } + + @Override + public EntityView unassignEntityViewFromEdge(TenantId tenantId, EntityViewId entityViewId, EdgeId edgeId) { + EntityView entityView = findEntityViewById(tenantId, entityViewId); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null) { + throw new DataValidationException("Can't unassign entityView from non-existent edge!"); + } + try { + deleteRelation(tenantId, new EntityRelation(edgeId, entityViewId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to delete entityView relation. Edge Id: [{}]", entityViewId, edgeId); + throw new RuntimeException(e); + } + return entityView; + } + + @Override + public PageData findEntityViewsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink) { + log.trace("Executing findEntityViewsByTenantIdAndEdgeId, tenantId [{}], edgeId [{}], pageLink [{}]", tenantId, edgeId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + validatePageLink(pageLink); + return entityViewDao.findEntityViewsByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink); + } + + @Override + public PageData findEntityViewsByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink) { + log.trace("Executing findEntityViewsByTenantIdAndEdgeIdAndType, tenantId [{}], edgeId [{}], type [{}], pageLink [{}]", tenantId, edgeId, type, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(edgeId, INCORRECT_EDGE_ID + edgeId); + validateString(type, "Incorrect type " + type); + validatePageLink(pageLink); + return entityViewDao.findEntityViewsByTenantIdAndEdgeIdAndType(tenantId.getId(), edgeId.getId(), type, pageLink); + } + private DataValidator entityViewValidator = new DataValidator() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java index 4cc853b2b9..91bfa954fd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java @@ -23,6 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.event.EventFilter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -30,7 +31,6 @@ import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; -import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Optional; @@ -111,6 +111,11 @@ public class BaseEventService implements EventService { return eventDao.findLatestEvents(tenantId.getId(), entityId, eventType, limit); } + @Override + public PageData findEventsByFilter(TenantId tenantId, EntityId entityId, EventFilter eventFilter, TimePageLink pageLink) { + return eventDao.findEventByFilter(tenantId.getId(), entityId, eventFilter, pageLink); + } + @Override public void removeEvents(TenantId tenantId, EntityId entityId) { PageData eventPageData; diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java index 0cb4fccfb3..ba4e86c95e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.event; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.event.EventFilter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -88,6 +89,8 @@ public interface EventDao extends Dao { */ PageData findEvents(UUID tenantId, EntityId entityId, String eventType, TimePageLink pageLink); + PageData findEventByFilter(UUID tenantId, EntityId entityId, EventFilter eventFilter, TimePageLink pageLink); + /** * Find latest events by tenantId, entityId and eventType. * diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 0fb748c83a..76a904b385 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -373,11 +373,13 @@ public class ModelConstants { public static final String RULE_CHAIN_COLUMN_FAMILY_NAME = "rule_chain"; public static final String RULE_CHAIN_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; public static final String RULE_CHAIN_NAME_PROPERTY = "name"; + public static final String RULE_CHAIN_TYPE_PROPERTY = "type"; public static final String RULE_CHAIN_FIRST_RULE_NODE_ID_PROPERTY = "first_rule_node_id"; public static final String RULE_CHAIN_ROOT_PROPERTY = "root"; public static final String RULE_CHAIN_CONFIGURATION_PROPERTY = "configuration"; public static final String RULE_CHAIN_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "rule_chain_by_tenant_and_search_text"; + public static final String RULE_CHAIN_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "rule_chain_by_tenant_by_type_and_search_text"; /** * Cassandra rule node constants. @@ -484,6 +486,42 @@ public class ModelConstants { public static final String FIRMWARE_ADDITIONAL_INFO_COLUMN = ADDITIONAL_INFO_PROPERTY; public static final String FIRMWARE_HAS_DATA_PROPERTY = "has_data"; + /** + * Edge constants. + */ + public static final String EDGE_COLUMN_FAMILY_NAME = "edge"; + public static final String EDGE_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String EDGE_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; + public static final String EDGE_ROOT_RULE_CHAIN_ID_PROPERTY = "root_rule_chain_id"; + public static final String EDGE_NAME_PROPERTY = "name"; + public static final String EDGE_LABEL_PROPERTY = "label"; + public static final String EDGE_TYPE_PROPERTY = "type"; + public static final String EDGE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; + public static final String EDGE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "edge_by_tenant_and_search_text"; + public static final String EDGE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "edge_by_tenant_by_type_and_search_text"; + public static final String EDGE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "edge_by_customer_and_search_text"; + public static final String EDGE_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "edge_by_customer_by_type_and_search_text"; + public static final String EDGE_BY_TENANT_AND_NAME_VIEW_NAME = "edge_by_tenant_and_name"; + public static final String EDGE_BY_ROUTING_KEY_VIEW_NAME = "edge_by_routing_key"; + + public static final String EDGE_ROUTING_KEY_PROPERTY = "routing_key"; + public static final String EDGE_SECRET_PROPERTY = "secret"; + public static final String EDGE_LICENSE_KEY_PROPERTY = "edge_license_key"; + public static final String EDGE_CLOUD_ENDPOINT_KEY_PROPERTY = "cloud_endpoint"; + + /** + * Edge queue constants. + */ + public static final String EDGE_EVENT_COLUMN_FAMILY_NAME = "edge_event"; + public static final String EDGE_EVENT_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String EDGE_EVENT_EDGE_ID_PROPERTY = "edge_id"; + public static final String EDGE_EVENT_TYPE_PROPERTY = "edge_event_type"; + public static final String EDGE_EVENT_ACTION_PROPERTY = "edge_event_action"; + public static final String EDGE_EVENT_UID_PROPERTY = "edge_event_uid"; + public static final String EDGE_EVENT_ENTITY_ID_PROPERTY = "entity_id"; + public static final String EDGE_EVENT_BODY_PROPERTY = "body"; + + public static final String EDGE_EVENT_BY_ID_VIEW_NAME = "edge_event_by_id"; /** * Cassandra attributes and timeseries constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java new file mode 100644 index 0000000000..bb9e9d98ff --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java @@ -0,0 +1,174 @@ +/** + * Copyright © 2016-2021 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.model.sql; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.CustomerId; +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.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_CLOUD_ENDPOINT_KEY_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_CUSTOMER_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_LABEL_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_LICENSE_KEY_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_NAME_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_ROOT_RULE_CHAIN_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_ROUTING_KEY_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_SECRET_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_TENANT_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_TYPE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; + +@Data +@EqualsAndHashCode(callSuper = true) +@TypeDef(name = "json", typeClass = JsonStringType.class) +@MappedSuperclass +public abstract class AbstractEdgeEntity extends BaseSqlEntity implements SearchTextEntity { + + @Column(name = EDGE_TENANT_ID_PROPERTY, columnDefinition = "uuid") + private UUID tenantId; + + @Column(name = EDGE_CUSTOMER_ID_PROPERTY, columnDefinition = "uuid") + private UUID customerId; + + @Column(name = EDGE_ROOT_RULE_CHAIN_ID_PROPERTY, columnDefinition = "uuid") + private UUID rootRuleChainId; + + @Column(name = EDGE_TYPE_PROPERTY) + private String type; + + @Column(name = EDGE_NAME_PROPERTY) + private String name; + + @Column(name = EDGE_LABEL_PROPERTY) + private String label; + + @Column(name = SEARCH_TEXT_PROPERTY) + private String searchText; + + @Column(name = EDGE_ROUTING_KEY_PROPERTY) + private String routingKey; + + @Column(name = EDGE_SECRET_PROPERTY) + private String secret; + + @Column(name = EDGE_LICENSE_KEY_PROPERTY) + private String edgeLicenseKey; + + @Column(name = EDGE_CLOUD_ENDPOINT_KEY_PROPERTY) + private String cloudEndpoint; + + @Type(type = "json") + @Column(name = ModelConstants.EDGE_ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + + public AbstractEdgeEntity() { + super(); + } + + public AbstractEdgeEntity(Edge edge) { + if (edge.getId() != null) { + this.setUuid(edge.getId().getId()); + } + this.setCreatedTime(edge.getCreatedTime()); + if (edge.getTenantId() != null) { + this.tenantId = edge.getTenantId().getId(); + } + if (edge.getCustomerId() != null) { + this.customerId = edge.getCustomerId().getId(); + } + if (edge.getRootRuleChainId() != null) { + this.rootRuleChainId = edge.getRootRuleChainId().getId(); + } + this.type = edge.getType(); + this.name = edge.getName(); + this.label = edge.getLabel(); + this.routingKey = edge.getRoutingKey(); + this.secret = edge.getSecret(); + this.edgeLicenseKey = edge.getEdgeLicenseKey(); + this.cloudEndpoint = edge.getCloudEndpoint(); + this.additionalInfo = edge.getAdditionalInfo(); + } + + public AbstractEdgeEntity(EdgeEntity edgeEntity) { + this.setId(edgeEntity.getId()); + this.setCreatedTime(edgeEntity.getCreatedTime()); + this.tenantId = edgeEntity.getTenantId(); + this.customerId = edgeEntity.getCustomerId(); + this.rootRuleChainId = edgeEntity.getRootRuleChainId(); + this.type = edgeEntity.getType(); + this.name = edgeEntity.getName(); + this.label = edgeEntity.getLabel(); + this.searchText = edgeEntity.getSearchText(); + this.routingKey = edgeEntity.getRoutingKey(); + this.secret = edgeEntity.getSecret(); + this.edgeLicenseKey = edgeEntity.getEdgeLicenseKey(); + this.cloudEndpoint = edgeEntity.getCloudEndpoint(); + this.additionalInfo = edgeEntity.getAdditionalInfo(); + } + + public String getSearchText() { + return searchText; + } + + @Override + public String getSearchTextSource() { + return name; + } + + @Override + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + protected Edge toEdge() { + Edge edge = new Edge(new EdgeId(getUuid())); + edge.setCreatedTime(createdTime); + if (tenantId != null) { + edge.setTenantId(new TenantId(tenantId)); + } + if (customerId != null) { + edge.setCustomerId(new CustomerId(customerId)); + } + if (rootRuleChainId != null) { + edge.setRootRuleChainId(new RuleChainId(rootRuleChainId)); + } + edge.setType(type); + edge.setName(name); + edge.setLabel(label); + edge.setRoutingKey(routingKey); + edge.setSecret(secret); + edge.setEdgeLicenseKey(edgeLicenseKey); + edge.setCloudEndpoint(cloudEndpoint); + edge.setAdditionalInfo(additionalInfo); + return edge; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java index 2fb67b5cf6..2181aefc7d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEntityViewEntity.java @@ -25,6 +25,7 @@ import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEntity.java new file mode 100644 index 0000000000..e736c74d38 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEntity.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2021 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.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Entity; +import javax.persistence.Table; + +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_COLUMN_FAMILY_NAME; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = EDGE_COLUMN_FAMILY_NAME) +public class EdgeEntity extends AbstractEdgeEntity { + + public EdgeEntity() { + super(); + } + + public EdgeEntity(Edge edge) { + super(edge); + } + + @Override + public Edge toData() { + return super.toEdge(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java new file mode 100644 index 0000000000..2aad852c89 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java @@ -0,0 +1,131 @@ +/** + * Copyright © 2016-2021 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.model.sql; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.EdgeEventId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseEntity; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Table; +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_ACTION_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_COLUMN_FAMILY_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_EDGE_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_BODY_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_ENTITY_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TENANT_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TYPE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_UID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EPOCH_DIFF; +import static org.thingsboard.server.dao.model.ModelConstants.EVENT_UID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = EDGE_EVENT_COLUMN_FAMILY_NAME) +@NoArgsConstructor +public class EdgeEventEntity extends BaseSqlEntity implements BaseEntity { + + @Column(name = EDGE_EVENT_TENANT_ID_PROPERTY) + private UUID tenantId; + + @Column(name = EDGE_EVENT_EDGE_ID_PROPERTY) + private UUID edgeId; + + @Column(name = EDGE_EVENT_ENTITY_ID_PROPERTY) + private UUID entityId; + + @Enumerated(EnumType.STRING) + @Column(name = EDGE_EVENT_TYPE_PROPERTY) + private EdgeEventType edgeEventType; + + @Enumerated(EnumType.STRING) + @Column(name = EDGE_EVENT_ACTION_PROPERTY) + private EdgeEventActionType edgeEventAction; + + @Type(type = "json") + @Column(name = EDGE_EVENT_BODY_PROPERTY) + private JsonNode entityBody; + + @Column(name = EDGE_EVENT_UID_PROPERTY) + private String edgeEventUid; + + @Column(name = TS_COLUMN) + private long ts; + + public EdgeEventEntity(EdgeEvent edgeEvent) { + if (edgeEvent.getId() != null) { + this.setUuid(edgeEvent.getId().getId()); + this.ts = getTs(edgeEvent.getId().getId()); + } else { + this.ts = System.currentTimeMillis(); + } + this.setCreatedTime(edgeEvent.getCreatedTime()); + if (edgeEvent.getTenantId() != null) { + this.tenantId = edgeEvent.getTenantId().getId(); + } + if (edgeEvent.getEdgeId() != null) { + this.edgeId = edgeEvent.getEdgeId().getId(); + } + if (edgeEvent.getEntityId() != null) { + this.entityId = edgeEvent.getEntityId(); + } + this.edgeEventType = edgeEvent.getType(); + this.edgeEventAction = edgeEvent.getAction(); + this.entityBody = edgeEvent.getBody(); + this.edgeEventUid = edgeEvent.getUid(); + } + + @Override + public EdgeEvent toData() { + EdgeEvent edgeEvent = new EdgeEvent(new EdgeEventId(this.getUuid())); + edgeEvent.setCreatedTime(createdTime); + edgeEvent.setTenantId(new TenantId(tenantId)); + edgeEvent.setEdgeId(new EdgeId(edgeId)); + if (entityId != null) { + edgeEvent.setEntityId(entityId); + } + edgeEvent.setType(edgeEventType); + edgeEvent.setAction(edgeEventAction); + edgeEvent.setBody(entityBody); + edgeEvent.setUid(edgeEventUid); + return edgeEvent; + } + + private static long getTs(UUID uuid) { + return (uuid.timestamp() - EPOCH_DIFF) / 10000; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeInfoEntity.java new file mode 100644 index 0000000000..f7c86831f7 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeInfoEntity.java @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2021 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.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.edge.EdgeInfo; + +import java.util.HashMap; +import java.util.Map; + +@Data +@EqualsAndHashCode(callSuper = true) +public class EdgeInfoEntity extends AbstractEdgeEntity { + + public static final Map edgeInfoColumnMap = new HashMap<>(); + static { + edgeInfoColumnMap.put("customerTitle", "c.title"); + } + + private String customerTitle; + private boolean customerIsPublic; + + public EdgeInfoEntity() { + super(); + } + + public EdgeInfoEntity(EdgeEntity edgeEntity, + String customerTitle, + Object customerAdditionalInfo) { + super(edgeEntity); + this.customerTitle = customerTitle; + if (customerAdditionalInfo != null && ((JsonNode)customerAdditionalInfo).has("isPublic")) { + this.customerIsPublic = ((JsonNode)customerAdditionalInfo).get("isPublic").asBoolean(); + } else { + this.customerIsPublic = false; + } + } + + @Override + public EdgeInfo toData() { + return new EdgeInfo(super.toEdge(), customerTitle, customerIsPublic); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java index dd5bbfc10b..d8e820a87e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java @@ -25,10 +25,6 @@ import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Entity; import javax.persistence.Table; -/** - * Created by Victor Basanets on 8/30/2017. - */ - @Data @EqualsAndHashCode(callSuper = true) @Entity diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java index 3909d55c44..be195723bd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java @@ -24,6 +24,7 @@ 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.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; @@ -32,6 +33,8 @@ import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; import javax.persistence.Table; import java.util.UUID; @@ -48,6 +51,10 @@ public class RuleChainEntity extends BaseSqlEntity implements SearchT @Column(name = ModelConstants.RULE_CHAIN_NAME_PROPERTY) private String name; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.RULE_CHAIN_TYPE_PROPERTY) + private RuleChainType type; + @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) private String searchText; @@ -78,6 +85,7 @@ public class RuleChainEntity extends BaseSqlEntity implements SearchT this.setCreatedTime(ruleChain.getCreatedTime()); this.tenantId = DaoUtil.getId(ruleChain.getTenantId()); this.name = ruleChain.getName(); + this.type = ruleChain.getType(); this.searchText = ruleChain.getName(); if (ruleChain.getFirstRuleNodeId() != null) { this.firstRuleNodeId = ruleChain.getFirstRuleNodeId().getId(); @@ -104,6 +112,7 @@ public class RuleChainEntity extends BaseSqlEntity implements SearchT ruleChain.setCreatedTime(createdTime); ruleChain.setTenantId(new TenantId(tenantId)); ruleChain.setName(name); + ruleChain.setType(type); if (firstRuleNodeId != null) { ruleChain.setFirstRuleNodeId(new RuleNodeId(firstRuleNodeId)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index 44b328fd9f..15ddb9a390 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -179,11 +179,28 @@ public class BaseRelationService implements RelationService { @Override public void deleteEntityRelations(TenantId tenantId, EntityId entityId) { - try { - deleteEntityRelationsAsync(tenantId, entityId).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); + log.trace("Executing deleteEntityRelations [{}]", entityId); + validate(entityId); + final Cache cache = cacheManager.getCache(RELATIONS_CACHE); + List inboundRelations = new ArrayList<>(); + for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { + inboundRelations.addAll(relationDao.findAllByTo(tenantId, entityId, typeGroup)); + } + + List outboundRelations = new ArrayList<>(); + for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { + outboundRelations.addAll(relationDao.findAllByFrom(tenantId, entityId, typeGroup)); + } + + for (EntityRelation relation : inboundRelations){ + delete(tenantId, cache, relation, true); } + + for (EntityRelation relation : outboundRelations){ + delete(tenantId, cache, relation, false); + } + + relationDao.deleteOutboundRelations(tenantId, entityId); } @Override @@ -193,14 +210,14 @@ public class BaseRelationService implements RelationService { validate(entityId); List>> inboundRelationsList = new ArrayList<>(); for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { - inboundRelationsList.add(relationDao.findAllByTo(tenantId, entityId, typeGroup)); + inboundRelationsList.add(relationDao.findAllByToAsync(tenantId, entityId, typeGroup)); } ListenableFuture>> inboundRelations = Futures.allAsList(inboundRelationsList); List>> outboundRelationsList = new ArrayList<>(); for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { - outboundRelationsList.add(relationDao.findAllByFrom(tenantId, entityId, typeGroup)); + outboundRelationsList.add(relationDao.findAllByFromAsync(tenantId, entityId, typeGroup)); } ListenableFuture>> outboundRelations = Futures.allAsList(outboundRelationsList); @@ -242,6 +259,15 @@ public class BaseRelationService implements RelationService { } } + boolean delete(TenantId tenantId, Cache cache, EntityRelation relation, boolean deleteFromDb) { + cacheEviction(relation, cache); + if (deleteFromDb) { + return relationDao.deleteRelation(tenantId, relation); + } else { + return false; + } + } + private void cacheEviction(EntityRelation relation, Cache cache) { List fromToTypeAndTypeGroup = new ArrayList<>(); fromToTypeAndTypeGroup.add(relation.getFrom()); @@ -283,7 +309,7 @@ public class BaseRelationService implements RelationService { validate(from); validateTypeGroup(typeGroup); try { - return relationDao.findAllByFrom(tenantId, from, typeGroup).get(); + return relationDao.findAllByFromAsync(tenantId, from, typeGroup).get(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } @@ -306,7 +332,7 @@ public class BaseRelationService implements RelationService { if (fromCache != null) { return Futures.immediateFuture(fromCache); } else { - ListenableFuture> relationsFuture = relationDao.findAllByFrom(tenantId, from, typeGroup); + ListenableFuture> relationsFuture = relationDao.findAllByFromAsync(tenantId, from, typeGroup); Futures.addCallback(relationsFuture, new FutureCallback>() { @Override @@ -327,7 +353,7 @@ public class BaseRelationService implements RelationService { log.trace("Executing findInfoByFrom [{}][{}]", from, typeGroup); validate(from); validateTypeGroup(typeGroup); - ListenableFuture> relations = relationDao.findAllByFrom(tenantId, from, typeGroup); + ListenableFuture> relations = relationDao.findAllByFromAsync(tenantId, from, typeGroup); return Futures.transformAsync(relations, relations1 -> { List> futures = new ArrayList<>(); @@ -365,7 +391,7 @@ public class BaseRelationService implements RelationService { validate(to); validateTypeGroup(typeGroup); try { - return relationDao.findAllByTo(tenantId, to, typeGroup).get(); + return relationDao.findAllByToAsync(tenantId, to, typeGroup).get(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } @@ -388,7 +414,7 @@ public class BaseRelationService implements RelationService { if (fromCache != null) { return Futures.immediateFuture(fromCache); } else { - ListenableFuture> relationsFuture = relationDao.findAllByTo(tenantId, to, typeGroup); + ListenableFuture> relationsFuture = relationDao.findAllByToAsync(tenantId, to, typeGroup); Futures.addCallback(relationsFuture, new FutureCallback>() { @Override @@ -409,7 +435,7 @@ public class BaseRelationService implements RelationService { log.trace("Executing findInfoByTo [{}][{}]", to, typeGroup); validate(to); validateTypeGroup(typeGroup); - ListenableFuture> relations = relationDao.findAllByTo(tenantId, to, typeGroup); + ListenableFuture> relations = relationDao.findAllByToAsync(tenantId, to, typeGroup); return Futures.transformAsync(relations, relations1 -> { List> futures = new ArrayList<>(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java index a8c4d9bbc2..d60f176832 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java @@ -31,11 +31,15 @@ import java.util.List; */ public interface RelationDao { - ListenableFuture> findAllByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup); + ListenableFuture> findAllByFromAsync(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup); + + List findAllByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup); ListenableFuture> findAllByFromAndType(TenantId tenantId, EntityId from, String relationType, RelationTypeGroup typeGroup); - ListenableFuture> findAllByTo(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup); + ListenableFuture> findAllByToAsync(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup); + + List findAllByTo(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup); ListenableFuture> findAllByToAndType(TenantId tenantId, EntityId to, String relationType, RelationTypeGroup typeGroup); diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseTbResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java similarity index 55% rename from dao/src/main/java/org/thingsboard/server/dao/resource/BaseTbResourceService.java rename to dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index 0df2f19e80..97675d4b3e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseTbResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -17,10 +17,6 @@ package org.thingsboard.server.dao.resource; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.eclipse.leshan.core.model.DDFFileParser; -import org.eclipse.leshan.core.model.DefaultDDFFileValidator; -import org.eclipse.leshan.core.model.InvalidDDFFileException; -import org.eclipse.leshan.core.model.ObjectModel; import org.hibernate.exception.ConstraintViolationException; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.ResourceType; @@ -29,9 +25,6 @@ import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.lwm2m.LwM2mInstance; -import org.thingsboard.server.common.data.lwm2m.LwM2mObject; -import org.thingsboard.server.common.data.lwm2m.LwM2mResourceObserve; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.exception.DataValidationException; @@ -41,63 +34,30 @@ import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.tenant.TenantDao; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Comparator; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY; -import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_SEARCH_TEXT; import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID; import static org.thingsboard.server.dao.service.Validator.validateId; @Service @Slf4j -public class BaseTbResourceService implements TbResourceService { +public class BaseResourceService implements ResourceService { public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId "; private final TbResourceDao resourceDao; private final TbResourceInfoDao resourceInfoDao; private final TenantDao tenantDao; - private final DDFFileParser ddfFileParser; - public BaseTbResourceService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, TenantDao tenantDao) { + + public BaseResourceService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, TenantDao tenantDao) { this.resourceDao = resourceDao; this.resourceInfoDao = resourceInfoDao; this.tenantDao = tenantDao; - this.ddfFileParser = new DDFFileParser(new DefaultDDFFileValidator()); } @Override - public TbResource saveResource(TbResource resource) throws InvalidDDFFileException, IOException { - log.trace("Executing saveResource [{}]", resource); - if (StringUtils.isEmpty(resource.getData())) { - throw new DataValidationException("Resource data should be specified!"); - } - if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType())) { - List objectModels = - ddfFileParser.parseEx(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText()); - if (!objectModels.isEmpty()) { - ObjectModel objectModel = objectModels.get(0); - - String resourceKey = objectModel.id + LWM2M_SEPARATOR_KEY + objectModel.getVersion(); - String name = objectModel.name; - resource.setResourceKey(resourceKey); - if (resource.getId() == null) { - resource.setTitle(name + " id=" + objectModel.id + " v" + objectModel.getVersion()); - } - resource.setSearchText(resourceKey + LWM2M_SEPARATOR_SEARCH_TEXT + name); - } else { - throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", resource.getSearchText())); - } - } else { - resource.setResourceKey(resource.getFileName()); - } - + public TbResource saveResource(TbResource resource) { resourceValidator.validate(resource, TbResourceInfo::getTenantId); try { @@ -111,7 +71,6 @@ public class BaseTbResourceService implements TbResourceService { throw t; } } - } @Override @@ -156,31 +115,17 @@ public class BaseTbResourceService implements TbResourceService { } @Override - public List findLwM2mObjectPage(TenantId tenantId, String sortProperty, String sortOrder, PageLink pageLink) { - log.trace("Executing findByTenantId [{}]", tenantId); + public List findTenantResourcesByResourceTypeAndObjectIds(TenantId tenantId, ResourceType resourceType, String[] objectIds) { + log.trace("Executing findTenantResourcesByResourceTypeAndObjectIds [{}][{}][{}]", tenantId, resourceType, objectIds); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - PageData resourcePageData = resourceDao.findResourcesByTenantIdAndResourceType( - tenantId, - ResourceType.LWM2M_MODEL, pageLink); - return resourcePageData.getData().stream() - .map(this::toLwM2mObject) - .sorted(getComparator(sortProperty, sortOrder)) - .collect(Collectors.toList()); + return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, objectIds, null); } @Override - public List findLwM2mObject(TenantId tenantId, String sortOrder, - String sortProperty, - String[] objectIds) { - log.trace("Executing findByTenantId [{}]", tenantId); + public PageData findTenantResourcesByResourceTypeAndPageLink(TenantId tenantId, ResourceType resourceType, PageLink pageLink) { + log.trace("Executing findTenantResourcesByResourceTypeAndPageLink [{}][{}][{}]", tenantId, resourceType, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - List resources = resourceDao.findResourcesByTenantIdAndResourceType(tenantId, ResourceType.LWM2M_MODEL, - objectIds, - null); - return resources.stream() - .map(this::toLwM2mObject) - .sorted(getComparator(sortProperty, sortOrder)) - .collect(Collectors.toList()); + return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, pageLink); } @Override @@ -190,50 +135,6 @@ public class BaseTbResourceService implements TbResourceService { tenantResourcesRemover.removeEntities(tenantId, tenantId); } - private LwM2mObject toLwM2mObject(TbResource resource) { - try { - DDFFileParser ddfFileParser = new DDFFileParser(new DefaultDDFFileValidator()); - List objectModels = - ddfFileParser.parseEx(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText()); - if (objectModels.size() == 0) { - return null; - } else { - ObjectModel obj = objectModels.get(0); - LwM2mObject lwM2mObject = new LwM2mObject(); - lwM2mObject.setId(obj.id); - lwM2mObject.setKeyId(resource.getResourceKey()); - lwM2mObject.setName(obj.name); - lwM2mObject.setMultiple(obj.multiple); - lwM2mObject.setMandatory(obj.mandatory); - LwM2mInstance instance = new LwM2mInstance(); - instance.setId(0); - List resources = new ArrayList<>(); - obj.resources.forEach((k, v) -> { - if (!v.operations.isExecutable()) { - LwM2mResourceObserve lwM2MResourceObserve = new LwM2mResourceObserve(k, v.name, false, false, false); - resources.add(lwM2MResourceObserve); - } - }); - instance.setResources(resources.toArray(LwM2mResourceObserve[]::new)); - lwM2mObject.setInstances(new LwM2mInstance[]{instance}); - return lwM2mObject; - } - } catch (IOException | InvalidDDFFileException e) { - log.error("Could not parse the XML of objectModel with name [{}]", resource.getSearchText(), e); - return null; - } - } - - private Comparator getComparator(String sortProperty, String sortOrder) { - Comparator comparator; - if ("name".equals(sortProperty)) { - comparator = Comparator.comparing(LwM2mObject::getName); - } else { - comparator = Comparator.comparingLong(LwM2mObject::getId); - } - return "DESC".equals(sortOrder) ? comparator.reversed() : comparator; - } - private DataValidator resourceValidator = new DataValidator<>() { @Override @@ -259,9 +160,6 @@ public class BaseTbResourceService implements TbResourceService { throw new DataValidationException("Resource is referencing to non-existent tenant!"); } } - if (resource.getResourceType().equals(ResourceType.LWM2M_MODEL) && toLwM2mObject(resource) == null) { - throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", resource.getSearchText())); - } } }; diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 5eb0bc190a..786d86d8dd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -26,9 +26,12 @@ import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; @@ -44,6 +47,7 @@ import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChainData; import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.entity.AbstractEntityService; @@ -66,6 +70,7 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.DataConstants.TENANT; +import static org.thingsboard.server.dao.service.Validator.validateId; /** * Created by igor on 3/12/18. @@ -75,6 +80,9 @@ import static org.thingsboard.server.common.data.DataConstants.TENANT; public class BaseRuleChainService extends AbstractEntityService implements RuleChainService { private static final int DEFAULT_PAGE_SIZE = 1000; + + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + @Autowired private RuleChainDao ruleChainDao; @@ -89,23 +97,19 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC private TbTenantProfileCache tenantProfileCache; @Override + @Transactional public RuleChain saveRuleChain(RuleChain ruleChain) { ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); RuleChain savedRuleChain = ruleChainDao.save(ruleChain.getTenantId(), ruleChain); if (ruleChain.isRoot() && ruleChain.getId() == null) { - try { - createRelation(ruleChain.getTenantId(), new EntityRelation(savedRuleChain.getTenantId(), savedRuleChain.getId(), - EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); - } catch (ExecutionException | InterruptedException e) { - log.warn("[{}] Failed to create tenant to root rule chain relation. from: [{}], to: [{}]", - savedRuleChain.getTenantId(), savedRuleChain.getId()); - throw new RuntimeException(e); - } + createRelation(ruleChain.getTenantId(), new EntityRelation(savedRuleChain.getTenantId(), savedRuleChain.getId(), + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); } return savedRuleChain; } @Override + @Transactional public boolean setRootRuleChain(TenantId tenantId, RuleChainId ruleChainId) { RuleChain ruleChain = ruleChainDao.findById(tenantId, ruleChainId.getId()); if (!ruleChain.isRoot()) { @@ -138,11 +142,12 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } @Override - public RuleChainMetaData saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData) { + @Transactional + public boolean saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData) { Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id."); RuleChain ruleChain = findRuleChainById(tenantId, ruleChainMetaData.getRuleChainId()); if (ruleChain == null) { - return null; + return false; } if (CollectionUtils.isNotEmpty(ruleChainMetaData.getConnections())) { @@ -177,14 +182,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC for (RuleNode node : toAddOrUpdate) { node.setRuleChainId(ruleChain.getId()); RuleNode savedNode = ruleNodeDao.save(tenantId, node); - try { - createRelation(tenantId, new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(), - EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); - } catch (ExecutionException | InterruptedException e) { - log.warn("[{}] Failed to create rule chain to rule node relation. from: [{}], to: [{}]", - ruleChainMetaData.getRuleChainId(), savedNode.getId()); - throw new RuntimeException(e); - } + createRelation(tenantId, new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(), + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); int index = nodes.indexOf(node); nodes.set(index, savedNode); ruleNodeIndexMap.put(savedNode.getId(), index); @@ -206,12 +205,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC EntityId from = nodes.get(nodeConnection.getFromIndex()).getId(); EntityId to = nodes.get(nodeConnection.getToIndex()).getId(); String type = nodeConnection.getType(); - try { - createRelation(tenantId, new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE)); - } catch (ExecutionException | InterruptedException e) { - log.warn("[{}] Failed to create rule node relation. from: [{}], to: [{}]", from, to); - throw new RuntimeException(e); - } + createRelation(tenantId, new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE)); } } if (ruleChainMetaData.getRuleChainConnections() != null) { @@ -219,16 +213,11 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC EntityId from = nodes.get(nodeToRuleChainConnection.getFromIndex()).getId(); EntityId to = nodeToRuleChainConnection.getTargetRuleChainId(); String type = nodeToRuleChainConnection.getType(); - try { - createRelation(tenantId, new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE, nodeToRuleChainConnection.getAdditionalInfo())); - } catch (ExecutionException | InterruptedException e) { - log.warn("[{}] Failed to create rule node to rule chain relation. from: [{}], to: [{}]", from, to); - throw new RuntimeException(e); - } + createRelation(tenantId, new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE, nodeToRuleChainConnection.getAdditionalInfo())); } } - return loadRuleChainMetaData(tenantId, ruleChainMetaData.getRuleChainId()); + return true; } private void validateCircles(List connectionInfos) { @@ -318,12 +307,21 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override public RuleChain getRootTenantRuleChain(TenantId tenantId) { + return getRootRuleChainByType(tenantId, RuleChainType.CORE); + } + + private RuleChain getRootRuleChainByType(TenantId tenantId, RuleChainType type) { Validator.validateId(tenantId, "Incorrect tenant id for search request."); List relations = relationService.findByFrom(tenantId, tenantId, RelationTypeGroup.RULE_CHAIN); if (relations != null && !relations.isEmpty()) { - EntityRelation relation = relations.get(0); - RuleChainId ruleChainId = new RuleChainId(relation.getTo().getId()); - return findRuleChainById(tenantId, ruleChainId); + for (EntityRelation relation : relations) { + RuleChainId ruleChainId = new RuleChainId(relation.getTo().getId()); + RuleChain ruleChainById = findRuleChainById(tenantId, ruleChainId); + if (type.equals(ruleChainById.getType())) { + return ruleChainById; + } + } + return null; } else { return null; } @@ -387,18 +385,35 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } @Override - public PageData findTenantRuleChains(TenantId tenantId, PageLink pageLink) { + public PageData findTenantRuleChainsByType(TenantId tenantId, RuleChainType type, PageLink pageLink) { Validator.validateId(tenantId, "Incorrect tenant id for search rule chain request."); Validator.validatePageLink(pageLink); - return ruleChainDao.findRuleChainsByTenantId(tenantId.getId(), pageLink); + return ruleChainDao.findRuleChainsByTenantIdAndType(tenantId.getId(), type, pageLink); } @Override + @Transactional public void deleteRuleChainById(TenantId tenantId, RuleChainId ruleChainId) { Validator.validateId(ruleChainId, "Incorrect rule chain id for delete request."); RuleChain ruleChain = ruleChainDao.findById(tenantId, ruleChainId.getId()); - if (ruleChain != null && ruleChain.isRoot()) { - throw new DataValidationException("Deletion of Root Tenant Rule Chain is prohibited!"); + if (ruleChain != null) { + if (ruleChain.isRoot()) { + throw new DataValidationException("Deletion of Root Tenant Rule Chain is prohibited!"); + } + if (RuleChainType.EDGE.equals(ruleChain.getType())) { + try { + List edges = edgeService.findEdgesByTenantIdAndRuleChainId(tenantId, ruleChainId).get(); + if (edges != null && !edges.isEmpty()) { + for (Edge edge : edges) { + if (edge.getRootRuleChainId() != null && edge.getRootRuleChainId().equals(ruleChainId)) { + throw new DataValidationException("Can't delete rule chain that is root for edge [" + edge.getName() + "]. Please assign another root rule chain first to the edge!"); + } + } + } + } catch (InterruptedException | ExecutionException e) { + log.error("Can't get edges by tenant id [{}] and rule chain id [{}]", tenantId.getId(), ruleChainId.getId(), e); + } + } } checkRuleNodesAndDelete(tenantId, ruleChainId); } @@ -425,13 +440,13 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } @Override - public List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite) { + public List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, RuleChainType type, boolean overwrite) { List importResults = new ArrayList<>(); setRandomRuleChainIds(ruleChainData); resetRuleNodeIds(ruleChainData.getMetadata()); resetRuleChainMetadataTenantIds(tenantId, ruleChainData.getMetadata()); if (overwrite) { - List persistentRuleChains = findAllTenantRuleChains(tenantId); + List persistentRuleChains = findAllTenantRuleChains(tenantId, type); for (RuleChain ruleChain : ruleChainData.getRuleChains()) { ComponentLifecycleEvent lifecycleEvent; Optional persistentRuleChainOpt = persistentRuleChains.stream().filter(rc -> rc.getName().equals(ruleChain.getName())).findFirst(); @@ -511,19 +526,19 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } } - private List findAllTenantRuleChains(TenantId tenantId) { + private List findAllTenantRuleChains(TenantId tenantId, RuleChainType type) { PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); - return findAllTenantRuleChainsRecursive(tenantId, new ArrayList<>(), pageLink); + return findAllTenantRuleChainsRecursive(tenantId, new ArrayList<>(), type, pageLink); } - private List findAllTenantRuleChainsRecursive(TenantId tenantId, List accumulator, PageLink pageLink) { - PageData persistentRuleChainData = findTenantRuleChains(tenantId, pageLink); + private List findAllTenantRuleChainsRecursive(TenantId tenantId, List accumulator, RuleChainType type, PageLink pageLink) { + PageData persistentRuleChainData = findTenantRuleChainsByType(tenantId, type, pageLink); List ruleChains = persistentRuleChainData.getData(); if (!CollectionUtils.isEmpty(ruleChains)) { accumulator.addAll(ruleChains); } if (persistentRuleChainData.hasNext()) { - return findAllTenantRuleChainsRecursive(tenantId, accumulator, pageLink.nextPageLink()); + return findAllTenantRuleChainsRecursive(tenantId, accumulator, type, pageLink.nextPageLink()); } return accumulator; } @@ -544,6 +559,115 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } } + @Override + public RuleChain assignRuleChainToEdge(TenantId tenantId, RuleChainId ruleChainId, EdgeId edgeId) { + RuleChain ruleChain = findRuleChainById(tenantId, ruleChainId); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null) { + throw new DataValidationException("Can't assign ruleChain to non-existent edge!"); + } + if (!edge.getTenantId().equals(ruleChain.getTenantId())) { + throw new DataValidationException("Can't assign ruleChain to edge from different tenant!"); + } + try { + createRelation(tenantId, new EntityRelation(edgeId, ruleChainId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to create ruleChain relation. Edge Id: [{}]", ruleChainId, edgeId); + throw new RuntimeException(e); + } + return ruleChain; + } + + @Override + public RuleChain unassignRuleChainFromEdge(TenantId tenantId, RuleChainId ruleChainId, EdgeId edgeId, boolean remove) { + RuleChain ruleChain = findRuleChainById(tenantId, ruleChainId); + Edge edge = edgeService.findEdgeById(tenantId, edgeId); + if (edge == null) { + throw new DataValidationException("Can't unassign rule chain from non-existent edge!"); + } + if (!remove && edge.getRootRuleChainId() != null && edge.getRootRuleChainId().equals(ruleChainId)) { + throw new DataValidationException("Can't unassign root rule chain from edge [" + edge.getName() + "]. Please assign another root rule chain first!"); + } + try { + deleteRelation(tenantId, new EntityRelation(edgeId, ruleChainId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); + } catch (Exception e) { + log.warn("[{}] Failed to delete rule chain relation. Edge Id: [{}]", ruleChainId, edgeId); + throw new RuntimeException(e); + } + return ruleChain; + } + + @Override + public PageData findRuleChainsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink) { + log.trace("Executing findRuleChainsByTenantIdAndEdgeId, tenantId [{}], edgeId [{}], pageLink [{}]", tenantId, edgeId, pageLink); + Validator.validateId(tenantId, "Incorrect tenantId " + tenantId); + Validator.validateId(edgeId, "Incorrect edgeId " + edgeId); + Validator.validatePageLink(pageLink); + return ruleChainDao.findRuleChainsByTenantIdAndEdgeId(tenantId.getId(), edgeId.getId(), pageLink); + } + + @Override + public RuleChain getEdgeTemplateRootRuleChain(TenantId tenantId) { + return getRootRuleChainByType(tenantId, RuleChainType.EDGE); + } + + @Override + public boolean setEdgeTemplateRootRuleChain(TenantId tenantId, RuleChainId ruleChainId) { + RuleChain ruleChain = ruleChainDao.findById(tenantId, ruleChainId.getId()); + RuleChain previousEdgeTemplateRootRuleChain = getEdgeTemplateRootRuleChain(ruleChain.getTenantId()); + if (previousEdgeTemplateRootRuleChain == null || !previousEdgeTemplateRootRuleChain.getId().equals(ruleChain.getId())) { + try { + if (previousEdgeTemplateRootRuleChain != null) { + deleteRelation(tenantId, new EntityRelation(previousEdgeTemplateRootRuleChain.getTenantId(), previousEdgeTemplateRootRuleChain.getId(), + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); + previousEdgeTemplateRootRuleChain.setRoot(false); + ruleChainDao.save(tenantId, previousEdgeTemplateRootRuleChain); + } + createRelation(tenantId, new EntityRelation(ruleChain.getTenantId(), ruleChain.getId(), + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); + ruleChain.setRoot(true); + ruleChainDao.save(tenantId, ruleChain); + return true; + } catch (Exception e) { + log.warn("Failed to set edge template root rule chain, ruleChainId: [{}]", ruleChainId, e); + throw new RuntimeException(e); + } + } + return false; + } + + @Override + public boolean setAutoAssignToEdgeRuleChain(TenantId tenantId, RuleChainId ruleChainId) { + try { + createRelation(tenantId, new EntityRelation(tenantId, ruleChainId, + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE_AUTO_ASSIGN_RULE_CHAIN)); + return true; + } catch (Exception e) { + log.warn("Failed to set auto assign to edge rule chain, ruleChainId: [{}]", ruleChainId, e); + throw new RuntimeException(e); + } + } + + @Override + public boolean unsetAutoAssignToEdgeRuleChain(TenantId tenantId, RuleChainId ruleChainId) { + try { + deleteRelation(tenantId, new EntityRelation(tenantId, ruleChainId, + EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE_AUTO_ASSIGN_RULE_CHAIN)); + return true; + } catch (Exception e) { + log.warn("Failed to unset auto assign to edge rule chain, ruleChainId: [{}]", ruleChainId, e); + throw new RuntimeException(e); + } + } + + @Override + public ListenableFuture> findAutoAssignToEdgeRuleChainsByTenantId(TenantId tenantId) { + log.trace("Executing findAutoAssignToEdgeRuleChainsByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return ruleChainDao.findAutoAssignToEdgeRuleChainsByTenantId(tenantId.getId()); + } + + private void checkRuleNodesAndDelete(TenantId tenantId, RuleChainId ruleChainId) { try{ ruleChainDao.removeById(tenantId, ruleChainId.getId()); @@ -575,16 +699,6 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC ruleNodeDao.removeById(tenantId, entityId.getId()); } - private void createRelation(TenantId tenantId, EntityRelation relation) throws ExecutionException, InterruptedException { - log.debug("Creating relation: {}", relation); - relationService.saveRelation(tenantId, relation); - } - - private void deleteRelation(TenantId tenantId, EntityRelation relation) throws ExecutionException, InterruptedException { - log.debug("Deleting relation: {}", relation); - relationService.deleteRelation(tenantId, relation); - } - private DataValidator ruleChainValidator = new DataValidator() { @Override @@ -598,7 +712,10 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override protected void validateDataImpl(TenantId tenantId, RuleChain ruleChain) { if (StringUtils.isEmpty(ruleChain.getName())) { - throw new DataValidationException("Rule chain name should be specified!."); + throw new DataValidationException("Rule chain name should be specified!"); + } + if (ruleChain.getType() == null) { + ruleChain.setType(RuleChainType.CORE); } if (ruleChain.getTenantId() == null || ruleChain.getTenantId().isNullUid()) { throw new DataValidationException("Rule chain should be assigned to tenant!"); @@ -607,12 +724,18 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (tenant == null) { throw new DataValidationException("Rule chain is referencing to non-existent tenant!"); } - if (ruleChain.isRoot()) { + if (ruleChain.isRoot() && RuleChainType.CORE.equals(ruleChain.getType())) { RuleChain rootRuleChain = getRootTenantRuleChain(ruleChain.getTenantId()); if (rootRuleChain != null && !rootRuleChain.getId().equals(ruleChain.getId())) { throw new DataValidationException("Another root rule chain is present in scope of current tenant!"); } } + if (ruleChain.isRoot() && RuleChainType.EDGE.equals(ruleChain.getType())) { + RuleChain edgeTemplateRootRuleChain = getEdgeTemplateRootRuleChain(ruleChain.getTenantId()); + if (edgeTemplateRootRuleChain != null && !edgeTemplateRootRuleChain.getId().equals(ruleChain.getId())) { + throw new DataValidationException("Another edge template root rule chain is present in scope of current tenant!"); + } + } } }; diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java index 30c06f95de..4baa3bd576 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java @@ -15,12 +15,15 @@ */ package org.thingsboard.server.dao.rule; +import com.google.common.util.concurrent.ListenableFuture; 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.RuleChainType; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.TenantEntityDao; +import java.util.List; import java.util.UUID; /** @@ -36,4 +39,32 @@ public interface RuleChainDao extends Dao, TenantEntityDao { * @return the list of rule chain objects */ PageData findRuleChainsByTenantId(UUID tenantId, PageLink pageLink); + + /** + * Find rule chains by tenantId, type and page link. + * + * @param tenantId the tenantId + * @param type the type + * @param pageLink the page link + * @return the list of rule chain objects + */ + PageData findRuleChainsByTenantIdAndType(UUID tenantId, RuleChainType type, PageLink pageLink); + + /** + * Find rule chains by tenantId, edgeId and page link. + * + * @param tenantId the tenantId + * @param edgeId the edgeId + * @param pageLink the page link + * @return the list of rule chain objects + */ + PageData findRuleChainsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink); + + /** + * Find auto assign to edge rule chains by tenantId. + * + * @param tenantId the tenantId + * @return the list of rule chain objects + */ + ListenableFuture> findAutoAssignToEdgeRuleChainsByTenantId(UUID tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java index e16aebbfea..b1dcf5b47a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.service; -import com.google.common.io.Resources; import lombok.extern.slf4j.Slf4j; import org.owasp.validator.html.AntiSamy; import org.owasp.validator.html.Policy; @@ -25,6 +24,7 @@ import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; +import java.util.Optional; @Slf4j public class NoXssValidator implements ConstraintValidator { @@ -34,17 +34,21 @@ public class NoXssValidator implements ConstraintValidator { @Override public void initialize(NoXss constraintAnnotation) { if (xssPolicy == null) { - try { - xssPolicy = Policy.getInstance(Resources.getResource("xss-policy.xml")); - } catch (Exception e) { - log.error("Failed to set xss policy: {}", e.getMessage()); - } + xssPolicy = Optional.ofNullable(getClass().getClassLoader().getResourceAsStream("xss-policy.xml")) + .map(inputStream -> { + try { + return Policy.getInstance(inputStream); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .orElseThrow(() -> new IllegalStateException("XSS policy file not found")); } } @Override public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { - if (!(value instanceof String) || ((String) value).isEmpty() || xssPolicy == null) { + if (!(value instanceof String) || ((String) value).isEmpty()) { return true; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index b422f1cd4a..446125e4fb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -50,23 +50,41 @@ public interface AlarmRepository extends CrudRepository { "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + - "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%'))" + - "OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%'))" + - "OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%')))", - countQuery = "SELECT count(a) FROM AlarmEntity a " + - "LEFT JOIN RelationEntity re ON a.id = re.toId " + + "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%'))) " + , + countQuery = "" + + "SELECT count(a) + " + //alarms with relations only + " (SELECT count(a) FROM AlarmEntity a " + //alarms WITHOUT any relations + " LEFT JOIN RelationEntity re ON a.id = re.toId " + + " AND re.relationTypeGroup = 'ALARM' " + + " AND re.toType = 'ALARM' " + + " AND re.fromId = :affectedEntityId " + + " AND re.fromType = :affectedEntityType " + + " WHERE a.tenantId = :tenantId " + + " AND (a.originatorId = :affectedEntityId) " + + " AND (re.fromId IS NULL) " + //anti join + " AND (:startTime IS NULL OR a.createdTime >= :startTime) " + + " AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + " AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + " AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%'))) " + + " )" + + "FROM AlarmEntity a " + + "INNER JOIN RelationEntity re ON a.id = re.toId " + "AND re.relationTypeGroup = 'ALARM' " + "AND re.toType = 'ALARM' " + "AND re.fromId = :affectedEntityId " + "AND re.fromType = :affectedEntityType " + "WHERE a.tenantId = :tenantId " + - "AND (a.originatorId = :affectedEntityId or re.fromId IS NOT NULL) " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + - "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%'))" + - "OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%'))" + - "OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%')))") + "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%')) " + + " OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%'))) ") Page findAlarms(@Param("tenantId") UUID tenantId, @Param("affectedEntityId") UUID affectedEntityId, @Param("affectedEntityType") String affectedEntityType, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java index e49f09aaaa..89d1006018 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java @@ -22,6 +22,7 @@ import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.AssetEntity; import org.thingsboard.server.dao.model.sql.AssetInfoEntity; +import org.thingsboard.server.dao.model.sql.RuleChainEntity; import java.util.List; import java.util.UUID; @@ -122,5 +123,25 @@ public interface AssetRepository extends PagingAndSortingRepository findTenantAssetTypes(@Param("tenantId") UUID tenantId); + @Query("SELECT a FROM AssetEntity a, RelationEntity re WHERE a.tenantId = :tenantId " + + "AND a.id = re.toId AND re.toType = 'ASSET' AND re.relationTypeGroup = 'EDGE' " + + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + + "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT a FROM AssetEntity a, RelationEntity re WHERE a.tenantId = :tenantId " + + "AND a.id = re.toId AND re.toType = 'ASSET' AND re.relationTypeGroup = 'EDGE' " + + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + + "AND a.type = :type " + + "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("type") String type, + @Param("searchText") String searchText, + Pageable pageable); + Long countByTenantIdAndTypeIsNot(UUID tenantId, String type); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index cdd5a4c66b..2f8dbe5b02 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -15,7 +15,10 @@ */ package org.thingsboard.server.dao.sql.asset; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; @@ -23,13 +26,18 @@ import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; +import org.thingsboard.server.common.data.id.EdgeId; 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.page.TimePageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.model.sql.AssetEntity; import org.thingsboard.server.dao.model.sql.AssetInfoEntity; +import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; import java.util.ArrayList; @@ -45,6 +53,7 @@ import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE * Created by Valerii Sosliuk on 5/19/2017. */ @Component +@Slf4j public class JpaAssetDao extends JpaAbstractSearchTextDao implements AssetDao { @Autowired @@ -179,6 +188,29 @@ public class JpaAssetDao extends JpaAbstractSearchTextDao im return list; } + @Override + public PageData findAssetsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink) { + log.debug("Try to find assets by tenantId [{}], edgeId [{}] and pageLink [{}]", tenantId, edgeId, pageLink); + return DaoUtil.toPageData(assetRepository + .findByTenantIdAndEdgeId( + tenantId, + edgeId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findAssetsByTenantIdAndEdgeIdAndType(UUID tenantId, UUID edgeId, String type, PageLink pageLink) { + log.debug("Try to find assets by tenantId [{}], edgeId [{}], type [{}] and pageLink [{}]", tenantId, edgeId, type, pageLink); + return DaoUtil.toPageData(assetRepository + .findByTenantIdAndEdgeIdAndType( + tenantId, + edgeId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + @Override public Long countByTenantId(TenantId tenantId) { return assetRepository.countByTenantIdAndTypeIsNot(tenantId.getId(), TB_SERVICE_QUEUE); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java index c6c545b574..f4eb71eaed 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java @@ -51,11 +51,11 @@ public abstract class AbstractComponentDescriptorInsertRepository implements Com TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW); try { componentDescriptorEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict); + transactionManager.commit(transaction); } catch (Throwable th) { log.trace("Could not execute the update statement for Component Descriptor with id {}, name {} and entityType {}", entity.getUuid(), entity.getName(), entity.getType()); transactionManager.rollback(transaction); } - transactionManager.commit(transaction); } else { log.trace("Could not execute the insert statement for Component Descriptor with id {}, name {} and entityType {}", entity.getUuid(), entity.getName(), entity.getType()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java index 803c805b63..6bbf3a1d61 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java @@ -44,4 +44,13 @@ public interface DashboardInfoRepository extends PagingAndSortingRepository findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("searchText") String searchText, + Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java index 6d026327dd..4c34f6148a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java @@ -39,10 +39,10 @@ import java.util.UUID; public class JpaDashboardInfoDao extends JpaAbstractSearchTextDao implements DashboardInfoDao { @Autowired - private RelationDao relationDao; + private DashboardInfoRepository dashboardInfoRepository; @Autowired - private DashboardInfoRepository dashboardInfoRepository; + private RelationDao relationDao; @Override protected Class getEntityClass() { @@ -72,4 +72,15 @@ public class JpaDashboardInfoDao extends JpaAbstractSearchTextDao findDashboardsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink) { + log.debug("Try to find dashboards by tenantId [{}], edgeId [{}] and pageLink [{}]", tenantId, edgeId, pageLink); + return DaoUtil.toPageData(dashboardInfoRepository + .findByTenantIdAndEdgeId( + tenantId, + edgeId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java index c39e481638..0de59ac8be 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java @@ -169,5 +169,36 @@ public interface DeviceRepository extends PagingAndSortingRepository findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT d FROM DeviceEntity d, RelationEntity re WHERE d.tenantId = :tenantId " + + "AND d.id = re.toId AND re.toType = 'DEVICE' AND re.relationTypeGroup = 'EDGE' " + + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + + "AND d.type = :type " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("type") String type, + @Param("searchText") String searchText, + Pageable pageable); + + /** + * Count devices by tenantId. + * Custom query applied because default QueryDSL produces slow count(id). + *

+ * There is two way to count devices. + * OPTIMAL: count(*) + * - returns _row_count_ and use index-only scan (super fast). + * SLOW: count(id) + * - returns _NON_NULL_id_count and performs table scan to verify isNull for each id in filtered rows. + * */ + @Query("SELECT count(*) FROM DeviceEntity d WHERE d.tenantId = :tenantId") + Long countByTenantId(@Param("tenantId") UUID tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java index 18979eba91..96313f7a45 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.sql.device; import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; @@ -27,6 +28,7 @@ import org.thingsboard.server.common.data.EntityType; 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.page.TimePageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.device.DeviceDao; import org.thingsboard.server.dao.model.sql.DeviceEntity; @@ -44,6 +46,7 @@ import java.util.UUID; * Created by Valerii Sosliuk on 5/6/2017. */ @Component +@Slf4j public class JpaDeviceDao extends JpaAbstractSearchTextDao implements DeviceDao { @Autowired @@ -234,4 +237,27 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao } return list; } + + @Override + public PageData findDevicesByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink) { + log.debug("Try to find devices by tenantId [{}], edgeId [{}] and pageLink [{}]", tenantId, edgeId, pageLink); + return DaoUtil.toPageData(deviceRepository + .findByTenantIdAndEdgeId( + tenantId, + edgeId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findDevicesByTenantIdAndEdgeIdAndType(UUID tenantId, UUID edgeId, String type, PageLink pageLink) { + log.debug("Try to find devices by tenantId [{}], edgeId [{}], type [{}] and pageLink [{}]", tenantId, edgeId, type, pageLink); + return DaoUtil.toPageData(deviceRepository + .findByTenantIdAndEdgeIdAndType( + tenantId, + edgeId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java new file mode 100644 index 0000000000..4962b2e8d2 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2021 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.edge; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.dao.model.sql.EdgeEventEntity; + +import java.util.UUID; + +public interface EdgeEventRepository extends PagingAndSortingRepository, JpaSpecificationExecutor { + + @Query("SELECT e FROM EdgeEventEntity e WHERE " + + "e.tenantId = :tenantId " + + "AND e.edgeId = :edgeId " + + "AND (:startTime IS NULL OR e.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR e.createdTime <= :endTime) " + ) + Page findEdgeEventsByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + Pageable pageable); + + @Query("SELECT e FROM EdgeEventEntity e WHERE " + + "e.tenantId = :tenantId " + + "AND e.edgeId = :edgeId " + + "AND (:startTime IS NULL OR e.createdTime >= :startTime) " + + "AND (:endTime IS NULL OR e.createdTime <= :endTime) " + + "AND e.edgeEventAction <> 'TIMESERIES_UPDATED'" + ) + Page findEdgeEventsByTenantIdAndEdgeIdWithoutTimeseriesUpdated(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + Pageable pageable); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java new file mode 100644 index 0000000000..a5216033b3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java @@ -0,0 +1,123 @@ +/** + * Copyright © 2016-2021 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.edge; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.dao.model.sql.EdgeEntity; +import org.thingsboard.server.dao.model.sql.EdgeInfoEntity; + +import java.util.List; +import java.util.UUID; + +public interface EdgeRepository extends PagingAndSortingRepository { + + @Query("SELECT d FROM EdgeEntity d WHERE d.tenantId = :tenantId " + + "AND d.customerId = :customerId " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.EdgeInfoEntity(d, c.title, c.additionalInfo) " + + "FROM EdgeEntity d " + + "LEFT JOIN CustomerEntity c on c.id = d.customerId " + + "WHERE d.id = :edgeId") + EdgeInfoEntity findEdgeInfoById(@Param("edgeId") UUID edgeId); + + @Query("SELECT d FROM EdgeEntity d WHERE d.tenantId = :tenantId " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantId(@Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.EdgeInfoEntity(d, c.title, c.additionalInfo) " + + "FROM EdgeEntity d " + + "LEFT JOIN CustomerEntity c on c.id = d.customerId " + + "WHERE d.tenantId = :tenantId " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findEdgeInfosByTenantId(@Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT d FROM EdgeEntity d WHERE d.tenantId = :tenantId " + + "AND d.type = :type " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantIdAndType(@Param("tenantId") UUID tenantId, + @Param("type") String type, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.EdgeInfoEntity(d, c.title, c.additionalInfo) " + + "FROM EdgeEntity d " + + "LEFT JOIN CustomerEntity c on c.id = d.customerId " + + "WHERE d.tenantId = :tenantId " + + "AND d.type = :type " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findEdgeInfosByTenantIdAndType(@Param("tenantId") UUID tenantId, + @Param("type") String type, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT d FROM EdgeEntity d WHERE d.tenantId = :tenantId " + + "AND d.customerId = :customerId " + + "AND d.type = :type " + + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("type") String type, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.EdgeInfoEntity(a, c.title, c.additionalInfo) " + + "FROM EdgeEntity a " + + "LEFT JOIN CustomerEntity c on c.id = a.customerId " + + "WHERE a.tenantId = :tenantId " + + "AND a.customerId = :customerId " + + "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findEdgeInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.EdgeInfoEntity(a, c.title, c.additionalInfo) " + + "FROM EdgeEntity a " + + "LEFT JOIN CustomerEntity c on c.id = a.customerId " + + "WHERE a.tenantId = :tenantId " + + "AND a.customerId = :customerId " + + "AND a.type = :type " + + "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findEdgeInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("type") String type, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT DISTINCT d.type FROM EdgeEntity d WHERE d.tenantId = :tenantId") + List findTenantEdgeTypes(@Param("tenantId") UUID tenantId); + + EdgeEntity findByTenantIdAndName(UUID tenantId, String name); + + List findEdgesByTenantIdAndCustomerIdAndIdIn(UUID tenantId, UUID customerId, List edgeIds); + + List findEdgesByTenantIdAndIdIn(UUID tenantId, List edgeIds); + + EdgeEntity findByRoutingKey(String routingKey); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java new file mode 100644 index 0000000000..9dc5c7a9a2 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java @@ -0,0 +1,119 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.id.EdgeEventId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EventId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.edge.EdgeEventDao; +import org.thingsboard.server.dao.model.sql.EdgeEventEntity; +import org.thingsboard.server.dao.model.sql.EventEntity; +import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; + +import java.util.Optional; +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; + +@Slf4j +@Component +public class JpaBaseEdgeEventDao extends JpaAbstractSearchTextDao implements EdgeEventDao { + + private final UUID systemTenantId = NULL_UUID; + + @Autowired + private EdgeEventRepository edgeEventRepository; + + @Override + protected Class getEntityClass() { + return EdgeEventEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return edgeEventRepository; + } + + @Override + public ListenableFuture saveAsync(EdgeEvent edgeEvent) { + log.debug("Save edge event [{}] ", edgeEvent); + if (edgeEvent.getId() == null) { + UUID timeBased = Uuids.timeBased(); + edgeEvent.setId(new EdgeEventId(timeBased)); + edgeEvent.setCreatedTime(Uuids.unixTimestamp(timeBased)); + } else if (edgeEvent.getCreatedTime() == 0L) { + UUID eventId = edgeEvent.getId().getId(); + if (eventId.version() == 1) { + edgeEvent.setCreatedTime(Uuids.unixTimestamp(eventId)); + } else { + edgeEvent.setCreatedTime(System.currentTimeMillis()); + } + } + if (StringUtils.isEmpty(edgeEvent.getUid())) { + edgeEvent.setUid(edgeEvent.getId().toString()); + } + return service.submit(() -> save(new EdgeEventEntity(edgeEvent)).orElse(null)); + } + + @Override + public PageData findEdgeEvents(UUID tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate) { + if (withTsUpdate) { + return DaoUtil.toPageData( + edgeEventRepository + .findEdgeEventsByTenantIdAndEdgeId( + tenantId, + edgeId.getId(), + pageLink.getStartTime(), + pageLink.getEndTime(), + DaoUtil.toPageable(pageLink))); + } else { + return DaoUtil.toPageData( + edgeEventRepository + .findEdgeEventsByTenantIdAndEdgeIdWithoutTimeseriesUpdated( + tenantId, + edgeId.getId(), + pageLink.getStartTime(), + pageLink.getEndTime(), + DaoUtil.toPageable(pageLink))); + + } + } + + public Optional save(EdgeEventEntity entity) { + log.debug("Save edge event [{}] ", entity); + if (entity.getTenantId() == null) { + log.trace("Save system edge event with predefined id {}", systemTenantId); + entity.setTenantId(systemTenantId); + } + if (entity.getUuid() == null) { + entity.setUuid(Uuids.timeBased()); + } + return Optional.of(DaoUtil.getData(edgeEventRepository.save(entity))); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java new file mode 100644 index 0000000000..f17196fe93 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java @@ -0,0 +1,218 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeInfo; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.edge.EdgeDao; +import org.thingsboard.server.dao.model.sql.EdgeEntity; +import org.thingsboard.server.dao.model.sql.EdgeInfoEntity; +import org.thingsboard.server.dao.relation.RelationDao; +import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +@Component +@Slf4j +public class JpaEdgeDao extends JpaAbstractSearchTextDao implements EdgeDao { + + @Autowired + private EdgeRepository edgeRepository; + + @Autowired + private RelationDao relationDao; + + @Override + protected Class getEntityClass() { + return EdgeEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return edgeRepository; + } + + @Override + public EdgeInfo findEdgeInfoById(TenantId tenantId, UUID edgeId) { + return DaoUtil.getData(edgeRepository.findEdgeInfoById(edgeId)); + } + + @Override + public PageData findEdgesByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData( + edgeRepository.findByTenantId( + tenantId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public ListenableFuture> findEdgesByTenantIdAndIdsAsync(UUID tenantId, List edgeIds) { + return service.submit(() -> DaoUtil.convertDataList(edgeRepository.findEdgesByTenantIdAndIdIn(tenantId, edgeIds))); + } + + @Override + public PageData findEdgesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink) { + return DaoUtil.toPageData( + edgeRepository.findByTenantIdAndCustomerId( + tenantId, + customerId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public ListenableFuture> findEdgesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List edgeIds) { + return service.submit(() -> DaoUtil.convertDataList( + edgeRepository.findEdgesByTenantIdAndCustomerIdAndIdIn(tenantId, customerId, edgeIds))); + } + + @Override + public Optional findEdgeByTenantIdAndName(UUID tenantId, String name) { + Edge edge = DaoUtil.getData(edgeRepository.findByTenantIdAndName(tenantId, name)); + return Optional.ofNullable(edge); + } + + @Override + public PageData findEdgesByTenantIdAndType(UUID tenantId, String type, PageLink pageLink) { + return DaoUtil.toPageData( + edgeRepository.findByTenantIdAndType( + tenantId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findEdgesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) { + return DaoUtil.toPageData( + edgeRepository.findByTenantIdAndCustomerIdAndType( + tenantId, + customerId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findEdgeInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink) { + return DaoUtil.toPageData( + edgeRepository.findEdgeInfosByTenantIdAndCustomerId( + tenantId, + customerId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, EdgeInfoEntity.edgeInfoColumnMap))); + } + + @Override + public PageData findEdgeInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) { + return DaoUtil.toPageData( + edgeRepository.findEdgeInfosByTenantIdAndCustomerIdAndType( + tenantId, + customerId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, EdgeInfoEntity.edgeInfoColumnMap))); + } + + @Override + public ListenableFuture> findTenantEdgeTypesAsync(UUID tenantId) { + return service.submit(() -> convertTenantEdgeTypesToDto(tenantId, edgeRepository.findTenantEdgeTypes(tenantId))); + } + + @Override + public PageData findEdgeInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink) { + return DaoUtil.toPageData( + edgeRepository.findEdgeInfosByTenantIdAndType( + tenantId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, EdgeInfoEntity.edgeInfoColumnMap))); + } + + @Override + public PageData findEdgeInfosByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData( + edgeRepository.findEdgeInfosByTenantId( + tenantId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, EdgeInfoEntity.edgeInfoColumnMap))); + } + + @Override + public Optional findByRoutingKey(UUID tenantId, String routingKey) { + Edge edge = DaoUtil.getData(edgeRepository.findByRoutingKey(routingKey)); + return Optional.ofNullable(edge); + } + + @Override + public ListenableFuture> findEdgesByTenantIdAndRuleChainId(UUID tenantId, UUID ruleChainId) { + log.debug("Try to find edges by tenantId [{}], ruleChainId [{}]", tenantId, ruleChainId); + ListenableFuture> relations = relationDao.findAllByToAndType(new TenantId(tenantId), new RuleChainId(ruleChainId), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE); + return transformFromRelationToEdge(tenantId, relations); + } + + @Override + public ListenableFuture> findEdgesByTenantIdAndDashboardId(UUID tenantId, UUID dashboardId) { + log.debug("Try to find edges by tenantId [{}], dashboardId [{}]", tenantId, dashboardId); + ListenableFuture> relations = relationDao.findAllByToAndType(new TenantId(tenantId), new DashboardId(dashboardId), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE); + return transformFromRelationToEdge(tenantId, relations); + } + + private ListenableFuture> transformFromRelationToEdge(UUID tenantId, ListenableFuture> relations) { + return Futures.transformAsync(relations, input -> { + List> edgeFutures = new ArrayList<>(input.size()); + for (EntityRelation relation : input) { + edgeFutures.add(findByIdAsync(new TenantId(tenantId), relation.getFrom().getId())); + } + return Futures.successfulAsList(edgeFutures); + }, MoreExecutors.directExecutor()); + } + + private List convertTenantEdgeTypesToDto(UUID tenantId, List types) { + List list = Collections.emptyList(); + if (types != null && !types.isEmpty()) { + list = new ArrayList<>(); + for (String type : types) { + list.add(new EntitySubtype(new TenantId(tenantId), EntityType.EDGE, type)); + } + } + return list; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java index abdd2c2e19..9e33b6e701 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.dao.model.sql.DeviceEntity; import org.thingsboard.server.dao.model.sql.EntityViewEntity; import org.thingsboard.server.dao.model.sql.EntityViewInfoEntity; @@ -119,4 +120,24 @@ public interface EntityViewRepository extends PagingAndSortingRepository findTenantEntityViewTypes(@Param("tenantId") UUID tenantId); + + @Query("SELECT ev FROM EntityViewEntity ev, RelationEntity re WHERE ev.tenantId = :tenantId " + + "AND ev.id = re.toId AND re.toType = 'ENTITY_VIEW' AND re.relationTypeGroup = 'EDGE' " + + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + + "AND LOWER(ev.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT ev FROM EntityViewEntity ev, RelationEntity re WHERE ev.tenantId = :tenantId " + + "AND ev.id = re.toId AND re.toType = 'ENTITY_VIEW' AND re.relationTypeGroup = 'EDGE' " + + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + + "AND ev.type = :type " + + "AND LOWER(ev.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("type") String type, + @Param("searchText") String searchText, + Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java index 05a61307ec..35f81a860c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.sql.entityview; import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; @@ -43,6 +44,7 @@ import java.util.UUID; * Created by Victor Basanets on 8/31/2017. */ @Component +@Slf4j public class JpaEntityViewDao extends JpaAbstractSearchTextDao implements EntityViewDao { @@ -175,4 +177,27 @@ public class JpaEntityViewDao extends JpaAbstractSearchTextDao findEntityViewsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink) { + log.debug("Try to find entity views by tenantId [{}], edgeId [{}] and pageLink [{}]", tenantId, edgeId, pageLink); + return DaoUtil.toPageData(entityViewRepository + .findByTenantIdAndEdgeId( + tenantId, + edgeId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findEntityViewsByTenantIdAndEdgeIdAndType(UUID tenantId, UUID edgeId, String type, PageLink pageLink) { + log.debug("Try to find entity views by tenantId [{}], edgeId [{}], type [{}] and pageLink [{}]", tenantId, edgeId, type, pageLink); + return DaoUtil.toPageData(entityViewRepository + .findByTenantIdAndEdgeIdAndType( + tenantId, + edgeId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java index fde9498702..93a33d1f9f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java @@ -51,11 +51,11 @@ public abstract class AbstractEventInsertRepository implements EventInsertReposi TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW); try { eventEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict); + transactionManager.commit(transaction); } catch (Throwable th) { log.trace("Could not execute the update statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType()); transactionManager.rollback(transaction); } - transactionManager.commit(transaction); } else { log.trace("Could not execute the insert statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java index c68d6f1a7e..270ab26329 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java @@ -44,11 +44,11 @@ public interface EventRepository extends PagingAndSortingRepository findLatestByTenantIdAndEntityTypeAndEntityIdAndEventType( - @Param("tenantId") UUID tenantId, - @Param("entityType") EntityType entityType, - @Param("entityId") UUID entityId, - @Param("eventType") String eventType, - Pageable pageable); + @Param("tenantId") UUID tenantId, + @Param("entityType") EntityType entityType, + @Param("entityId") UUID entityId, + @Param("eventType") String eventType, + Pageable pageable); @Query("SELECT e FROM EventEntity e WHERE " + "e.tenantId = :tenantId " + @@ -80,4 +80,165 @@ public interface EventRepository extends PagingAndSortingRepository= :startTime AND (:endTime = 0 OR e.created_time <= :endTime) " + + ") AS e WHERE " + + "(:type IS NULL OR lower(json_body->>'type') LIKE concat('%', lower(:type\\:\\:varchar), '%')) " + + "AND (:server IS NULL OR lower(json_body->>'server') LIKE concat('%', lower(:server\\:\\:varchar), '%')) " + + "AND (:entityName IS NULL OR lower(json_body->>'entityName') LIKE concat('%', lower(:entityName\\:\\:varchar), '%')) " + + "AND (:relationType IS NULL OR lower(json_body->>'relationType') LIKE concat('%', lower(:relationType\\:\\:varchar), '%')) " + + "AND (:bodyEntityId IS NULL OR lower(json_body->>'entityId') LIKE concat('%', lower(:bodyEntityId\\:\\:varchar), '%')) " + + "AND (:msgType IS NULL OR lower(json_body->>'msgType') LIKE concat('%', lower(:msgType\\:\\:varchar), '%')) " + + "AND ((:isError = FALSE) OR (json_body->>'error') IS NOT NULL) " + + "AND (:error IS NULL OR lower(json_body->>'error') LIKE concat('%', lower(:error\\:\\:varchar), '%')) " + + "AND (:data IS NULL OR lower(json_body->>'data') LIKE concat('%', lower(:data\\:\\:varchar), '%')) " + + "AND (:metadata IS NULL OR lower(json_body->>'metadata') LIKE concat('%', lower(:metadata\\:\\:varchar), '%')) ", + countQuery = "SELECT count(*) FROM " + + "(SELECT *, e.body\\:\\:jsonb as json_body FROM event e WHERE " + + "e.tenant_id = :tenantId " + + "AND e.entity_type = :entityType " + + "AND e.entity_id = :entityId " + + "AND e.event_type = :eventType " + + "AND e.created_time >= :startTime AND (:endTime = 0 OR e.created_time <= :endTime) " + + ") AS e WHERE " + + "(:type IS NULL OR lower(json_body->>'type') LIKE concat('%', lower(:type\\:\\:varchar), '%')) " + + "AND (:server IS NULL OR lower(json_body->>'server') LIKE concat('%', lower(:server\\:\\:varchar), '%')) " + + "AND (:entityName IS NULL OR lower(json_body->>'entityName') LIKE concat('%', lower(:entityName\\:\\:varchar), '%')) " + + "AND (:relationType IS NULL OR lower(json_body->>'relationType') LIKE concat('%', lower(:relationType\\:\\:varchar), '%')) " + + "AND (:bodyEntityId IS NULL OR lower(json_body->>'entityId') LIKE concat('%', lower(:bodyEntityId\\:\\:varchar), '%')) " + + "AND (:msgType IS NULL OR lower(json_body->>'msgType') LIKE concat('%', lower(:msgType\\:\\:varchar), '%')) " + + "AND ((:isError = FALSE) OR (json_body->>'error') IS NOT NULL) " + + "AND (:error IS NULL OR lower(json_body->>'error') LIKE concat('%', lower(:error\\:\\:varchar), '%')) " + + "AND (:data IS NULL OR lower(json_body->>'data') LIKE concat('%', lower(:data\\:\\:varchar), '%')) " + + "AND (:metadata IS NULL OR lower(json_body->>'metadata') LIKE concat('%', lower(:metadata\\:\\:varchar), '%'))" + ) + Page findDebugRuleNodeEvents(@Param("tenantId") UUID tenantId, + @Param("entityId") UUID entityId, + @Param("entityType") String entityType, + @Param("eventType") String eventType, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime, + @Param("type") String type, + @Param("server") String server, + @Param("entityName") String entityName, + @Param("relationType") String relationType, + @Param("bodyEntityId") String bodyEntityId, + @Param("msgType") String msgType, + @Param("isError") boolean isError, + @Param("error") String error, + @Param("data") String data, + @Param("metadata") String metadata, + Pageable pageable); + + @Query(nativeQuery = true, + value = "SELECT e.id, e.created_time, e.body, e.entity_id, e.entity_type, e.event_type, e.event_uid, e.tenant_id, ts FROM " + + "(SELECT *, e.body\\:\\:jsonb as json_body FROM event e WHERE " + + "e.tenant_id = :tenantId " + + "AND e.entity_type = :entityType " + + "AND e.entity_id = :entityId " + + "AND e.event_type = 'ERROR' " + + "AND e.created_time >= :startTime AND (:endTime = 0 OR e.created_time <= :endTime) " + + ") AS e WHERE " + + "(:server IS NULL OR lower(json_body->>'server') LIKE concat('%', lower(:server\\:\\:varchar), '%')) " + + "AND (:method IS NULL OR lower(json_body->>'method') LIKE concat('%', lower(:method\\:\\:varchar), '%')) " + + "AND (:error IS NULL OR lower(json_body->>'error') LIKE concat('%', lower(:error\\:\\:varchar), '%'))", + countQuery = "SELECT count(*) FROM " + + "(SELECT *, e.body\\:\\:jsonb as json_body FROM event e WHERE " + + "e.tenant_id = :tenantId " + + "AND e.entity_type = :entityType " + + "AND e.entity_id = :entityId " + + "AND e.event_type = 'ERROR' " + + "AND e.created_time >= :startTime AND (:endTime = 0 OR e.created_time <= :endTime) " + + ") AS e WHERE " + + "(:server IS NULL OR lower(json_body->>'server') LIKE concat('%', lower(:server\\:\\:varchar), '%')) " + + "AND (:method IS NULL OR lower(json_body->>'method') LIKE concat('%', lower(:method\\:\\:varchar), '%')) " + + "AND (:error IS NULL OR lower(json_body->>'error') LIKE concat('%', lower(:error\\:\\:varchar), '%'))") + Page findErrorEvents(@Param("tenantId") UUID tenantId, + @Param("entityId") UUID entityId, + @Param("entityType") String entityType, + @Param("startTime") Long startTime, + @Param("endTime") Long endTIme, + @Param("server") String server, + @Param("method") String method, + @Param("error") String error, + Pageable pageable); + + @Query(nativeQuery = true, + value = "SELECT e.id, e.created_time, e.body, e.entity_id, e.entity_type, e.event_type, e.event_uid, e.tenant_id, ts FROM " + + "(SELECT *, e.body\\:\\:jsonb as json_body FROM event e WHERE " + + "e.tenant_id = :tenantId " + + "AND e.entity_type = :entityType " + + "AND e.entity_id = :entityId " + + "AND e.event_type = 'LC_EVENT' " + + "AND e.created_time >= :startTime AND (:endTime = 0 OR e.created_time <= :endTime) " + + ") AS e WHERE " + + "(:server IS NULL OR lower(json_body->>'server') LIKE concat('%', lower(:server\\:\\:varchar), '%')) " + + "AND (:event IS NULL OR lower(json_body->>'event') LIKE concat('%', lower(:event\\:\\:varchar), '%')) " + + "AND ((:statusFilterEnabled = FALSE) OR lower(json_body->>'success')\\:\\:boolean = :statusFilter) " + + "AND (:error IS NULL OR lower(json_body->>'error') LIKE concat('%', lower(:error\\:\\:varchar), '%'))" + , + countQuery = "SELECT count(*) FROM " + + "(SELECT *, e.body\\:\\:jsonb as json_body FROM event e WHERE " + + "e.tenant_id = :tenantId " + + "AND e.entity_type = :entityType " + + "AND e.entity_id = :entityId " + + "AND e.event_type = 'LC_EVENT' " + + "AND e.created_time >= :startTime AND (:endTime = 0 OR e.created_time <= :endTime) " + + ") AS e WHERE " + + "(:server IS NULL OR lower(json_body->>'server') LIKE concat('%', lower(:server\\:\\:varchar), '%')) " + + "AND (:event IS NULL OR lower(json_body->>'event') LIKE concat('%', lower(:event\\:\\:varchar), '%')) " + + "AND ((:statusFilterEnabled = FALSE) OR lower(json_body->>'success')\\:\\:boolean = :statusFilter) " + + "AND (:error IS NULL OR lower(json_body->>'error') LIKE concat('%', lower(:error\\:\\:varchar), '%'))" + ) + Page findLifeCycleEvents(@Param("tenantId") UUID tenantId, + @Param("entityId") UUID entityId, + @Param("entityType") String entityType, + @Param("startTime") Long startTime, + @Param("endTime") Long endTIme, + @Param("server") String server, + @Param("event") String event, + @Param("statusFilterEnabled") boolean statusFilterEnabled, + @Param("statusFilter") boolean statusFilter, + @Param("error") String error, + Pageable pageable); + + @Query(nativeQuery = true, + value = "SELECT e.id, e.created_time, e.body, e.entity_id, e.entity_type, e.event_type, e.event_uid, e.tenant_id, ts FROM " + + "(SELECT *, e.body\\:\\:jsonb as json_body FROM event e WHERE " + + "e.tenant_id = :tenantId " + + "AND e.entity_type = :entityType " + + "AND e.entity_id = :entityId " + + "AND e.event_type = 'STATS' " + + "AND e.created_time >= :startTime AND (:endTime = 0 OR e.created_time <= :endTime) " + + ") AS e WHERE " + + "(:server IS NULL OR lower(e.body\\:\\:json->>'server') LIKE concat('%', lower(:server\\:\\:varchar), '%')) " + + "AND (:messagesProcessed = 0 OR (json_body->>'messagesProcessed')\\:\\:integer >= :messagesProcessed) " + + "AND (:errorsOccurred = 0 OR (json_body->>'errorsOccurred')\\:\\:integer >= :errorsOccurred) ", + countQuery = "SELECT count(*) FROM " + + "(SELECT *, e.body\\:\\:jsonb as json_body FROM event e WHERE " + + "e.tenant_id = :tenantId " + + "AND e.entity_type = :entityType " + + "AND e.entity_id = :entityId " + + "AND e.event_type = 'LC_EVENT' " + + "AND e.created_time >= :startTime AND (:endTime = 0 OR e.created_time <= :endTime) " + + ") AS e WHERE " + + "(:server IS NULL OR lower(e.body\\:\\:json->>'server') LIKE concat('%', lower(:server\\:\\:varchar), '%')) " + + "AND (:messagesProcessed = 0 OR (json_body->>'messagesProcessed')\\:\\:integer >= :messagesProcessed) " + + "AND (:errorsOccurred = 0 OR (json_body->>'errorsOccurred')\\:\\:integer >= :errorsOccurred) ") + Page findStatisticsEvents(@Param("tenantId") UUID tenantId, + @Param("entityId") UUID entityId, + @Param("entityType") String entityType, + @Param("startTime") Long startTime, + @Param("endTime") Long endTIme, + @Param("server") String server, + @Param("messagesProcessed") Integer messagesProcessed, + @Param("errorsOccurred") Integer errorsOccurred, + Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java index 06f54042d7..44848ec515 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java @@ -24,6 +24,12 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.event.DebugEvent; +import org.thingsboard.server.common.data.event.ErrorEventFilter; +import org.thingsboard.server.common.data.event.EventFilter; +import org.thingsboard.server.common.data.event.EventType; +import org.thingsboard.server.common.data.event.LifeCycleEventFilter; +import org.thingsboard.server.common.data.event.StatisticsEventFilter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EventId; import org.thingsboard.server.common.data.id.TenantId; @@ -147,6 +153,98 @@ public class JpaBaseEventDao extends JpaAbstractDao implemen DaoUtil.toPageable(pageLink))); } + @Override + public PageData findEventByFilter(UUID tenantId, EntityId entityId, EventFilter eventFilter, TimePageLink pageLink) { + if (eventFilter.hasFilterForJsonBody()) { + switch (eventFilter.getEventType()) { + case DEBUG_RULE_NODE: + case DEBUG_RULE_CHAIN: + return findEventByFilter(tenantId, entityId, (DebugEvent) eventFilter, pageLink); + case LC_EVENT: + return findEventByFilter(tenantId, entityId, (LifeCycleEventFilter) eventFilter, pageLink); + case ERROR: + return findEventByFilter(tenantId, entityId, (ErrorEventFilter) eventFilter, pageLink); + case STATS: + return findEventByFilter(tenantId, entityId, (StatisticsEventFilter) eventFilter, pageLink); + default: + throw new RuntimeException("Not supported event type: " + eventFilter.getEventType()); + } + } else { + return findEvents(tenantId, entityId, eventFilter.getEventType().name(), pageLink); + } + } + + private PageData findEventByFilter(UUID tenantId, EntityId entityId, DebugEvent eventFilter, TimePageLink pageLink) { + return DaoUtil.toPageData( + eventRepository.findDebugRuleNodeEvents( + tenantId, + entityId.getId(), + entityId.getEntityType().name(), + eventFilter.getEventType().name(), + notNull(pageLink.getStartTime()), + notNull(pageLink.getEndTime()), + eventFilter.getMsgDirectionType(), + eventFilter.getServer(), + eventFilter.getEntityName(), + eventFilter.getRelationType(), + eventFilter.getEntityId(), + eventFilter.getMsgType(), + eventFilter.isError(), + eventFilter.getError(), + eventFilter.getDataSearch(), + eventFilter.getMetadataSearch(), + DaoUtil.toPageable(pageLink))); + } + + private PageData findEventByFilter(UUID tenantId, EntityId entityId, ErrorEventFilter eventFilter, TimePageLink pageLink) { + return DaoUtil.toPageData( + eventRepository.findErrorEvents( + tenantId, + entityId.getId(), + entityId.getEntityType().name(), + notNull(pageLink.getStartTime()), + notNull(pageLink.getEndTime()), + eventFilter.getServer(), + eventFilter.getMethod(), + eventFilter.getError(), + DaoUtil.toPageable(pageLink)) + ); + } + + private PageData findEventByFilter(UUID tenantId, EntityId entityId, LifeCycleEventFilter eventFilter, TimePageLink pageLink) { + boolean statusFilterEnabled = !StringUtils.isEmpty(eventFilter.getStatus()); + boolean statusFilter = statusFilterEnabled && eventFilter.getStatus().equalsIgnoreCase("Success"); + return DaoUtil.toPageData( + eventRepository.findLifeCycleEvents( + tenantId, + entityId.getId(), + entityId.getEntityType().name(), + notNull(pageLink.getStartTime()), + notNull(pageLink.getEndTime()), + eventFilter.getServer(), + eventFilter.getEvent(), + statusFilterEnabled, + statusFilter, + eventFilter.getError(), + DaoUtil.toPageable(pageLink)) + ); + } + + private PageData findEventByFilter(UUID tenantId, EntityId entityId, StatisticsEventFilter eventFilter, TimePageLink pageLink) { + return DaoUtil.toPageData( + eventRepository.findStatisticsEvents( + tenantId, + entityId.getId(), + entityId.getEntityType().name(), + notNull(pageLink.getStartTime()), + notNull(pageLink.getEndTime()), + eventFilter.getServer(), + notNull(eventFilter.getMessagesProcessed()), + notNull(eventFilter.getErrorsOccurred()), + DaoUtil.toPageable(pageLink)) + ); + } + @Override public List findLatestEvents(UUID tenantId, EntityId entityId, String eventType, int limit) { List latest = eventRepository.findLatestByTenantIdAndEntityTypeAndEntityIdAndEventType( @@ -177,4 +275,12 @@ public class JpaBaseEventDao extends JpaAbstractDao implemen return Optional.of(DaoUtil.getData(eventInsertRepository.saveOrUpdate(entity))); } + private long notNull(Long value) { + return value != null ? value : 0; + } + + private int notNull(Integer value) { + return value != null ? value : 0; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index 4907c03dea..ac4d2b9553 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -29,6 +29,8 @@ import org.thingsboard.server.common.data.query.AssetSearchQueryFilter; import org.thingsboard.server.common.data.query.AssetTypeFilter; import org.thingsboard.server.common.data.query.DeviceSearchQueryFilter; import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.common.data.query.EdgeSearchQueryFilter; +import org.thingsboard.server.common.data.query.EdgeTypeFilter; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataPageLink; @@ -112,6 +114,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { " THEN (select customer_id from device where id = entity_id)" + " WHEN entity.entity_type = 'ENTITY_VIEW'" + " THEN (select customer_id from entity_view where id = entity_id)" + + " WHEN entity.entity_type = 'EDGE'" + + " THEN (select customer_id from edge where id = entity_id)" + " END as customer_id"; private static final String SELECT_TENANT_ID = "SELECT CASE" + " WHEN entity.entity_type = 'TENANT' THEN entity_id" + @@ -127,6 +131,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { " THEN (select tenant_id from device where id = entity_id)" + " WHEN entity.entity_type = 'ENTITY_VIEW'" + " THEN (select tenant_id from entity_view where id = entity_id)" + + " WHEN entity.entity_type = 'EDGE'" + + " THEN (select tenant_id from edge where id = entity_id)" + " END as tenant_id"; private static final String SELECT_CREATED_TIME = " CASE" + " WHEN entity.entity_type = 'TENANT'" + @@ -143,6 +149,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { " THEN (select created_time from device where id = entity_id)" + " WHEN entity.entity_type = 'ENTITY_VIEW'" + " THEN (select created_time from entity_view where id = entity_id)" + + " WHEN entity.entity_type = 'EDGE'" + + " THEN (select created_time from edge where id = entity_id)" + " END as created_time"; private static final String SELECT_NAME = " CASE" + " WHEN entity.entity_type = 'TENANT'" + @@ -159,6 +167,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { " THEN (select name from device where id = entity_id)" + " WHEN entity.entity_type = 'ENTITY_VIEW'" + " THEN (select name from entity_view where id = entity_id)" + + " WHEN entity.entity_type = 'EDGE'" + + " THEN (select name from edge where id = entity_id)" + " END as name"; private static final String SELECT_TYPE = " CASE" + " WHEN entity.entity_type = 'USER'" + @@ -169,6 +179,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { " THEN (select type from device where id = entity_id)" + " WHEN entity.entity_type = 'ENTITY_VIEW'" + " THEN (select type from entity_view where id = entity_id)" + + " WHEN entity.entity_type = 'EDGE'" + + " THEN (select type from edge where id = entity_id)" + " ELSE entity.entity_type END as type"; private static final String SELECT_LABEL = " CASE" + " WHEN entity.entity_type = 'TENANT'" + @@ -185,6 +197,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { " THEN (select label from device where id = entity_id)" + " WHEN entity.entity_type = 'ENTITY_VIEW'" + " THEN (select name from entity_view where id = entity_id)" + + " WHEN entity.entity_type = 'EDGE'" + + " THEN (select label from edge where id = entity_id)" + " END as label"; private static final String SELECT_ADDITIONAL_INFO = " CASE" + " WHEN entity.entity_type = 'TENANT'" + @@ -201,6 +215,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { " THEN (select additional_info from device where id = entity_id)" + " WHEN entity.entity_type = 'ENTITY_VIEW'" + " THEN (select additional_info from entity_view where id = entity_id)" + + " WHEN entity.entity_type = 'EDGE'" + + " THEN (select additional_info from edge where id = entity_id)" + " END as additional_info"; private static final String SELECT_API_USAGE_STATE = "(select aus.id, aus.created_time, aus.tenant_id, '13814000-1dd2-11b2-8080-808080808080'::uuid as customer_id, " + @@ -215,6 +231,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { entityTableMap.put(EntityType.USER, "tb_user"); entityTableMap.put(EntityType.TENANT, "tenant"); entityTableMap.put(EntityType.API_USAGE_STATE, SELECT_API_USAGE_STATE); + entityTableMap.put(EntityType.EDGE, "edge"); } public static EntityType[] RELATION_QUERY_ENTITY_TYPES = new EntityType[]{ @@ -447,6 +464,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { case DEVICE_SEARCH_QUERY: case ASSET_SEARCH_QUERY: case ENTITY_VIEW_SEARCH_QUERY: + case EDGE_SEARCH_QUERY: return this.defaultPermissionQuery(ctx); default: if (ctx.getEntityType() == EntityType.TENANT) { @@ -483,11 +501,13 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { case ASSET_TYPE: case DEVICE_TYPE: case ENTITY_VIEW_TYPE: + case EDGE_TYPE: return this.typeQuery(ctx, entityFilter); case RELATIONS_QUERY: case DEVICE_SEARCH_QUERY: case ASSET_SEARCH_QUERY: case ENTITY_VIEW_SEARCH_QUERY: + case EDGE_SEARCH_QUERY: case API_USAGE_STATE: case ENTITY_TYPE: return ""; @@ -509,6 +529,9 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { case ENTITY_VIEW_SEARCH_QUERY: EntityViewSearchQueryFilter entityViewQuery = (EntityViewSearchQueryFilter) entityFilter; return entitySearchQuery(ctx, entityViewQuery, EntityType.ENTITY_VIEW, entityViewQuery.getEntityViewTypes()); + case EDGE_SEARCH_QUERY: + EdgeSearchQueryFilter edgeQuery = (EdgeSearchQueryFilter) entityFilter; + return entitySearchQuery(ctx, edgeQuery, EntityType.EDGE, edgeQuery.getEdgeTypes()); default: return entityTableMap.get(ctx.getEntityType()); } @@ -714,6 +737,10 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { type = ((EntityViewTypeFilter) filter).getEntityViewType(); name = ((EntityViewTypeFilter) filter).getEntityViewNameFilter(); break; + case EDGE_TYPE: + type = ((EdgeTypeFilter) filter).getEdgeType(); + name = ((EdgeTypeFilter) filter).getEdgeNameFilter(); + break; default: throw new RuntimeException("Not supported!"); } @@ -741,6 +768,9 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { case ENTITY_VIEW_TYPE: case ENTITY_VIEW_SEARCH_QUERY: return EntityType.ENTITY_VIEW; + case EDGE_TYPE: + case EDGE_SEARCH_QUERY: + return EntityType.EDGE; case RELATIONS_QUERY: return ((RelationsQueryFilter) entityFilter).getRootEntity().getEntityType(); case API_USAGE_STATE: diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java index 89b63b87cf..5e10cabda2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java @@ -49,12 +49,17 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple private RelationInsertRepository relationInsertRepository; @Override - public ListenableFuture> findAllByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { - return service.submit(() -> DaoUtil.convertDataList( + public ListenableFuture> findAllByFromAsync(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { + return service.submit(() -> findAllByFrom(tenantId, from, typeGroup)); + } + + @Override + public List findAllByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { + return DaoUtil.convertDataList( relationRepository.findAllByFromIdAndFromTypeAndRelationTypeGroup( from.getId(), from.getEntityType().name(), - typeGroup.name()))); + typeGroup.name())); } @Override @@ -68,12 +73,17 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple } @Override - public ListenableFuture> findAllByTo(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup) { - return service.submit(() -> DaoUtil.convertDataList( + public ListenableFuture> findAllByToAsync(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup) { + return service.submit(() -> findAllByTo(tenantId, to, typeGroup)); + } + + @Override + public List findAllByTo(TenantId tenantId, EntityId to, RelationTypeGroup typeGroup) { + return DaoUtil.convertDataList( relationRepository.findAllByToIdAndToTypeAndRelationTypeGroup( to.getId(), to.getEntityType().name(), - typeGroup.name()))); + typeGroup.name())); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java index 66907fea5f..760d9e7aeb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.dao.sql.rule; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; @@ -22,12 +25,19 @@ import org.springframework.stereotype.Component; 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.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.RuleChainEntity; +import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.UUID; @@ -38,6 +48,9 @@ public class JpaRuleChainDao extends JpaAbstractSearchTextDao getEntityClass() { return RuleChainEntity.class; @@ -50,6 +63,7 @@ public class JpaRuleChainDao extends JpaAbstractSearchTextDao findRuleChainsByTenantId(UUID tenantId, PageLink pageLink) { + log.debug("Try to find rule chains by tenantId [{}] and pageLink [{}]", tenantId, pageLink); return DaoUtil.toPageData(ruleChainRepository .findByTenantId( tenantId, @@ -57,6 +71,47 @@ public class JpaRuleChainDao extends JpaAbstractSearchTextDao findRuleChainsByTenantIdAndType(UUID tenantId, RuleChainType type, PageLink pageLink) { + log.debug("Try to find rule chains by tenantId [{}], type [{}] and pageLink [{}]", tenantId, type, pageLink); + return DaoUtil.toPageData(ruleChainRepository + .findByTenantIdAndType( + tenantId, + type, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findRuleChainsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink) { + log.debug("Try to find rule chains by tenantId [{}], edgeId [{}] and pageLink [{}]", tenantId, edgeId, pageLink); + + return DaoUtil.toPageData(ruleChainRepository + .findByTenantIdAndEdgeId( + tenantId, + edgeId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public ListenableFuture> findAutoAssignToEdgeRuleChainsByTenantId(UUID tenantId) { + log.debug("Try to find auto assign to edge rule chains by tenantId [{}]", tenantId); + ListenableFuture> relations = + relationDao.findAllByFromAndType(new TenantId(tenantId), new TenantId(tenantId), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE_AUTO_ASSIGN_RULE_CHAIN); + return Futures.transformAsync(relations, input -> { + if (input != null && !input.isEmpty()) { + List> ruleChainsFutures = new ArrayList<>(input.size()); + for (EntityRelation relation : input) { + ruleChainsFutures.add(findByIdAsync(new TenantId(tenantId), relation.getTo().getId())); + } + return Futures.successfulAsList(ruleChainsFutures); + } else { + return Futures.immediateFuture(Collections.emptyList()); + } + }, MoreExecutors.directExecutor()); + } + @Override public Long countByTenantId(TenantId tenantId) { return ruleChainRepository.countByTenantId(tenantId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java index bf37aaafd6..3c3c5ff3b4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java @@ -20,6 +20,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.dao.model.sql.DashboardInfoEntity; import org.thingsboard.server.dao.model.sql.RuleChainEntity; import java.util.UUID; @@ -32,5 +34,21 @@ public interface RuleChainRepository extends PagingAndSortingRepository findByTenantIdAndType(@Param("tenantId") UUID tenantId, + @Param("type") RuleChainType type, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT rc FROM RuleChainEntity rc, RelationEntity re WHERE rc.tenantId = :tenantId " + + "AND rc.id = re.toId AND re.toType = 'RULE_CHAIN' AND re.relationTypeGroup = 'EDGE' " + + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + + "AND LOWER(rc.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, + @Param("edgeId") UUID edgeId, + @Param("searchText") String searchText, + Pageable pageable); Long countByTenantId(UUID tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/psql/PsqlLatestInsertTsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/psql/PsqlLatestInsertTsRepository.java index 7a056f4872..5b18fde202 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/psql/PsqlLatestInsertTsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/psql/PsqlLatestInsertTsRepository.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.sqlts.insert.latest.psql; +import org.springframework.beans.factory.annotation.Value; import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.stereotype.Repository; import org.springframework.transaction.TransactionStatus; @@ -37,20 +38,29 @@ import java.util.List; @Transactional public class PsqlLatestInsertTsRepository extends AbstractInsertRepository implements InsertLatestTsRepository { - private static final String BATCH_UPDATE = - "UPDATE ts_kv_latest SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json) WHERE entity_id = ? and key = ?"; + @Value("${sql.ts_latest.update_by_latest_ts:true}") + private Boolean updateByLatestTs; + private static final String BATCH_UPDATE = + "UPDATE ts_kv_latest SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json) WHERE entity_id = ? AND key = ?"; private static final String INSERT_OR_UPDATE = "INSERT INTO ts_kv_latest (entity_id, key, ts, bool_v, str_v, long_v, dbl_v, json_v) VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json)) " + - "ON CONFLICT (entity_id, key) DO UPDATE SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json);"; + "ON CONFLICT (entity_id, key) DO UPDATE SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json)"; + + private static final String BATCH_UPDATE_BY_LATEST_TS = BATCH_UPDATE + " AND ts_kv_latest.ts <= ?"; + + private static final String INSERT_OR_UPDATE_BY_LATEST_TS = INSERT_OR_UPDATE + " WHERE ts_kv_latest.ts <= ?"; @Override public void saveOrUpdate(List entities) { transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { - int[] result = jdbcTemplate.batchUpdate(BATCH_UPDATE, new BatchPreparedStatementSetter() { + String batchUpdateQuery = updateByLatestTs ? BATCH_UPDATE_BY_LATEST_TS : BATCH_UPDATE; + String insertOrUpdateQuery = updateByLatestTs ? INSERT_OR_UPDATE_BY_LATEST_TS : INSERT_OR_UPDATE; + + int[] result = jdbcTemplate.batchUpdate(batchUpdateQuery, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { TsKvLatestEntity tsKvLatestEntity = entities.get(i); @@ -80,6 +90,9 @@ public class PsqlLatestInsertTsRepository extends AbstractInsertRepository imple ps.setObject(7, tsKvLatestEntity.getEntityId()); ps.setInt(8, tsKvLatestEntity.getKey()); + if (updateByLatestTs) { + ps.setLong(9, tsKvLatestEntity.getTs()); + } } @Override @@ -102,7 +115,7 @@ public class PsqlLatestInsertTsRepository extends AbstractInsertRepository imple } } - jdbcTemplate.batchUpdate(INSERT_OR_UPDATE, new BatchPreparedStatementSetter() { + jdbcTemplate.batchUpdate(insertOrUpdateQuery, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { TsKvLatestEntity tsKvLatestEntity = insertEntities.get(i); @@ -111,6 +124,9 @@ public class PsqlLatestInsertTsRepository extends AbstractInsertRepository imple ps.setLong(3, tsKvLatestEntity.getTs()); ps.setLong(9, tsKvLatestEntity.getTs()); + if (updateByLatestTs) { + ps.setLong(15, tsKvLatestEntity.getTs()); + } if (tsKvLatestEntity.getBooleanValue() != null) { ps.setBoolean(4, tsKvLatestEntity.getBooleanValue()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 96a6d14c42..b514b43cf3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -35,7 +35,7 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.resource.TbResourceService; +import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -90,7 +90,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe private RuleChainService ruleChainService; @Autowired - private TbResourceService resourceService; + private ResourceService resourceService; @Override public Tenant findTenantById(TenantId tenantId) { @@ -141,6 +141,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe assetService.deleteAssetsByTenantId(tenantId); deviceService.deleteDevicesByTenantId(tenantId); deviceProfileService.deleteDeviceProfilesByTenantId(tenantId); + edgeService.deleteEdgesByTenantId(tenantId); userService.deleteTenantAdmins(tenantId); ruleChainService.deleteRuleChainsByTenantId(tenantId); apiUsageStateService.deleteApiUsageStateByTenantId(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index 7d09578dd1..240d5a0b88 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -62,6 +62,8 @@ import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; @@ -107,6 +109,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD private PreparedStatement[] fetchStmtsDesc; private PreparedStatement deleteStmt; private PreparedStatement deletePartitionStmt; + private final Lock stmtCreationLock = new ReentrantLock(); private boolean isInstall() { return environment.acceptsProfiles(Profiles.of("install")); @@ -545,13 +548,20 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD private PreparedStatement getDeleteStmt() { if (deleteStmt == null) { - deleteStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_CF + - " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.TS_COLUMN + " >= ? " - + "AND " + ModelConstants.TS_COLUMN + " < ?"); + stmtCreationLock.lock(); + try { + if (deleteStmt == null) { + deleteStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_CF + + " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.TS_COLUMN + " >= ? " + + "AND " + ModelConstants.TS_COLUMN + " < ?"); + } + } finally { + stmtCreationLock.unlock(); + } } return deleteStmt; } @@ -585,27 +595,41 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD private PreparedStatement getDeletePartitionStmt() { if (deletePartitionStmt == null) { - deletePartitionStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_PARTITIONS_CF + - " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM); + stmtCreationLock.lock(); + try { + if (deletePartitionStmt == null) { + deletePartitionStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_PARTITIONS_CF + + " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM); + } + } finally { + stmtCreationLock.unlock(); + } } return deletePartitionStmt; } private PreparedStatement getSaveStmt(DataType dataType) { if (saveStmts == null) { - saveStmts = new PreparedStatement[DataType.values().length]; - for (DataType type : DataType.values()) { - saveStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF + - "(" + ModelConstants.ENTITY_TYPE_COLUMN + - "," + ModelConstants.ENTITY_ID_COLUMN + - "," + ModelConstants.KEY_COLUMN + - "," + ModelConstants.PARTITION_COLUMN + - "," + ModelConstants.TS_COLUMN + - "," + getColumnName(type) + ")" + - " VALUES(?, ?, ?, ?, ?, ?)"); + stmtCreationLock.lock(); + try { + if (saveStmts == null) { + saveStmts = new PreparedStatement[DataType.values().length]; + for (DataType type : DataType.values()) { + saveStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF + + "(" + ModelConstants.ENTITY_TYPE_COLUMN + + "," + ModelConstants.ENTITY_ID_COLUMN + + "," + ModelConstants.KEY_COLUMN + + "," + ModelConstants.PARTITION_COLUMN + + "," + ModelConstants.TS_COLUMN + + "," + getColumnName(type) + ")" + + " VALUES(?, ?, ?, ?, ?, ?)"); + } + } + } finally { + stmtCreationLock.unlock(); } } return saveStmts[dataType.ordinal()]; @@ -613,16 +637,23 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD private PreparedStatement getSaveTtlStmt(DataType dataType) { if (saveTtlStmts == null) { - saveTtlStmts = new PreparedStatement[DataType.values().length]; - for (DataType type : DataType.values()) { - saveTtlStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF + - "(" + ModelConstants.ENTITY_TYPE_COLUMN + - "," + ModelConstants.ENTITY_ID_COLUMN + - "," + ModelConstants.KEY_COLUMN + - "," + ModelConstants.PARTITION_COLUMN + - "," + ModelConstants.TS_COLUMN + - "," + getColumnName(type) + ")" + - " VALUES(?, ?, ?, ?, ?, ?) USING TTL ?"); + stmtCreationLock.lock(); + try { + if (saveTtlStmts == null) { + saveTtlStmts = new PreparedStatement[DataType.values().length]; + for (DataType type : DataType.values()) { + saveTtlStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF + + "(" + ModelConstants.ENTITY_TYPE_COLUMN + + "," + ModelConstants.ENTITY_ID_COLUMN + + "," + ModelConstants.KEY_COLUMN + + "," + ModelConstants.PARTITION_COLUMN + + "," + ModelConstants.TS_COLUMN + + "," + getColumnName(type) + ")" + + " VALUES(?, ?, ?, ?, ?, ?) USING TTL ?"); + } + } + } finally { + stmtCreationLock.unlock(); } } return saveTtlStmts[dataType.ordinal()]; @@ -630,24 +661,38 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD private PreparedStatement getPartitionInsertStmt() { if (partitionInsertStmt == null) { - partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF + - "(" + ModelConstants.ENTITY_TYPE_COLUMN + - "," + ModelConstants.ENTITY_ID_COLUMN + - "," + ModelConstants.PARTITION_COLUMN + - "," + ModelConstants.KEY_COLUMN + ")" + - " VALUES(?, ?, ?, ?)"); + stmtCreationLock.lock(); + try { + if (partitionInsertStmt == null) { + partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF + + "(" + ModelConstants.ENTITY_TYPE_COLUMN + + "," + ModelConstants.ENTITY_ID_COLUMN + + "," + ModelConstants.PARTITION_COLUMN + + "," + ModelConstants.KEY_COLUMN + ")" + + " VALUES(?, ?, ?, ?)"); + } + } finally { + stmtCreationLock.unlock(); + } } return partitionInsertStmt; } private PreparedStatement getPartitionInsertTtlStmt() { if (partitionInsertTtlStmt == null) { - partitionInsertTtlStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF + - "(" + ModelConstants.ENTITY_TYPE_COLUMN + - "," + ModelConstants.ENTITY_ID_COLUMN + - "," + ModelConstants.PARTITION_COLUMN + - "," + ModelConstants.KEY_COLUMN + ")" + - " VALUES(?, ?, ?, ?) USING TTL ?"); + stmtCreationLock.lock(); + try { + if (partitionInsertTtlStmt == null) { + partitionInsertTtlStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF + + "(" + ModelConstants.ENTITY_TYPE_COLUMN + + "," + ModelConstants.ENTITY_ID_COLUMN + + "," + ModelConstants.PARTITION_COLUMN + + "," + ModelConstants.KEY_COLUMN + ")" + + " VALUES(?, ?, ?, ?) USING TTL ?"); + } + } finally { + stmtCreationLock.unlock(); + } } return partitionInsertTtlStmt; } @@ -713,12 +758,26 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD switch (orderBy) { case ASC_ORDER: if (fetchStmtsAsc == null) { - fetchStmtsAsc = initFetchStmt(orderBy); + stmtCreationLock.lock(); + try { + if (fetchStmtsAsc == null) { + fetchStmtsAsc = initFetchStmt(orderBy); + } + } finally { + stmtCreationLock.unlock(); + } } return fetchStmtsAsc[aggType.ordinal()]; case DESC_ORDER: if (fetchStmtsDesc == null) { - fetchStmtsDesc = initFetchStmt(orderBy); + stmtCreationLock.lock(); + try { + if (fetchStmtsDesc == null) { + fetchStmtsDesc = initFetchStmt(orderBy); + } + } finally { + stmtCreationLock.unlock(); + } } return fetchStmtsDesc[aggType.ordinal()]; default: diff --git a/dao/src/main/resources/sql/schema-entities-hsql.sql b/dao/src/main/resources/sql/schema-entities-hsql.sql index faaf91a94f..56d3730ea9 100644 --- a/dao/src/main/resources/sql/schema-entities-hsql.sql +++ b/dao/src/main/resources/sql/schema-entities-hsql.sql @@ -127,6 +127,7 @@ CREATE TABLE IF NOT EXISTS rule_chain ( additional_info varchar, configuration varchar(10000000), name varchar(255), + type varchar(255), first_rule_node_id uuid, root boolean, debug_mode boolean, @@ -360,7 +361,6 @@ CREATE TABLE IF NOT EXISTS ts_kv_dictionary ( CONSTRAINT ts_key_id_pkey PRIMARY KEY (key) ); - CREATE TABLE IF NOT EXISTS oauth2_client_registration_info ( id uuid NOT NULL CONSTRAINT oauth2_client_registration_info_pkey PRIMARY KEY, enabled boolean, @@ -457,3 +457,35 @@ CREATE TABLE IF NOT EXISTS resource ( data varchar, CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_key) ); + +CREATE TABLE IF NOT EXISTS edge ( + id uuid NOT NULL CONSTRAINT edge_pkey PRIMARY KEY, + created_time bigint NOT NULL, + additional_info varchar, + customer_id uuid, + root_rule_chain_id uuid, + type varchar(255), + name varchar(255), + label varchar(255), + routing_key varchar(255), + secret varchar(255), + edge_license_key varchar(30), + cloud_endpoint varchar(255), + search_text varchar(255), + tenant_id uuid, + CONSTRAINT edge_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT edge_routing_key_unq_key UNIQUE (routing_key) +); + +CREATE TABLE IF NOT EXISTS edge_event ( + id uuid NOT NULL CONSTRAINT edge_event_pkey PRIMARY KEY, + created_time bigint NOT NULL, + edge_id uuid, + edge_event_type varchar(255), + edge_event_uid varchar(255), + entity_id uuid, + edge_event_action varchar(255), + body varchar(10000000), + tenant_id uuid, + ts bigint NOT NULL +); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index cf3ef0f6a6..aba1fa0126 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -145,6 +145,7 @@ CREATE TABLE IF NOT EXISTS rule_chain ( additional_info varchar, configuration varchar(10000000), name varchar(255), + type varchar(255), first_rule_node_id uuid, root boolean, debug_mode boolean, @@ -484,6 +485,38 @@ CREATE TABLE IF NOT EXISTS resource ( CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_key) ); +CREATE TABLE IF NOT EXISTS edge ( + id uuid NOT NULL CONSTRAINT edge_pkey PRIMARY KEY, + created_time bigint NOT NULL, + additional_info varchar, + customer_id uuid, + root_rule_chain_id uuid, + type varchar(255), + name varchar(255), + label varchar(255), + routing_key varchar(255), + secret varchar(255), + edge_license_key varchar(30), + cloud_endpoint varchar(255), + search_text varchar(255), + tenant_id uuid, + CONSTRAINT edge_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT edge_routing_key_unq_key UNIQUE (routing_key) +); + +CREATE TABLE IF NOT EXISTS edge_event ( + id uuid NOT NULL CONSTRAINT edge_event_pkey PRIMARY KEY, + created_time bigint NOT NULL, + edge_id uuid, + edge_event_type varchar(255), + edge_event_uid varchar(255), + entity_id uuid, + edge_event_action varchar(255), + body varchar(10000000), + tenant_id uuid, + ts bigint NOT NULL +); + CREATE OR REPLACE PROCEDURE cleanup_events_by_ttl(IN ttl bigint, IN debug_ttl bigint, INOUT deleted bigint) LANGUAGE plpgsql AS $$ @@ -517,3 +550,20 @@ BEGIN END; $$ LANGUAGE plpgsql; + +CREATE OR REPLACE PROCEDURE cleanup_edge_events_by_ttl(IN ttl bigint, INOUT deleted bigint) + LANGUAGE plpgsql AS +$$ +DECLARE + ttl_ts bigint; + ttl_deleted_count bigint DEFAULT 0; +BEGIN + IF ttl > 0 THEN + ttl_ts := (EXTRACT(EPOCH FROM current_timestamp) * 1000 - ttl::bigint * 1000)::bigint; + EXECUTE format( + 'WITH deleted AS (DELETE FROM edge_event WHERE ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted', ttl_ts) into ttl_deleted_count; + END IF; + RAISE NOTICE 'Edge events removed by ttl: %', ttl_deleted_count; + deleted := ttl_deleted_count; +END +$$; diff --git a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java index 124a846522..cb6de2fe89 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java +++ b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java @@ -25,7 +25,7 @@ import java.util.Arrays; @RunWith(ClasspathSuite.class) @ClassnameFilters({ - "org.thingsboard.server.dao.service.*ServiceNoSqlTest" + "org.thingsboard.server.dao.service.nosql.*ServiceNoSqlTest" }) public class NoSqlDaoServiceTestSuite { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index 6cddc302fc..091ab1741e 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; @@ -48,12 +49,14 @@ 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.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.firmware.FirmwareService; import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.dao.resource.TbResourceService; +import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.tenant.TenantProfileService; @@ -69,6 +72,8 @@ import java.util.Comparator; import java.util.HashMap; import java.util.Map; +import static org.junit.Assert.assertNotNull; + @RunWith(SpringRunner.class) @ContextConfiguration(classes = AbstractServiceTest.class, loader = AnnotationConfigContextLoader.class) @@ -135,6 +140,12 @@ public abstract class AbstractServiceTest { @Autowired protected RuleChainService ruleChainService; + @Autowired + protected EdgeService edgeService; + + @Autowired + protected EdgeEventService edgeEventService; + @Autowired private ComponentDescriptorService componentDescriptorService; @@ -145,12 +156,13 @@ public abstract class AbstractServiceTest { protected DeviceProfileService deviceProfileService; @Autowired - protected TbResourceService resourceService; + protected ResourceService resourceService; + @Autowired protected FirmwareService firmwareService; - class IdComparator implements Comparator { + public class IdComparator implements Comparator { @Override public int compare(D o1, D o2) { return o1.getId().getId().compareTo(o2.getId().getId()); @@ -221,4 +233,12 @@ public abstract class AbstractServiceTest { return deviceProfile; } + public TenantId createTenant() { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant " + Uuids.timeBased()); + Tenant savedTenant = tenantService.saveTenant(tenant); + assertNotNull(savedTenant); + return savedTenant.getId(); + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java index 36f4314d7a..98b1110ddc 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java @@ -25,7 +25,9 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.edge.Edge; 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.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -122,7 +124,7 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { dashboard.setTenantId(tenantId); dashboard = dashboardService.saveDashboard(dashboard); Tenant tenant = new Tenant(); - tenant.setTitle("Test different tenant"); + tenant.setTitle("Test different tenant [dashboard]"); tenant = tenantService.saveTenant(tenant); Customer customer = new Customer(); customer.setTenantId(tenant.getId()); @@ -327,4 +329,43 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { tenantService.deleteTenant(tenantId); } + @Test(expected = DataValidationException.class) + public void testAssignDashboardToNonExistentEdge() { + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("My dashboard"); + dashboard.setTenantId(tenantId); + dashboard = dashboardService.saveDashboard(dashboard); + try { + dashboardService.assignDashboardToEdge(tenantId, dashboard.getId(), new EdgeId(Uuids.timeBased())); + } finally { + dashboardService.deleteDashboard(tenantId, dashboard.getId()); + } + } + + @Test(expected = DataValidationException.class) + public void testAssignDashboardToEdgeFromDifferentTenant() { + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("My dashboard"); + dashboard.setTenantId(tenantId); + dashboard = dashboardService.saveDashboard(dashboard); + Tenant tenant = new Tenant(); + tenant.setTitle("Test different tenant [edge]"); + tenant = tenantService.saveTenant(tenant); + Edge edge = new Edge(); + edge.setTenantId(tenant.getId()); + edge.setType("default"); + edge.setName("Test different edge"); + edge.setType("default"); + edge.setSecret(RandomStringUtils.randomAlphanumeric(20)); + edge.setRoutingKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setEdgeLicenseKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setCloudEndpoint("http://localhost:8080"); + edge = edgeService.saveEdge(edge); + try { + dashboardService.assignDashboardToEdge(tenantId, dashboard.getId(), edge.getId()); + } finally { + dashboardService.deleteDashboard(tenantId, dashboard.getId()); + tenantService.deleteTenant(tenant.getId()); + } + } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java index d9ca69632b..6d7b695ef1 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java @@ -21,13 +21,20 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.thingsboard.server.common.data.*; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceInfo; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.Firmware; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.CustomerId; 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.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.exception.DataValidationException; import java.nio.ByteBuffer; @@ -38,33 +45,93 @@ import java.util.List; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; public abstract class BaseDeviceServiceTest extends AbstractServiceTest { - + private IdComparator idComparator = new IdComparator<>(); - + private TenantId tenantId; + private TenantId anotherTenantId; @Before public void before() { - Tenant tenant = new Tenant(); - tenant.setTitle("My tenant"); - Tenant savedTenant = tenantService.saveTenant(tenant); - Assert.assertNotNull(savedTenant); - tenantId = savedTenant.getId(); + tenantId = createTenant(); + anotherTenantId = createTenant(); } @After public void after() { tenantService.deleteTenant(tenantId); + tenantService.deleteTenant(anotherTenantId); + + tenantProfileService.deleteTenantProfiles(tenantId); + tenantProfileService.deleteTenantProfiles(anotherTenantId); } @Test - public void testSaveDevice() { + public void testSaveDevicesWithoutMaxDeviceLimit() { + Device device = this.saveDevice(tenantId, "My device"); + deleteDevice(tenantId, device); + } + + @Test + public void testSaveDevicesWithInfiniteMaxDeviceLimit() { + TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); + defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxDevices(Long.MAX_VALUE).build()); + tenantProfileService.saveTenantProfile(tenantId, defaultTenantProfile); + + Device device = this.saveDevice(tenantId, "My device"); + deleteDevice(tenantId, device); + } + + @Test(expected = DataValidationException.class) + public void testSaveDevicesWithMaxDeviceOutOfLimit() { + TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); + defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxDevices(1).build()); + tenantProfileService.saveTenantProfile(tenantId, defaultTenantProfile); + + Assert.assertEquals(0, deviceService.countByTenantId(tenantId)); + + this.saveDevice(tenantId, "My first device"); + Assert.assertEquals(1, deviceService.countByTenantId(tenantId)); + + this.saveDevice(tenantId, "My second device that out of maxDeviceCount limit"); + } + + @Test + public void testCountByTenantId() { + Assert.assertEquals(0, deviceService.countByTenantId(tenantId)); + Assert.assertEquals(0, deviceService.countByTenantId(anotherTenantId)); + Assert.assertEquals(0, deviceService.countByTenantId(TenantId.SYS_TENANT_ID)); + + Device anotherDevice = this.saveDevice(anotherTenantId, "My device 1"); + Assert.assertEquals(1, deviceService.countByTenantId(anotherTenantId)); + + int maxDevices = 8; + List devices = new ArrayList<>(maxDevices); + + for (int i = 1; i <= maxDevices; i++) { + devices.add(this.saveDevice(tenantId, "My device " + i)); + Assert.assertEquals(i, deviceService.countByTenantId(tenantId)); + } + + Assert.assertEquals(maxDevices, deviceService.countByTenantId(tenantId)); + Assert.assertEquals(1, deviceService.countByTenantId(anotherTenantId)); + Assert.assertEquals(0, deviceService.countByTenantId(TenantId.SYS_TENANT_ID)); + + devices.forEach(device -> deleteDevice(tenantId, device)); + deleteDevice(anotherTenantId, anotherDevice); + } + + void deleteDevice(TenantId tenantId, Device device) { + deviceService.deleteDevice(tenantId, device.getId()); + } + + Device saveDevice(TenantId tenantId, final String name) { Device device = new Device(); device.setTenantId(tenantId); - device.setName("My device"); + device.setName(name); device.setType("default"); Device savedDevice = deviceService.saveDevice(device); - + Assert.assertNotNull(savedDevice); Assert.assertNotNull(savedDevice.getId()); Assert.assertTrue(savedDevice.getCreatedTime() > 0); @@ -72,7 +139,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { Assert.assertNotNull(savedDevice.getCustomerId()); Assert.assertEquals(NULL_UUID, savedDevice.getCustomerId().getId()); Assert.assertEquals(device.getName(), savedDevice.getName()); - + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, savedDevice.getId()); Assert.assertNotNull(deviceCredentials); Assert.assertNotNull(deviceCredentials.getId()); @@ -80,14 +147,13 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { Assert.assertEquals(DeviceCredentialsType.ACCESS_TOKEN, deviceCredentials.getCredentialsType()); Assert.assertNotNull(deviceCredentials.getCredentialsId()); Assert.assertEquals(20, deviceCredentials.getCredentialsId().length()); - - savedDevice.setName("My new device"); - + + savedDevice.setName("New " + savedDevice.getName()); + deviceService.saveDevice(savedDevice); Device foundDevice = deviceService.findDeviceById(tenantId, savedDevice.getId()); Assert.assertEquals(foundDevice.getName(), savedDevice.getName()); - - deviceService.deleteDevice(tenantId, savedDevice.getId()); + return foundDevice; } @Test @@ -140,7 +206,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { device.setTenantId(tenantId); deviceService.saveDevice(device); } - + @Test(expected = DataValidationException.class) public void testSaveDeviceWithEmptyTenant() { Device device = new Device(); @@ -148,7 +214,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { device.setType("default"); deviceService.saveDevice(device); } - + @Test(expected = DataValidationException.class) public void testSaveDeviceWithInvalidTenant() { Device device = new Device(); @@ -157,7 +223,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { device.setTenantId(new TenantId(Uuids.timeBased())); deviceService.saveDevice(device); } - + @Test(expected = DataValidationException.class) public void testAssignDeviceToNonExistentCustomer() { Device device = new Device(); @@ -193,7 +259,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { tenantService.deleteTenant(tenant.getId()); } } - + @Test public void testFindDeviceById() { Device device = new Device(); @@ -242,7 +308,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { devices.forEach((device) -> { deviceService.deleteDevice(tenantId, device.getId()); }); } } - + @Test public void testDeleteDevice() { Device device = new Device(); @@ -258,15 +324,15 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { DeviceCredentials foundDeviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, savedDevice.getId()); Assert.assertNull(foundDeviceCredentials); } - + @Test public void testFindDevicesByTenantId() { Tenant tenant = new Tenant(); tenant.setTitle("Test tenant"); tenant = tenantService.saveTenant(tenant); - + TenantId tenantId = tenant.getId(); - + List devices = new ArrayList<>(); for (int i=0;i<178;i++) { Device device = new Device(); @@ -275,7 +341,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { device.setType("default"); devices.add(deviceService.saveDevice(device)); } - + List loadedDevices = new ArrayList<>(); PageLink pageLink = new PageLink(23); PageData pageData = null; @@ -286,19 +352,19 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); - + Collections.sort(devices, idComparator); Collections.sort(loadedDevices, idComparator); - + Assert.assertEquals(devices, loadedDevices); - + deviceService.deleteDevicesByTenantId(tenantId); pageLink = new PageLink(33); pageData = deviceService.findDevicesByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); - + tenantService.deleteTenant(tenantId); } @@ -328,7 +394,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { device.setType("default"); devicesTitle2.add(new DeviceInfo(deviceService.saveDevice(device), null, false, "default")); } - + List loadedDevicesTitle1 = new ArrayList<>(); PageLink pageLink = new PageLink(15, 0, title1); PageData pageData = null; @@ -339,12 +405,12 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); - + Collections.sort(devicesTitle1, idComparator); Collections.sort(loadedDevicesTitle1, idComparator); - + Assert.assertEquals(devicesTitle1, loadedDevicesTitle1); - + List loadedDevicesTitle2 = new ArrayList<>(); pageLink = new PageLink(4, 0, title2); do { @@ -357,22 +423,22 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { Collections.sort(devicesTitle2, idComparator); Collections.sort(loadedDevicesTitle2, idComparator); - + Assert.assertEquals(devicesTitle2, loadedDevicesTitle2); for (Device device : loadedDevicesTitle1) { deviceService.deleteDevice(tenantId, device.getId()); } - + pageLink = new PageLink(4, 0, title1); pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); - + for (Device device : loadedDevicesTitle2) { deviceService.deleteDevice(tenantId, device.getId()); } - + pageLink = new PageLink(4, 0, title2); pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); @@ -457,21 +523,21 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } - + @Test public void testFindDevicesByTenantIdAndCustomerId() { Tenant tenant = new Tenant(); tenant.setTitle("Test tenant"); tenant = tenantService.saveTenant(tenant); - + TenantId tenantId = tenant.getId(); - + Customer customer = new Customer(); customer.setTitle("Test customer"); customer.setTenantId(tenantId); customer = customerService.saveCustomer(customer); CustomerId customerId = customer.getId(); - + List devices = new ArrayList<>(); for (int i=0;i<278;i++) { Device device = new Device(); @@ -481,7 +547,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { device = deviceService.saveDevice(device); devices.add(new DeviceInfo(deviceService.assignDeviceToCustomer(tenantId, device.getId(), customerId), customer.getTitle(), customer.isPublic(), "default")); } - + List loadedDevices = new ArrayList<>(); PageLink pageLink = new PageLink(23); PageData pageData = null; @@ -492,31 +558,31 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); - + Collections.sort(devices, idComparator); Collections.sort(loadedDevices, idComparator); - + Assert.assertEquals(devices, loadedDevices); - + deviceService.unassignCustomerDevices(tenantId, customerId); pageLink = new PageLink(33); pageData = deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); - + tenantService.deleteTenant(tenantId); } - + @Test public void testFindDevicesByTenantIdCustomerIdAndName() { - + Customer customer = new Customer(); customer.setTitle("Test customer"); customer.setTenantId(tenantId); customer = customerService.saveCustomer(customer); CustomerId customerId = customer.getId(); - + String title1 = "Device title 1"; List devicesTitle1 = new ArrayList<>(); for (int i=0;i<175;i++) { @@ -543,7 +609,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { device = deviceService.saveDevice(device); devicesTitle2.add(deviceService.assignDeviceToCustomer(tenantId, device.getId(), customerId)); } - + List loadedDevicesTitle1 = new ArrayList<>(); PageLink pageLink = new PageLink(15, 0, title1); PageData pageData = null; @@ -554,12 +620,12 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { pageLink = pageLink.nextPageLink(); } } while (pageData.hasNext()); - + Collections.sort(devicesTitle1, idComparator); Collections.sort(loadedDevicesTitle1, idComparator); - + Assert.assertEquals(devicesTitle1, loadedDevicesTitle1); - + List loadedDevicesTitle2 = new ArrayList<>(); pageLink = new PageLink(4, 0, title2); do { @@ -572,22 +638,22 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { Collections.sort(devicesTitle2, idComparator); Collections.sort(loadedDevicesTitle2, idComparator); - + Assert.assertEquals(devicesTitle2, loadedDevicesTitle2); for (Device device : loadedDevicesTitle1) { deviceService.deleteDevice(tenantId, device.getId()); } - + pageLink = new PageLink(4, 0, title1); pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); - + for (Device device : loadedDevicesTitle2) { deviceService.deleteDevice(tenantId, device.getId()); } - + pageLink = new PageLink(4, 0, title2); pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink); Assert.assertFalse(pageData.hasNext()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeEventServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeEventServiceTest.java new file mode 100644 index 0000000000..a7a67a8f4b --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeEventServiceTest.java @@ -0,0 +1,129 @@ +/** + * Copyright © 2016-2021 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.service; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeEventId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.SortOrder; +import org.thingsboard.server.common.data.page.TimePageLink; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.Month; +import java.time.ZoneOffset; + +public abstract class BaseEdgeEventServiceTest extends AbstractServiceTest { + + @Test + public void saveEdgeEvent() throws Exception { + EdgeId edgeId = new EdgeId(Uuids.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); + EdgeEvent edgeEvent = generateEdgeEvent(null, edgeId, deviceId, EdgeEventActionType.ADDED); + EdgeEvent saved = edgeEventService.saveAsync(edgeEvent).get(); + Assert.assertEquals(saved.getTenantId(), edgeEvent.getTenantId()); + Assert.assertEquals(saved.getEdgeId(), edgeEvent.getEdgeId()); + Assert.assertEquals(saved.getEntityId(), edgeEvent.getEntityId()); + Assert.assertEquals(saved.getType(), edgeEvent.getType()); + Assert.assertEquals(saved.getAction(), edgeEvent.getAction()); + Assert.assertEquals(saved.getBody(), edgeEvent.getBody()); + } + + protected EdgeEvent generateEdgeEvent(TenantId tenantId, EdgeId edgeId, EntityId entityId, EdgeEventActionType edgeEventAction) throws IOException { + if (tenantId == null) { + tenantId = new TenantId(Uuids.timeBased()); + } + EdgeEvent edgeEvent = new EdgeEvent(); + edgeEvent.setTenantId(tenantId); + edgeEvent.setEdgeId(edgeId); + edgeEvent.setEntityId(entityId.getId()); + edgeEvent.setType(EdgeEventType.DEVICE); + edgeEvent.setAction(edgeEventAction); + edgeEvent.setBody(readFromResource("TestJsonData.json")); + return edgeEvent; + } + + + @Test + public void findEdgeEventsByTimeDescOrder() throws Exception { + long timeBeforeStartTime = LocalDateTime.of(2020, Month.NOVEMBER, 1, 11, 30).toEpochSecond(ZoneOffset.UTC); + long startTime = LocalDateTime.of(2020, Month.NOVEMBER, 1, 12, 0).toEpochSecond(ZoneOffset.UTC); + long eventTime = LocalDateTime.of(2020, Month.NOVEMBER, 1, 12, 30).toEpochSecond(ZoneOffset.UTC); + long endTime = LocalDateTime.of(2020, Month.NOVEMBER, 1, 13, 0).toEpochSecond(ZoneOffset.UTC); + long timeAfterEndTime = LocalDateTime.of(2020, Month.NOVEMBER, 1, 13, 30).toEpochSecond(ZoneOffset.UTC); + + EdgeId edgeId = new EdgeId(Uuids.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); + TenantId tenantId = new TenantId(Uuids.timeBased()); + saveEdgeEventWithProvidedTime(timeBeforeStartTime, edgeId, deviceId, tenantId); + EdgeEvent savedEdgeEvent = saveEdgeEventWithProvidedTime(eventTime, edgeId, deviceId, tenantId); + EdgeEvent savedEdgeEvent2 = saveEdgeEventWithProvidedTime(eventTime + 1, edgeId, deviceId, tenantId); + EdgeEvent savedEdgeEvent3 = saveEdgeEventWithProvidedTime(eventTime + 2, edgeId, deviceId, tenantId); + saveEdgeEventWithProvidedTime(timeAfterEndTime, edgeId, deviceId, tenantId); + + TimePageLink pageLink = new TimePageLink(2, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), startTime, endTime); + PageData edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, true); + + Assert.assertNotNull(edgeEvents.getData()); + Assert.assertTrue(edgeEvents.getData().size() == 2); + Assert.assertTrue(edgeEvents.getData().get(0).getUuidId().equals(savedEdgeEvent3.getUuidId())); + Assert.assertTrue(edgeEvents.getData().get(1).getUuidId().equals(savedEdgeEvent2.getUuidId())); + Assert.assertTrue(edgeEvents.hasNext()); + Assert.assertNotNull(pageLink.nextPageLink()); + + edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink.nextPageLink(), true); + + Assert.assertNotNull(edgeEvents.getData()); + Assert.assertTrue(edgeEvents.getData().size() == 1); + Assert.assertTrue(edgeEvents.getData().get(0).getUuidId().equals(savedEdgeEvent.getUuidId())); + Assert.assertFalse(edgeEvents.hasNext()); + } + + @Test + public void findEdgeEventsWithTsUpdateAndWithout() throws Exception { + EdgeId edgeId = new EdgeId(Uuids.timeBased()); + DeviceId deviceId = new DeviceId(Uuids.timeBased()); + TenantId tenantId = new TenantId(Uuids.timeBased()); + TimePageLink pageLink = new TimePageLink(1); + + EdgeEvent edgeEventWithTsUpdate = generateEdgeEvent(tenantId, edgeId, deviceId, EdgeEventActionType.TIMESERIES_UPDATED); + edgeEventService.saveAsync(edgeEventWithTsUpdate).get(); + + PageData allEdgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, true); + PageData edgeEventsWithoutTsUpdate = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, false); + + Assert.assertNotNull(allEdgeEvents.getData()); + Assert.assertNotNull(edgeEventsWithoutTsUpdate.getData()); + Assert.assertEquals(1, allEdgeEvents.getData().size()); + Assert.assertEquals(allEdgeEvents.getData().get(0).getUuidId(), edgeEventWithTsUpdate.getUuidId()); + Assert.assertTrue(edgeEventsWithoutTsUpdate.getData().isEmpty()); + } + + private EdgeEvent saveEdgeEventWithProvidedTime(long time, EdgeId edgeId, EntityId entityId, TenantId tenantId) throws Exception { + EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, entityId, EdgeEventActionType.ADDED); + edgeEvent.setId(new EdgeEventId(Uuids.startOf(time))); + return edgeEventService.saveAsync(edgeEvent).get(); + } +} \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java new file mode 100644 index 0000000000..4725b52e6f --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java @@ -0,0 +1,598 @@ +/** + * Copyright © 2016-2021 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.service; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.CustomerId; +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.dao.exception.DataValidationException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; + +public abstract class BaseEdgeServiceTest extends AbstractServiceTest { + + private IdComparator idComparator = new IdComparator<>(); + + private TenantId tenantId; + + @Before + public void before() { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = tenantService.saveTenant(tenant); + Assert.assertNotNull(savedTenant); + tenantId = savedTenant.getId(); + } + + @After + public void after() { + tenantService.deleteTenant(tenantId); + } + + @Test + public void testSaveEdge() { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = edgeService.saveEdge(edge); + + Assert.assertNotNull(savedEdge); + Assert.assertNotNull(savedEdge.getId()); + Assert.assertTrue(savedEdge.getCreatedTime() > 0); + Assert.assertEquals(edge.getTenantId(), savedEdge.getTenantId()); + Assert.assertNotNull(savedEdge.getCustomerId()); + Assert.assertEquals(NULL_UUID, savedEdge.getCustomerId().getId()); + Assert.assertEquals(edge.getName(), savedEdge.getName()); + + savedEdge.setName("My new edge"); + + edgeService.saveEdge(savedEdge); + Edge foundEdge = edgeService.findEdgeById(tenantId, savedEdge.getId()); + Assert.assertEquals(foundEdge.getName(), savedEdge.getName()); + + edgeService.deleteEdge(tenantId, savedEdge.getId()); + } + + @Test(expected = DataValidationException.class) + public void testSaveEdgeWithEmptyName() { + Edge edge = new Edge(); + edge.setType("default"); + edge.setTenantId(tenantId); + edgeService.saveEdge(edge); + } + + @Test(expected = DataValidationException.class) + public void testSaveEdgeWithEmptyTenant() { + Edge edge = new Edge(); + edge.setName("My edge"); + edge.setType("default"); + edgeService.saveEdge(edge); + } + + @Test(expected = DataValidationException.class) + public void testSaveEdgeWithInvalidTenant() { + Edge edge = new Edge(); + edge.setName("My edge"); + edge.setType("default"); + edge.setTenantId(new TenantId(Uuids.timeBased())); + edgeService.saveEdge(edge); + } + + @Test(expected = DataValidationException.class) + public void testAssignEdgeToNonExistentCustomer() { + Edge edge = constructEdge("My edge", "default"); + edge = edgeService.saveEdge(edge); + try { + edgeService.assignEdgeToCustomer(tenantId, edge.getId(), new CustomerId(Uuids.timeBased())); + } finally { + edgeService.deleteEdge(tenantId, edge.getId()); + } + } + + @Test(expected = DataValidationException.class) + public void testAssignEdgeToCustomerFromDifferentTenant() { + Edge edge = constructEdge("My edge", "default"); + edge = edgeService.saveEdge(edge); + Tenant tenant = new Tenant(); + tenant.setTitle("Test different tenant"); + tenant = tenantService.saveTenant(tenant); + Customer customer = new Customer(); + customer.setTenantId(tenant.getId()); + customer.setTitle("Test different customer"); + customer = customerService.saveCustomer(customer); + try { + edgeService.assignEdgeToCustomer(tenantId, edge.getId(), customer.getId()); + } finally { + edgeService.deleteEdge(tenantId, edge.getId()); + tenantService.deleteTenant(tenant.getId()); + } + } + + @Test + public void testFindEdgeById() { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = edgeService.saveEdge(edge); + Edge foundEdge = edgeService.findEdgeById(tenantId, savedEdge.getId()); + Assert.assertNotNull(foundEdge); + Assert.assertEquals(savedEdge, foundEdge); + edgeService.deleteEdge(tenantId, savedEdge.getId()); + } + + @Test + public void testFindEdgeTypesByTenantId() throws Exception { + List edges = new ArrayList<>(); + try { + for (int i = 0; i < 3; i++) { + Edge edge = constructEdge("My edge B" + i, "typeB"); + edges.add(edgeService.saveEdge(edge)); + } + for (int i = 0; i < 7; i++) { + Edge edge = constructEdge("My edge C" + i, "typeC"); + edges.add(edgeService.saveEdge(edge)); + } + for (int i = 0; i < 9; i++) { + Edge edge = constructEdge("My edge A" + i, "typeA"); + edges.add(edgeService.saveEdge(edge)); + } + List edgeTypes = edgeService.findEdgeTypesByTenantId(tenantId).get(); + Assert.assertNotNull(edgeTypes); + Assert.assertEquals(3, edgeTypes.size()); + Assert.assertEquals("typeA", edgeTypes.get(0).getType()); + Assert.assertEquals("typeB", edgeTypes.get(1).getType()); + Assert.assertEquals("typeC", edgeTypes.get(2).getType()); + } finally { + edges.forEach((edge) -> { + edgeService.deleteEdge(tenantId, edge.getId()); + }); + } + } + + @Test + public void testDeleteEdge() { + Edge edge = constructEdge("My edge", "default"); + Edge savedEdge = edgeService.saveEdge(edge); + Edge foundEdge = edgeService.findEdgeById(tenantId, savedEdge.getId()); + Assert.assertNotNull(foundEdge); + edgeService.deleteEdge(tenantId, savedEdge.getId()); + foundEdge = edgeService.findEdgeById(tenantId, savedEdge.getId()); + Assert.assertNull(foundEdge); + } + + @Test + public void testFindEdgesByTenantId() { + Tenant tenant = new Tenant(); + tenant.setTitle("Test tenant"); + tenant = tenantService.saveTenant(tenant); + + TenantId tenantId = tenant.getId(); + + List edges = new ArrayList<>(); + for (int i = 0; i < 178; i++) { + Edge edge = constructEdge(tenantId, "Edge " + i, "default"); + edges.add(edgeService.saveEdge(edge)); + } + + List loadedEdges = new ArrayList<>(); + PageLink pageLink = new PageLink(23); + PageData pageData = null; + do { + pageData = edgeService.findEdgesByTenantId(tenantId, pageLink); + loadedEdges.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edges, idComparator); + Collections.sort(loadedEdges, idComparator); + + Assert.assertEquals(edges, loadedEdges); + + edgeService.deleteEdgesByTenantId(tenantId); + + pageLink = new PageLink(33); + pageData = edgeService.findEdgesByTenantId(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + + tenantService.deleteTenant(tenantId); + } + + @Test + public void testFindEdgesByTenantIdAndName() { + String title1 = "Edge title 1"; + List edgesTitle1 = new ArrayList<>(); + for (int i = 0; i < 143; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title1 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, "default"); + edgesTitle1.add(edgeService.saveEdge(edge)); + } + String title2 = "Edge title 2"; + List edgesTitle2 = new ArrayList<>(); + for (int i = 0; i < 175; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title2 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, "default"); + edgesTitle2.add(edgeService.saveEdge(edge)); + } + + List loadedEdgesTitle1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; + do { + pageData = edgeService.findEdgesByTenantId(tenantId, pageLink); + loadedEdgesTitle1.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesTitle1, idComparator); + Collections.sort(loadedEdgesTitle1, idComparator); + + Assert.assertEquals(edgesTitle1, loadedEdgesTitle1); + + List loadedEdgesTitle2 = new ArrayList<>(); + pageLink = new PageLink(4, 0, title2); + do { + pageData = edgeService.findEdgesByTenantId(tenantId, pageLink); + loadedEdgesTitle2.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesTitle2, idComparator); + Collections.sort(loadedEdgesTitle2, idComparator); + + Assert.assertEquals(edgesTitle2, loadedEdgesTitle2); + + for (Edge edge : loadedEdgesTitle1) { + edgeService.deleteEdge(tenantId, edge.getId()); + } + + pageLink = new PageLink(4, 0, title1); + pageData = edgeService.findEdgesByTenantId(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + + for (Edge edge : loadedEdgesTitle2) { + edgeService.deleteEdge(tenantId, edge.getId()); + } + + pageLink = new PageLink(4, 0, title2); + pageData = edgeService.findEdgesByTenantId(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + } + + @Test + public void testFindEdgesByTenantIdAndType() { + String title1 = "Edge title 1"; + String type1 = "typeA"; + List edgesType1 = new ArrayList<>(); + for (int i = 0; i < 143; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title1 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, type1); + edgesType1.add(edgeService.saveEdge(edge)); + } + String title2 = "Edge title 2"; + String type2 = "typeB"; + List edgesType2 = new ArrayList<>(); + for (int i = 0; i < 175; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title2 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, type2); + edgesType2.add(edgeService.saveEdge(edge)); + } + + List loadedEdgesType1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15, 0 , title1); + PageData pageData = null; + do { + pageData = edgeService.findEdgesByTenantIdAndType(tenantId, type1, pageLink); + loadedEdgesType1.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesType1, idComparator); + Collections.sort(loadedEdgesType1, idComparator); + + Assert.assertEquals(edgesType1, loadedEdgesType1); + + List loadedEdgesType2 = new ArrayList<>(); + pageLink = new PageLink(4, 0, title2); + do { + pageData = edgeService.findEdgesByTenantIdAndType(tenantId, type2, pageLink); + loadedEdgesType2.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesType2, idComparator); + Collections.sort(loadedEdgesType2, idComparator); + + Assert.assertEquals(edgesType2, loadedEdgesType2); + + for (Edge edge : loadedEdgesType1) { + edgeService.deleteEdge(tenantId, edge.getId()); + } + + pageLink = new PageLink(4, 0, title1); + pageData = edgeService.findEdgesByTenantIdAndType(tenantId, type1, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + + for (Edge edge : loadedEdgesType2) { + edgeService.deleteEdge(tenantId, edge.getId()); + } + + pageLink = new PageLink(4, 0, title2); + pageData = edgeService.findEdgesByTenantIdAndType(tenantId, type2, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + } + + @Test + public void testFindEdgesByTenantIdAndCustomerId() { + Tenant tenant = new Tenant(); + tenant.setTitle("Test tenant"); + tenant = tenantService.saveTenant(tenant); + + TenantId tenantId = tenant.getId(); + + Customer customer = new Customer(); + customer.setTitle("Test customer"); + customer.setTenantId(tenantId); + customer = customerService.saveCustomer(customer); + CustomerId customerId = customer.getId(); + + List edges = new ArrayList<>(); + for (int i = 0; i < 278; i++) { + Edge edge = constructEdge(tenantId, "Edge" + i, "default"); + edge = edgeService.saveEdge(edge); + edges.add(edgeService.assignEdgeToCustomer(tenantId, edge.getId(), customerId)); + } + + List loadedEdges = new ArrayList<>(); + PageLink pageLink = new PageLink(23); + PageData pageData = null; + do { + pageData = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + loadedEdges.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edges, idComparator); + Collections.sort(loadedEdges, idComparator); + + Assert.assertEquals(edges, loadedEdges); + + edgeService.unassignCustomerEdges(tenantId, customerId); + + pageLink = new PageLink(33); + pageData = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + + tenantService.deleteTenant(tenantId); + } + + @Test + public void testFindEdgesByTenantIdCustomerIdAndName() { + + Customer customer = new Customer(); + customer.setTitle("Test customer"); + customer.setTenantId(tenantId); + customer = customerService.saveCustomer(customer); + CustomerId customerId = customer.getId(); + + String title1 = "Edge title 1"; + List edgesTitle1 = new ArrayList<>(); + for (int i = 0; i < 175; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title1 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, "default"); + edge = edgeService.saveEdge(edge); + edgesTitle1.add(edgeService.assignEdgeToCustomer(tenantId, edge.getId(), customerId)); + } + String title2 = "Edge title 2"; + List edgesTitle2 = new ArrayList<>(); + for (int i = 0; i < 143; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title2 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, "default"); + edge = edgeService.saveEdge(edge); + edgesTitle2.add(edgeService.assignEdgeToCustomer(tenantId, edge.getId(), customerId)); + } + + List loadedEdgesTitle1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15, 0, title1); + PageData pageData = null; + do { + pageData = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + loadedEdgesTitle1.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesTitle1, idComparator); + Collections.sort(loadedEdgesTitle1, idComparator); + + Assert.assertEquals(edgesTitle1, loadedEdgesTitle1); + + List loadedEdgesTitle2 = new ArrayList<>(); + pageLink = new PageLink(4, 0, title2); + do { + pageData = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + loadedEdgesTitle2.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesTitle2, idComparator); + Collections.sort(loadedEdgesTitle2, idComparator); + + Assert.assertEquals(edgesTitle2, loadedEdgesTitle2); + + for (Edge edge : loadedEdgesTitle1) { + edgeService.deleteEdge(tenantId, edge.getId()); + } + + pageLink = new PageLink(4, 0, title1); + pageData = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + + for (Edge edge : loadedEdgesTitle2) { + edgeService.deleteEdge(tenantId, edge.getId()); + } + + pageLink = new PageLink(4, 0, title2); + pageData = edgeService.findEdgesByTenantIdAndCustomerId(tenantId, customerId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + customerService.deleteCustomer(tenantId, customerId); + } + + @Test + public void testFindEdgesByTenantIdCustomerIdAndType() { + + Customer customer = new Customer(); + customer.setTitle("Test customer"); + customer.setTenantId(tenantId); + customer = customerService.saveCustomer(customer); + CustomerId customerId = customer.getId(); + + String title1 = "Edge title 1"; + String type1 = "typeC"; + List edgesType1 = new ArrayList<>(); + for (int i = 0; i < 175; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title1 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, type1); + edge = edgeService.saveEdge(edge); + edgesType1.add(edgeService.assignEdgeToCustomer(tenantId, edge.getId(), customerId)); + } + String title2 = "Edge title 2"; + String type2 = "typeD"; + List edgesType2 = new ArrayList<>(); + for (int i = 0; i < 143; i++) { + String suffix = RandomStringUtils.randomAlphanumeric(15); + String name = title2 + suffix; + name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); + Edge edge = constructEdge(name, type2); + edge = edgeService.saveEdge(edge); + edgesType2.add(edgeService.assignEdgeToCustomer(tenantId, edge.getId(), customerId)); + } + + List loadedEdgesType1 = new ArrayList<>(); + PageLink pageLink = new PageLink(15); + PageData pageData = null; + do { + pageData = edgeService.findEdgesByTenantIdAndCustomerIdAndType(tenantId, customerId, type1, pageLink); + loadedEdgesType1.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesType1, idComparator); + Collections.sort(loadedEdgesType1, idComparator); + + Assert.assertEquals(edgesType1, loadedEdgesType1); + + List loadedEdgesType2 = new ArrayList<>(); + pageLink = new PageLink(4); + do { + pageData = edgeService.findEdgesByTenantIdAndCustomerIdAndType(tenantId, customerId, type2, pageLink); + loadedEdgesType2.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(edgesType2, idComparator); + Collections.sort(loadedEdgesType2, idComparator); + + Assert.assertEquals(edgesType2, loadedEdgesType2); + + for (Edge edge : loadedEdgesType1) { + edgeService.deleteEdge(tenantId, edge.getId()); + } + + pageLink = new PageLink(4); + pageData = edgeService.findEdgesByTenantIdAndCustomerIdAndType(tenantId, customerId, type1, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + + for (Edge edge : loadedEdgesType2) { + edgeService.deleteEdge(tenantId, edge.getId()); + } + + pageLink = new PageLink(4); + pageData = edgeService.findEdgesByTenantIdAndCustomerIdAndType(tenantId, customerId, type2, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(0, pageData.getData().size()); + customerService.deleteCustomer(tenantId, customerId); + } + + private Edge constructEdge(String name, String type) { + return constructEdge(tenantId, name, type); + } + + private Edge constructEdge(TenantId tenantId, String name, String type) { + Edge edge = new Edge(); + edge.setTenantId(tenantId); + edge.setName(name); + edge.setType(type); + edge.setSecret(RandomStringUtils.randomAlphanumeric(20)); + edge.setRoutingKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setEdgeLicenseKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setCloudEndpoint("http://localhost:8080"); + return edge; + } + +} \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java index 6b7530c4c3..e16fc3dac7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.service; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomUtils; import org.junit.After; import org.junit.Assert; @@ -29,8 +30,10 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @@ -44,6 +47,8 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AssetSearchQueryFilter; import org.thingsboard.server.common.data.query.DeviceSearchQueryFilter; import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.common.data.query.EdgeSearchQueryFilter; +import org.thingsboard.server.common.data.query.EdgeTypeFilter; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataPageLink; @@ -214,7 +219,89 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { Assert.assertEquals(0, count); } - + @Test + public void testCountEdgeEntitiesByQuery() throws InterruptedException { + List edges = new ArrayList<>(); + for (int i = 0; i < 97; i++) { + Edge edge = createEdge(i, "default"); + edges.add(edgeService.saveEdge(edge)); + } + + EdgeTypeFilter filter = new EdgeTypeFilter(); + filter.setEdgeType("default"); + filter.setEdgeNameFilter(""); + + EntityCountQuery countQuery = new EntityCountQuery(filter); + + long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(97, count); + + filter.setEdgeType("unknown"); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(0, count); + + filter.setEdgeType("default"); + filter.setEdgeNameFilter("Edge1"); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(11, count); + + EntityListFilter entityListFilter = new EntityListFilter(); + entityListFilter.setEntityType(EntityType.EDGE); + entityListFilter.setEntityList(edges.stream().map(Edge::getId).map(EdgeId::toString).collect(Collectors.toList())); + + countQuery = new EntityCountQuery(entityListFilter); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(97, count); + + edgeService.deleteEdgesByTenantId(tenantId); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(0, count); + } + + @Test + public void testCountHierarchicalEntitiesByEdgeSearchQuery() throws InterruptedException { + for (int i = 0; i < 5; i++) { + Edge edge = createEdge(i, "type" + i); + edge = edgeService.saveEdge(edge); + //TO make sure devices have different created time + Thread.sleep(1); + + EntityRelation er = new EntityRelation(); + er.setFrom(tenantId); + er.setTo(edge.getId()); + er.setType("Manages"); + er.setTypeGroup(RelationTypeGroup.COMMON); + relationService.saveRelation(tenantId, er); + } + + EdgeSearchQueryFilter filter = new EdgeSearchQueryFilter(); + filter.setRootEntity(tenantId); + filter.setDirection(EntitySearchDirection.FROM); + filter.setRelationType("Manages"); + + EntityCountQuery countQuery = new EntityCountQuery(filter); + + long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(5, count); + + filter.setEdgeTypes(Arrays.asList("type0", "type1")); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(2, count); + } + + private Edge createEdge(int i, String type) { + Edge edge = new Edge(); + edge.setTenantId(tenantId); + edge.setName("Edge" + i); + edge.setType(type); + edge.setLabel("EdgeLabel" + i); + edge.setSecret(RandomStringUtils.randomAlphanumeric(20)); + edge.setRoutingKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setEdgeLicenseKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setCloudEndpoint("http://localhost:8080"); + return edge; + } + @Test public void testHierarchicalFindEntityDataWithAttributesByQuery() throws ExecutionException, InterruptedException { List assets = new ArrayList<>(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java index b0cb197c17..3f4b103ce2 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java @@ -23,12 +23,14 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.RuleChainId; 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.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.exception.DataValidationException; @@ -143,7 +145,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { PageLink pageLink = new PageLink(16); PageData pageData = null; do { - pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); + pageData = ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, pageLink); loadedRuleChains.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); @@ -158,7 +160,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { ruleChainService.deleteRuleChainsByTenantId(tenantId); pageLink = new PageLink(31); - pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); + pageData = ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); @@ -194,7 +196,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { PageLink pageLink = new PageLink(19, 0, name1); PageData pageData = null; do { - pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); + pageData = ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, pageLink); loadedRuleChainsName1.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); @@ -209,7 +211,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { List loadedRuleChainsName2 = new ArrayList<>(); pageLink = new PageLink(4, 0, name2); do { - pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); + pageData = ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, pageLink); loadedRuleChainsName2.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); @@ -226,7 +228,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { } pageLink = new PageLink(4, 0, name1); - pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); + pageData = ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); @@ -235,7 +237,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { } pageLink = new PageLink(4, 0, name2); - pageData = ruleChainService.findTenantRuleChains(tenantId, pageLink); + pageData = ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } @@ -290,7 +292,8 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { ruleNodes.set(name3Index, ruleNode4); - RuleChainMetaData updatedRuleChainMetaData = ruleChainService.saveRuleChainMetaData(tenantId, savedRuleChainMetaData); + Assert.assertTrue(ruleChainService.saveRuleChainMetaData(tenantId, savedRuleChainMetaData)); + RuleChainMetaData updatedRuleChainMetaData = ruleChainService.loadRuleChainMetaData(tenantId, savedRuleChainMetaData.getRuleChainId()); Assert.assertEquals(3, updatedRuleChainMetaData.getNodes().size()); Assert.assertEquals(3, updatedRuleChainMetaData.getConnections().size()); @@ -327,6 +330,44 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { ruleChainService.saveRuleChainMetaData(tenantId, createRuleChainMetadataWithCirclingRelation2()); } + @Test + public void testGetDefaultEdgeRuleChains() throws Exception { + RuleChainId ruleChainId = saveRuleChainAndSetAutoAssignToEdge("Default Edge Rule Chain 1"); + saveRuleChainAndSetAutoAssignToEdge("Default Edge Rule Chain 2"); + List result = ruleChainService.findAutoAssignToEdgeRuleChainsByTenantId(tenantId).get(); + Assert.assertEquals(2, result.size()); + + ruleChainService.unsetAutoAssignToEdgeRuleChain(tenantId, ruleChainId); + + result = ruleChainService.findAutoAssignToEdgeRuleChainsByTenantId(tenantId).get(); + Assert.assertEquals(1, result.size()); + } + + @Test + public void setEdgeTemplateRootRuleChain() throws Exception { + RuleChainId ruleChainId1 = saveRuleChainAndSetAutoAssignToEdge("Default Edge Rule Chain 1"); + RuleChainId ruleChainId2 = saveRuleChainAndSetAutoAssignToEdge("Default Edge Rule Chain 2"); + + ruleChainService.setEdgeTemplateRootRuleChain(tenantId, ruleChainId1); + ruleChainService.setEdgeTemplateRootRuleChain(tenantId, ruleChainId2); + + RuleChain ruleChainById = ruleChainService.findRuleChainById(tenantId, ruleChainId1); + Assert.assertFalse(ruleChainById.isRoot()); + + ruleChainById = ruleChainService.findRuleChainById(tenantId, ruleChainId2); + Assert.assertTrue(ruleChainById.isRoot()); + } + + private RuleChainId saveRuleChainAndSetAutoAssignToEdge(String name) { + RuleChain edgeRuleChain = new RuleChain(); + edgeRuleChain.setTenantId(tenantId); + edgeRuleChain.setType(RuleChainType.EDGE); + edgeRuleChain.setName(name); + RuleChain savedEdgeRuleChain = ruleChainService.saveRuleChain(edgeRuleChain); + ruleChainService.setAutoAssignToEdgeRuleChain(tenantId, savedEdgeRuleChain.getId()); + return savedEdgeRuleChain.getId(); + } + private RuleChainMetaData createRuleChainMetadata() throws Exception { RuleChain ruleChain = new RuleChain(); ruleChain.setName("My RuleChain"); @@ -364,7 +405,8 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { ruleChainMetaData.addConnectionInfo(0,2,"fail"); ruleChainMetaData.addConnectionInfo(1,2,"success"); - return ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData); + Assert.assertTrue(ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData)); + return ruleChainService.loadRuleChainMetaData(tenantId, ruleChainMetaData.getRuleChainId()); } private RuleChainMetaData createRuleChainMetadataWithCirclingRelation() throws Exception { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/TbResourceServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/EdgeEventServiceSqlTest.java similarity index 83% rename from dao/src/test/java/org/thingsboard/server/dao/service/sql/TbResourceServiceSqlTest.java rename to dao/src/test/java/org/thingsboard/server/dao/service/sql/EdgeEventServiceSqlTest.java index 2af4881c84..4e772917b5 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/TbResourceServiceSqlTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/EdgeEventServiceSqlTest.java @@ -15,9 +15,9 @@ */ package org.thingsboard.server.dao.service.sql; -import org.thingsboard.server.dao.service.BaseTbResourceServiceTest; +import org.thingsboard.server.dao.service.BaseEdgeEventServiceTest; import org.thingsboard.server.dao.service.DaoSqlTest; @DaoSqlTest -public class TbResourceServiceSqlTest extends BaseTbResourceServiceTest { +public class EdgeEventServiceSqlTest extends BaseEdgeEventServiceTest { } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/EdgeServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/EdgeServiceSqlTest.java new file mode 100644 index 0000000000..f522103814 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/EdgeServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2021 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.service.sql; + +import org.thingsboard.server.dao.service.BaseEdgeServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class EdgeServiceSqlTest extends BaseEdgeServiceTest { +} diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 6760f96631..2805c78fdd 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -39,6 +39,10 @@ caffeine.specs.deviceProfiles.maxSize=100000 caffeine.specs.firmwares.timeToLiveInMinutes=1440 caffeine.specs.firmwares.maxSize=100000 +caffeine.specs.edges.timeToLiveInMinutes=1440 +caffeine.specs.edges.maxSize=100000 + + redis.connection.host=localhost redis.connection.port=6379 redis.connection.db=0 @@ -51,3 +55,5 @@ security.claim.duration=60000 database.ts_max_intervals=700 sql.remove_null_chars=true + +edges.enabled=true diff --git a/dao/src/test/resources/sql/hsql/drop-all-tables.sql b/dao/src/test/resources/sql/hsql/drop-all-tables.sql index 8297553ea6..09b7221cf4 100644 --- a/dao/src/test/resources/sql/hsql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/hsql/drop-all-tables.sql @@ -30,4 +30,6 @@ DROP TABLE IF EXISTS oauth2_client_registration_template; DROP TABLE IF EXISTS api_usage_state; DROP TABLE IF EXISTS resource; DROP TABLE IF EXISTS firmware; +DROP TABLE IF EXISTS edge; +DROP TABLE IF EXISTS edge_event; DROP FUNCTION IF EXISTS to_uuid; diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index fbc0c76391..57661fb036 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -31,3 +31,5 @@ DROP TABLE IF EXISTS oauth2_client_registration_template; DROP TABLE IF EXISTS api_usage_state; DROP TABLE IF EXISTS resource; DROP TABLE IF EXISTS firmware; +DROP TABLE IF EXISTS edge; +DROP TABLE IF EXISTS edge_event; diff --git a/dao/src/test/resources/sql/timescale/drop-all-tables.sql b/dao/src/test/resources/sql/timescale/drop-all-tables.sql index 950d3b2722..430635a758 100644 --- a/dao/src/test/resources/sql/timescale/drop-all-tables.sql +++ b/dao/src/test/resources/sql/timescale/drop-all-tables.sql @@ -24,10 +24,12 @@ DROP TABLE IF EXISTS rule_chain; DROP TABLE IF EXISTS entity_view; DROP TABLE IF EXISTS device_profile; DROP TABLE IF EXISTS tenant_profile; +DROP TABLE IF EXISTS edge; +DROP TABLE IF EXISTS edge_event; DROP TABLE IF EXISTS tb_schema_settings; DROP TABLE IF EXISTS oauth2_client_registration; DROP TABLE IF EXISTS oauth2_client_registration_info; DROP TABLE IF EXISTS oauth2_client_registration_template; DROP TABLE IF EXISTS api_usage_state; DROP TABLE IF EXISTS resource; -DROP TABLE IF EXISTS firmware; \ No newline at end of file +DROP TABLE IF EXISTS firmware; diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 1f2047b052..d2440329ed 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -42,6 +42,7 @@ services: image: "${DOCKER_REPO}/${TB_NODE_DOCKER_NAME}:${TB_VERSION}" ports: - "8080" + - "7070" logging: driver: "json-file" options: @@ -50,6 +51,7 @@ services: environment: TB_SERVICE_ID: tb-core1 TB_SERVICE_TYPE: tb-core + EDGES_ENABLED: "true" env_file: - tb-node.env volumes: @@ -66,6 +68,7 @@ services: image: "${DOCKER_REPO}/${TB_NODE_DOCKER_NAME}:${TB_VERSION}" ports: - "8080" + - "7070" logging: driver: "json-file" options: @@ -74,6 +77,7 @@ services: environment: TB_SERVICE_ID: tb-core2 TB_SERVICE_TYPE: tb-core + EDGES_ENABLED: "true" env_file: - tb-node.env volumes: @@ -239,6 +243,7 @@ services: - "80:80" - "443:443" - "1883:1883" + - "7070:7070" - "9999:9999" cap_add: - NET_ADMIN @@ -246,6 +251,7 @@ services: HTTP_PORT: 80 HTTPS_PORT: 443 MQTT_PORT: 1883 + EDGES_RPC_PORT: 7070 FORCE_HTTPS_REDIRECT: "false" links: - tb-core1 diff --git a/docker/haproxy/config/haproxy.cfg b/docker/haproxy/config/haproxy.cfg index 2a457a0696..50dcf36434 100644 --- a/docker/haproxy/config/haproxy.cfg +++ b/docker/haproxy/config/haproxy.cfg @@ -49,8 +49,19 @@ listen mqtt-in server tbMqtt1 tb-mqtt-transport1:1883 check inter 5s resolvers docker_resolver resolve-prefer ipv4 server tbMqtt2 tb-mqtt-transport2:1883 check inter 5s resolvers docker_resolver resolve-prefer ipv4 +listen edges-rpc-in + bind *:${EDGES_RPC_PORT} + mode tcp + option clitcpka # For TCP keep-alive + timeout client 3h + timeout server 3h + option tcplog + balance leastconn + server tbEdgesRpc1 tb-core1:7070 check inter 5s resolvers docker_resolver resolve-prefer ipv4 + server tbEdgesRpc2 tb-core2:7070 check inter 5s resolvers docker_resolver resolve-prefer ipv4 + frontend http-in - bind *:${HTTP_PORT} + bind *:${HTTP_PORT} alpn h2,http/1.1 option forwardfor @@ -69,7 +80,7 @@ frontend http-in default_backend tb-web-backend frontend https_in - bind *:${HTTPS_PORT} ssl crt /usr/local/etc/haproxy/default.pem crt /usr/local/etc/haproxy/certs.d ciphers ECDHE-RSA-AES256-SHA:RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM + bind *:${HTTPS_PORT} ssl crt /usr/local/etc/haproxy/default.pem crt /usr/local/etc/haproxy/certs.d ciphers ECDHE-RSA-AES256-SHA:RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM alpn h2,http/1.1 option forwardfor diff --git a/msa/black-box-tests/README.md b/msa/black-box-tests/README.md index 044fe00255..98718a201b 100644 --- a/msa/black-box-tests/README.md +++ b/msa/black-box-tests/README.md @@ -10,6 +10,7 @@ To run the black box tests with using Docker, the local Docker images of Thingsb As result, in REPOSITORY column, next images should be present: thingsboard/tb-coap-transport + thingsboard/tb-lwm2m-transport thingsboard/tb-http-transport thingsboard/tb-mqtt-transport thingsboard/tb-node diff --git a/msa/tb/docker-cassandra/Dockerfile b/msa/tb/docker-cassandra/Dockerfile index f9eddd420c..ec92116c81 100644 --- a/msa/tb/docker-cassandra/Dockerfile +++ b/msa/tb/docker-cassandra/Dockerfile @@ -17,15 +17,14 @@ FROM thingsboard/openjdk11 RUN apt-get update -RUN apt-get install -y curl nmap procps -RUN echo 'deb http://ftp.us.debian.org/debian sid main' | tee --append /etc/apt/sources.list.d/debian.list > /dev/null -RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ sid-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null +RUN apt-get install -y curl nmap procps gnupg2 +RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - RUN echo 'deb http://downloads.apache.org/cassandra/debian 40x main' | tee --append /etc/apt/sources.list.d/cassandra.list > /dev/null RUN curl -L https://downloads.apache.org/cassandra/KEYS | apt-key add - -ENV PG_MAJOR=11 +ENV PG_MAJOR=12 RUN apt-get update -RUN apt-get install -y cassandra cassandra-tools postgresql-11 +RUN apt-get install -y cassandra cassandra-tools postgresql-12 RUN update-rc.d cassandra disable RUN update-rc.d postgresql disable RUN sed -i.old '/ulimit/d' /etc/init.d/cassandra diff --git a/msa/tb/docker-postgres/Dockerfile b/msa/tb/docker-postgres/Dockerfile index 9b314a5a5d..7221bc08c6 100644 --- a/msa/tb/docker-postgres/Dockerfile +++ b/msa/tb/docker-postgres/Dockerfile @@ -17,13 +17,12 @@ FROM thingsboard/openjdk11 RUN apt-get update -RUN apt-get install -y curl -RUN echo 'deb http://ftp.us.debian.org/debian sid main' | tee --append /etc/apt/sources.list.d/debian.list > /dev/null -RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ sid-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null +RUN apt-get install -y curl gnupg2 +RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - -ENV PG_MAJOR 11 +ENV PG_MAJOR 12 RUN apt-get update -RUN apt-get install -y postgresql-11 +RUN apt-get install -y postgresql-12 RUN update-rc.d postgresql disable COPY logback.xml ${pkg.name}.conf start-db.sh stop-db.sh start-tb.sh upgrade-tb.sh install-tb.sh ${pkg.name}.deb /tmp/ diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index efb9a709e9..a19c2c542f 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -39,7 +39,7 @@ tb-postgres tb-cassandra /usr/share/${pkg.name} - 2.4.2 + 3.3.0 diff --git a/pom.xml b/pom.xml index d635859b8a..a44ade17de 100755 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ 4.2.0 3.5.5 3.11.4 - 1.22.1 + 1.36.0 1.18.18 1.2.4 4.1.60.Final @@ -99,7 +99,7 @@ org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/* 5.0.2 - 0.1.31 + 0.1.16 2.6.0 4.1.1 2.57 @@ -918,6 +918,11 @@ lwm2m ${project.version} + + org.thingsboard.common + edge-api + ${project.version} + org.thingsboard dao @@ -933,6 +938,11 @@ stats ${project.version} + + org.thingsboard.common + coap-server + ${project.version} + org.thingsboard tools @@ -1291,6 +1301,11 @@ protobuf-java ${protobuf.version} + + com.google.protobuf + protobuf-java-util + ${protobuf.version} + io.grpc grpc-netty diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 2528e5f36c..e51117c080 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -63,6 +63,9 @@ import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.device.DeviceSearchQuery; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeSearchQuery; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; @@ -70,6 +73,7 @@ import org.thingsboard.server.common.data.id.CustomerId; 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.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; @@ -105,6 +109,7 @@ import org.thingsboard.server.common.data.rule.DefaultRuleChainCreateRequest; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainData; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.security.model.SecuritySettings; @@ -300,16 +305,24 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } public PageData getAlarms(EntityId entityId, AlarmSearchStatus searchStatus, AlarmStatus status, TimePageLink pageLink, Boolean fetchOriginator) { + String urlSecondPart = "/api/alarm/{entityType}/{entityId}?fetchOriginator={fetchOriginator}"; Map params = new HashMap<>(); params.put("entityType", entityId.getEntityType().name()); params.put("entityId", entityId.getId().toString()); - params.put("searchStatus", searchStatus.name()); - params.put("status", status.name()); params.put("fetchOriginator", String.valueOf(fetchOriginator)); + if(searchStatus != null) { + params.put("searchStatus", searchStatus.name()); + urlSecondPart += "&searchStatus={searchStatus}"; + } + if(status != null) { + params.put("status", status.name()); + urlSecondPart += "&status={status}"; + } + addTimePageLinkToParam(params, pageLink); return restTemplate.exchange( - baseURL + "/api/alarm/{entityType}/{entityId}?searchStatus={searchStatus}&status={status}&fetchOriginator={fetchOriginator}&" + getTimeUrlParams(pageLink), + baseURL + urlSecondPart + getTimeUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -697,22 +710,31 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } public List getComponentDescriptorsByType(ComponentType componentType) { + return getComponentDescriptorsByType(componentType, RuleChainType.CORE); + } + + public List getComponentDescriptorsByType(ComponentType componentType, RuleChainType ruleChainType) { return restTemplate.exchange( - baseURL + "/api/components?componentType={componentType}", + baseURL + "/api/components/" + componentType.name() + "/?ruleChainType={ruleChainType}", HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { }, - componentType).getBody(); + ruleChainType).getBody(); } public List getComponentDescriptorsByTypes(List componentTypes) { + return getComponentDescriptorsByTypes(componentTypes, RuleChainType.CORE); + } + + public List getComponentDescriptorsByTypes(List componentTypes, RuleChainType ruleChainType) { return restTemplate.exchange( - baseURL + "/api/components?componentTypes={componentTypes}", + baseURL + "/api/components?componentTypes={componentTypes}&ruleChainType={ruleChainType}", HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { }, - listEnumToString(componentTypes)) + listEnumToString(componentTypes), + ruleChainType) .getBody(); } @@ -1323,7 +1345,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { HttpEntity.EMPTY, DeviceProfile.class, deviceProfileId).getBody(); } - public PageData getTenantDevices(PageLink pageLink) { + public PageData getDeviceProfiles(PageLink pageLink) { Map params = new HashMap<>(); addPageLinkToParam(params, pageLink); return restTemplate.exchange( @@ -2410,6 +2432,403 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } + public Edge saveEdge(Edge edge) { + return restTemplate.postForEntity(baseURL + "/api/edge", edge, Edge.class).getBody(); + } + + public void deleteEdge(EdgeId edgeId) { + restTemplate.delete(baseURL + "/api/edge/{edgeId}", edgeId.getId()); + } + + public Optional getEdgeById(EdgeId edgeId) { + try { + ResponseEntity edge = restTemplate.getForEntity(baseURL + "/api/edge/{edgeId}", Edge.class, edgeId.getId()); + return Optional.ofNullable(edge.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional assignEdgeToCustomer(CustomerId customerId, EdgeId edgeId) { + try { + ResponseEntity edge = restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/edge/{edgeId}", null, Edge.class, customerId.getId(), edgeId.getId()); + return Optional.ofNullable(edge.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional assignEdgeToPublicCustomer(EdgeId edgeId) { + try { + ResponseEntity edge = restTemplate.postForEntity(baseURL + "/api/customer/public/edge/{edgeId}", null, Edge.class, edgeId.getId()); + return Optional.ofNullable(edge.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional setRootRuleChain(EdgeId edgeId, RuleChainId ruleChainId) { + try { + ResponseEntity ruleChain = restTemplate.postForEntity(baseURL + "/api/edge/{edgeId}/{ruleChainId}/root", null, Edge.class, edgeId.getId(), ruleChainId.getId()); + return Optional.ofNullable(ruleChain.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getEdges(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/edges?" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Optional unassignEdgeFromCustomer(EdgeId edgeId) { + try { + ResponseEntity edge = restTemplate.exchange(baseURL + "/api/customer/edge/{edgeId}", HttpMethod.DELETE, HttpEntity.EMPTY, Edge.class, edgeId.getId()); + return Optional.ofNullable(edge.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional assignDeviceToEdge(EdgeId edgeId, DeviceId deviceId) { + try { + ResponseEntity device = restTemplate.postForEntity(baseURL + "/api/edge/{edgeId}/device/{deviceId}", null, Device.class, edgeId.getId(), deviceId.getId()); + return Optional.ofNullable(device.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional unassignDeviceFromEdge(EdgeId edgeId, DeviceId deviceId) { + try { + ResponseEntity device = restTemplate.exchange(baseURL + "/api/edge/{edgeId}/device/{deviceId}", HttpMethod.DELETE, HttpEntity.EMPTY, Device.class, edgeId.getId(), deviceId.getId()); + return Optional.ofNullable(device.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getEdgeDevices(EdgeId edgeId, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("edgeId", edgeId.getId().toString()); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/edge/{edgeId}/devices?" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Optional assignAssetToEdge(EdgeId edgeId, AssetId assetId) { + try { + ResponseEntity asset = restTemplate.postForEntity(baseURL + "/api/edge/{edgeId}/asset/{assetId}", null, Asset.class, edgeId.getId(), assetId.getId()); + return Optional.ofNullable(asset.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional unassignAssetFromEdge(EdgeId edgeId, AssetId assetId) { + try { + ResponseEntity asset = restTemplate.exchange(baseURL + "/api/edge/{edgeId}/asset/{assetId}", HttpMethod.DELETE, HttpEntity.EMPTY, Asset.class, edgeId.getId(), assetId.getId()); + return Optional.ofNullable(asset.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getEdgeAssets(EdgeId edgeId, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("edgeId", edgeId.getId().toString()); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/edge/{edgeId}/assets?" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Optional assignDashboardToEdge(EdgeId edgeId, DashboardId dashboardId) { + try { + ResponseEntity dashboard = restTemplate.postForEntity(baseURL + "/api/edge/{edgeId}/dashboard/{dashboardId}", null, Dashboard.class, edgeId.getId(), dashboardId.getId()); + return Optional.ofNullable(dashboard.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional unassignDashboardFromEdge(EdgeId edgeId, DashboardId dashboardId) { + try { + ResponseEntity dashboard = restTemplate.exchange(baseURL + "/api/edge/{edgeId}/dashboard/{dashboardId}", HttpMethod.DELETE, HttpEntity.EMPTY, Dashboard.class, edgeId.getId(), dashboardId.getId()); + return Optional.ofNullable(dashboard.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getEdgeDashboards(EdgeId edgeId, TimePageLink pageLink) { + Map params = new HashMap<>(); + params.put("edgeId", edgeId.getId().toString()); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/edge/{edgeId}/dashboards?" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Optional assignEntityViewToEdge(EdgeId edgeId, EntityViewId entityViewId) { + try { + ResponseEntity entityView = restTemplate.postForEntity(baseURL + "/api/edge/{edgeId}/entityView/{entityViewId}", null, EntityView.class, edgeId.getId(), entityViewId.getId()); + return Optional.ofNullable(entityView.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional unassignEntityViewFromEdge(EdgeId edgeId, EntityViewId entityViewId) { + try { + ResponseEntity entityView = restTemplate.exchange(baseURL + "/api/edge/{edgeId}/entityView/{entityViewId}", + HttpMethod.DELETE, HttpEntity.EMPTY, EntityView.class, edgeId.getId(), entityViewId.getId()); + return Optional.ofNullable(entityView.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getEdgeEntityViews(EdgeId edgeId, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("edgeId", edgeId.getId().toString()); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/edge/{edgeId}/entityViews?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Optional assignRuleChainToEdge(EdgeId edgeId, RuleChainId ruleChainId) { + try { + ResponseEntity ruleChain = restTemplate.postForEntity(baseURL + "/api/edge/{edgeId}/ruleChain/{ruleChainId}", null, RuleChain.class, edgeId.getId(), ruleChainId.getId()); + return Optional.ofNullable(ruleChain.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional unassignRuleChainFromEdge(EdgeId edgeId, RuleChainId ruleChainId) { + try { + ResponseEntity ruleChain = restTemplate.exchange(baseURL + "/api/edge/{edgeId}/ruleChain/{ruleChainId}", HttpMethod.DELETE, HttpEntity.EMPTY, RuleChain.class, edgeId.getId(), ruleChainId.getId()); + return Optional.ofNullable(ruleChain.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getEdgeRuleChains(EdgeId edgeId, TimePageLink pageLink) { + Map params = new HashMap<>(); + params.put("edgeId", edgeId.getId().toString()); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/edge/{edgeId}/ruleChains?" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Optional setAutoAssignToEdgeRuleChain(RuleChainId ruleChainId) { + try { + ResponseEntity ruleChain = restTemplate.postForEntity(baseURL + "/api/ruleChain/{ruleChainId}/autoAssignToEdge", null, RuleChain.class, ruleChainId.getId()); + return Optional.ofNullable(ruleChain.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional unsetAutoAssignToEdgeRuleChain(RuleChainId ruleChainId) { + try { + ResponseEntity ruleChain = restTemplate.exchange(baseURL + "/api/ruleChain/{ruleChainId}/autoAssignToEdge", HttpMethod.DELETE, HttpEntity.EMPTY, RuleChain.class, ruleChainId.getId()); + return Optional.ofNullable(ruleChain.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public List getAutoAssignToEdgeRuleChains() { + return restTemplate.exchange(baseURL + "/api/ruleChain/autoAssignToEdgeRuleChains", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public Optional setRootEdgeTemplateRuleChain(RuleChainId ruleChainId) { + try { + ResponseEntity ruleChain = restTemplate.postForEntity(baseURL + "/api/ruleChain/{ruleChainId}/edgeTemplateRoot", null, RuleChain.class, ruleChainId.getId()); + return Optional.ofNullable(ruleChain.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getTenantEdges(String type, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("type", type); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/tenant/edges?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Optional getTenantEdge(String edgeName) { + try { + ResponseEntity edge = restTemplate.getForEntity(baseURL + "/api/tenant/edges?edgeName={edgeName}", Edge.class, edgeName); + return Optional.ofNullable(edge.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getCustomerEdges(CustomerId customerId, PageLink pageLink, String edgeType) { + Map params = new HashMap<>(); + params.put("customerId", customerId.getId().toString()); + params.put("type", edgeType); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/customer/{customerId}/edges?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public List getEdgesByIds(List edgeIds) { + return restTemplate.exchange(baseURL + "/api/edges?edgeIds={edgeIds}", + HttpMethod.GET, + HttpEntity.EMPTY, new ParameterizedTypeReference>() { + }, listIdsToString(edgeIds)).getBody(); + } + + public List findByQuery(EdgeSearchQuery query) { + return restTemplate.exchange( + baseURL + "/api/edges", + HttpMethod.POST, + new HttpEntity<>(query), + new ParameterizedTypeReference>() { + }).getBody(); + } + + public List getEdgeTypes() { + return restTemplate.exchange( + baseURL + "/api/edge/types", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public PageData getEdgeEvents(EdgeId edgeId, TimePageLink pageLink) { + Map params = new HashMap<>(); + params.put("edgeId", edgeId.toString()); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/edge/{edgeId}/events?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public void syncEdge(EdgeId edgeId) { + Map params = new HashMap<>(); + params.put("edgeId", edgeId.toString()); + restTemplate.postForEntity(baseURL + "/api/edge/sync/{edgeId}", null, EdgeId.class, params); + } + @Deprecated public Optional getAttributes(String accessToken, String clientKeys, String sharedKeys) { Map params = new HashMap<>(); @@ -2429,7 +2848,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } private String getTimeUrlParams(TimePageLink pageLink) { - String urlParams = "limit={limit}&ascOrder={ascOrder}"; + String urlParams = getUrlParams(pageLink); if (pageLink.getStartTime() != null) { urlParams += "&startTime={startTime}"; } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java index d1e86c6d76..53b52491da 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java @@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.api; import org.thingsboard.server.common.data.plugin.ComponentScope; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.rule.RuleChainType; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -57,4 +58,6 @@ public @interface RuleNode { boolean customRelations() default false; + RuleChainType[] ruleChainTypes() default {RuleChainType.CORE, RuleChainType.EDGE}; + } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 5f182c2c34..e4a71ee33d 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -42,6 +43,8 @@ import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; 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.entityview.EntityViewService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; import org.thingsboard.server.dao.nosql.TbResultSetFuture; @@ -152,6 +155,8 @@ public interface TbContext { // TODO: Does this changes the message? TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, String action); + void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId); + /* * * METHODS TO PROCESS THE MESSAGES @@ -198,6 +203,10 @@ public interface TbContext { RuleEngineDeviceProfileCache getDeviceProfileCache(); + EdgeService getEdgeService(); + + EdgeEventService getEdgeEventService(); + ListeningExecutor getJsExecutor(); ListeningExecutor getMailExecutor(); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceEdgeUpdateMsg.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceEdgeUpdateMsg.java new file mode 100644 index 0000000000..3c7fc7a68c --- /dev/null +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceEdgeUpdateMsg.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2021 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.msg; + +import lombok.AllArgsConstructor; +import lombok.Data; +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.msg.MsgType; + +@Data +@AllArgsConstructor +public class DeviceEdgeUpdateMsg implements ToDeviceActorNotificationMsg { + + private final TenantId tenantId; + private final DeviceId deviceId; + private final EdgeId edgeId; + + @Override + public MsgType getMsgType() { + return MsgType.DEVICE_EDGE_UPDATE_TO_DEVICE_ACTOR_MSG; + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java index 12bbdf450d..861e2a3f0d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.page.PageData; @@ -49,6 +50,7 @@ import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.user.UserService; @@ -238,6 +240,13 @@ public abstract class TbAbstractRelationActionNode dashboardInfoTextPageData = dashboardService.findDashboardsByTenantId(ctx.getTenantId(), new PageLink(200, 0, entitykey.getEntityName())); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java index a3e0c430aa..117e5d3225 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java @@ -22,7 +22,12 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.id.*; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -67,6 +72,9 @@ public class TbAssignToCustomerNode extends TbAbstractCustomerActionNode relationTypes; + private AlarmSeverity notDynamicAlarmSeverity; + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + super.init(ctx, configuration); + if(!this.config.isDynamicSeverity()) { + this.notDynamicAlarmSeverity = EnumUtils.getEnum(AlarmSeverity.class, this.config.getSeverity()); + if(this.notDynamicAlarmSeverity == null) { + throw new TbNodeException("Incorrect Alarm Severity value: " + this.config.getSeverity()); + } + } + } + @Override protected TbCreateAlarmNodeConfiguration loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException { @@ -132,7 +147,7 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode { - private AlarmSeverity severity; + private String severity; private boolean propagate; private boolean useMessageAlarmData; + private boolean dynamicSeverity; private List relationTypes; @@ -40,10 +41,11 @@ public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigura "}\n" + "return details;"); configuration.setAlarmType("General Alarm"); - configuration.setSeverity(AlarmSeverity.CRITICAL); + configuration.setSeverity(AlarmSeverity.CRITICAL.name()); configuration.setPropagate(false); configuration.setUseMessageAlarmData(false); configuration.setRelationTypes(Collections.emptyList()); + configuration.setDynamicSeverity(false); return configuration; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java index 28872ede05..5c06183106 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -135,6 +136,8 @@ public class TbCreateRelationNode extends TbAbstractRelationActionNode processEdge(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) { + return Futures.transformAsync(ctx.getEdgeService().findEdgeByIdAsync(ctx.getTenantId(), new EdgeId(entityContainer.getEntityId().getId())), edge -> { + if (edge != null) { + return processSave(ctx, sdId, relationType); + } else { + return Futures.immediateFuture(true); + } + }, ctx.getDbCallbackExecutor()); + } + private ListenableFuture processDevice(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) { return Futures.transformAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), new DeviceId(entityContainer.getEntityId().getId())), device -> { if (device != null) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java index a3c1e9e730..2b2f0d76a4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java @@ -17,13 +17,17 @@ package org.thingsboard.rule.engine.action; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.ScriptEngine; +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.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import static org.thingsboard.common.util.DonAsynchron.withCallback; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; @Slf4j @RuleNode( @@ -38,7 +42,6 @@ import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; configDirective = "tbActionNodeLogConfig", icon = "menu" ) - public class TbLogNode implements TbNode { private TbLogNodeConfiguration config; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java index 2697655f67..ca98e104c5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java @@ -18,11 +18,14 @@ package org.thingsboard.rule.engine.action; import com.google.gson.Gson; import com.google.gson.JsonObject; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.rule.engine.api.*; +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.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.common.msg.session.SessionMsgType; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java index 384982427d..5cf1a3a7a5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java @@ -29,9 +29,14 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.rule.engine.api.*; +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.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.cassandra.guava.GuavaSession; @@ -62,7 +67,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; " otherwise, the message will be routed via success chain.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeCustomTableConfig", - icon = "file_upload") + icon = "file_upload", + ruleChainTypes = RuleChainType.CORE) public class TbSaveToCustomCassandraTableNode implements TbNode { private static final String TABLE_PREFIX = "cs_tb_"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java index 30e6afd723..5820759107 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java @@ -21,7 +21,12 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.id.*; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -60,6 +65,9 @@ public class TbUnassignFromCustomerNode extends TbAbstractCustomerActionNodeSupports next originator types:" + + "
DEVICE" + + "
ASSET" + + "
ENTITY_VIEW" + + "
DASHBOARD" + + "
TENANT" + + "
CUSTOMER" + + "
EDGE

" + + "As well node supports next message types:" + + "
POST_TELEMETRY_REQUEST" + + "
POST_ATTRIBUTES_REQUEST" + + "
ATTRIBUTES_UPDATED" + + "
ATTRIBUTES_DELETED" + + "
ALARM

" + + "Message will be routed via Failure route if node was not able to save cloud event to database or unsupported originator type/message type arrived. " + + "In case successful storage cloud event to database message will be routed via Success route.", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbNodeEmptyConfig", + icon = "cloud_upload", + ruleChainTypes = RuleChainType.EDGE +) +public class TbMsgPushToCloudNode implements TbNode { + + private EmptyNodeConfiguration config; + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class); + } + + @Override + public void onMsg(TbContext ctx, TbMsg msg) { + // Implementation of this node is done on the Edge + } + + @Override + public void destroy() { + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNode.java new file mode 100644 index 0000000000..e766c2600c --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNode.java @@ -0,0 +1,289 @@ +/** + * Copyright © 2016-2021 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.edge; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; +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.util.TbNodeUtils; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.session.SessionMsgType; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; + +@Slf4j +@RuleNode( + type = ComponentType.ACTION, + name = "push to edge", + configClazz = EmptyNodeConfiguration.class, + nodeDescription = "Push messages from cloud to edge", + nodeDetails = "Push messages from cloud to edge. " + + "Message originator must be assigned to particular edge or message originator is EDGE entity itself. " + + "This node used only on cloud instances to push messages from cloud to edge. " + + "Once message arrived into this node it’s going to be converted into edge event and saved to the database. " + + "Node doesn't push messages directly to edge, but stores event(s) in the edge queue. " + + "
Supports next originator types:" + + "
DEVICE" + + "
ASSET" + + "
ENTITY_VIEW" + + "
DASHBOARD" + + "
TENANT" + + "
CUSTOMER" + + "
EDGE

" + + "As well node supports next message types:" + + "
POST_TELEMETRY_REQUEST" + + "
POST_ATTRIBUTES_REQUEST" + + "
ATTRIBUTES_UPDATED" + + "
ATTRIBUTES_DELETED" + + "
ALARM

" + + "Message will be routed via Failure route if node was not able to save edge event to database or unsupported originator type/message type arrived. " + + "In case successful storage edge event to database message will be routed via Success route.", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbNodeEmptyConfig", + icon = "cloud_download", + ruleChainTypes = RuleChainType.CORE +) +public class TbMsgPushToEdgeNode implements TbNode { + + private EmptyNodeConfiguration config; + + private static final String SCOPE = "scope"; + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class); + } + + @Override + public void onMsg(TbContext ctx, TbMsg msg) { + if (DataConstants.EDGE_MSG_SOURCE.equalsIgnoreCase(msg.getMetaData().getValue(DataConstants.MSG_SOURCE_KEY))) { + log.debug("Ignoring msg from the cloud, msg [{}]", msg); + ctx.ack(msg); + return; + } + if (isSupportedOriginator(msg.getOriginator().getEntityType())) { + if (isSupportedMsgType(msg.getType())) { + processMsg(ctx, msg); + } else { + log.debug("Unsupported msg type {}", msg.getType()); + ctx.tellFailure(msg, new RuntimeException("Unsupported msg type '" + msg.getType() + "'")); + } + } else { + log.debug("Unsupported originator type {}", msg.getOriginator().getEntityType()); + ctx.tellFailure(msg, new RuntimeException("Unsupported originator type '" + msg.getOriginator().getEntityType() + "'")); + } + } + + private void processMsg(TbContext ctx, TbMsg msg) { + if (EntityType.EDGE.equals(msg.getOriginator().getEntityType())) { + try { + EdgeEvent edgeEvent = buildEdgeEvent(msg, ctx); + if (edgeEvent != null) { + EdgeId edgeId = new EdgeId(msg.getOriginator().getId()); + edgeEvent.setEdgeId(edgeId); + ListenableFuture saveFuture = ctx.getEdgeEventService().saveAsync(edgeEvent); + Futures.addCallback(saveFuture, new FutureCallback() { + @Override + public void onSuccess(@Nullable EdgeEvent event) { + ctx.tellNext(msg, SUCCESS); + ctx.onEdgeEventUpdate(ctx.getTenantId(), edgeId); + } + + @Override + public void onFailure(Throwable th) { + log.warn("[{}] Can't save edge event [{}] for edge [{}]", ctx.getTenantId().getId(), edgeEvent, edgeId.getId(), th); + ctx.tellFailure(msg, th); + } + }, ctx.getDbCallbackExecutor()); + } + } catch (JsonProcessingException e) { + log.error("Failed to build edge event", e); + ctx.tellFailure(msg, e); + } + } else { + ListenableFuture> getEdgeIdsFuture = ctx.getEdgeService().findRelatedEdgeIdsByEntityId(ctx.getTenantId(), msg.getOriginator()); + Futures.addCallback(getEdgeIdsFuture, new FutureCallback>() { + @Override + public void onSuccess(@Nullable List edgeIds) { + if (edgeIds != null && !edgeIds.isEmpty()) { + for (EdgeId edgeId : edgeIds) { + try { + EdgeEvent edgeEvent = buildEdgeEvent(msg, ctx); + if (edgeEvent == null) { + log.debug("Edge event type is null. Entity Type {}", msg.getOriginator().getEntityType()); + ctx.tellFailure(msg, new RuntimeException("Edge event type is null. Entity Type '" + msg.getOriginator().getEntityType() + "'")); + } else { + edgeEvent.setEdgeId(edgeId); + ListenableFuture saveFuture = ctx.getEdgeEventService().saveAsync(edgeEvent); + Futures.addCallback(saveFuture, new FutureCallback() { + @Override + public void onSuccess(@Nullable EdgeEvent event) { + ctx.tellNext(msg, SUCCESS); + ctx.onEdgeEventUpdate(ctx.getTenantId(), edgeId); + } + + @Override + public void onFailure(Throwable th) { + log.warn("[{}] Can't save edge event [{}] for edge [{}]", ctx.getTenantId().getId(), edgeEvent, edgeId.getId(), th); + ctx.tellFailure(msg, th); + } + }, ctx.getDbCallbackExecutor()); + } + } catch (JsonProcessingException e) { + log.error("Failed to build edge event", e); + ctx.tellFailure(msg, e); + } + } + } + } + + @Override + public void onFailure(Throwable t) { + ctx.tellFailure(msg, t); + } + + }, ctx.getDbCallbackExecutor()); + } + } + + private EdgeEvent buildEdgeEvent(TbMsg msg, TbContext ctx) throws JsonProcessingException { + String msgType = msg.getType(); + if (DataConstants.ALARM.equals(msgType)) { + return buildEdgeEvent(ctx.getTenantId(), EdgeEventActionType.ADDED, getUUIDFromMsgData(msg), EdgeEventType.ALARM, null); + } else { + EdgeEventType edgeEventTypeByEntityType = EdgeUtils.getEdgeEventTypeByEntityType(msg.getOriginator().getEntityType()); + if (edgeEventTypeByEntityType == null) { + return null; + } + EdgeEventActionType actionType = getEdgeEventActionTypeByMsgType(msgType); + Map entityBody = new HashMap<>(); + Map metadata = msg.getMetaData().getData(); + JsonNode dataJson = JacksonUtil.toJsonNode(msg.getData()); + switch (actionType) { + case ATTRIBUTES_UPDATED: + case POST_ATTRIBUTES: + entityBody.put("kv", dataJson); + entityBody.put(SCOPE, getScope(metadata)); + break; + case ATTRIBUTES_DELETED: + List keys = JacksonUtil.convertValue(dataJson.get("attributes"), new TypeReference<>() {}); + entityBody.put("keys", keys); + entityBody.put(SCOPE, getScope(metadata)); + break; + case TIMESERIES_UPDATED: + entityBody.put("data", dataJson); + entityBody.put("ts", metadata.get("ts")); + break; + } + return buildEdgeEvent(ctx.getTenantId(), actionType, msg.getOriginator().getId(), edgeEventTypeByEntityType, JacksonUtil.valueToTree(entityBody)); + } + } + + private String getScope(Map metadata) { + String scope = metadata.get(SCOPE); + if (StringUtils.isEmpty(scope)) { + // TODO: voba - move this to configuration of the node UI or some other place + scope = DataConstants.SERVER_SCOPE; + } + return scope; + } + + private EdgeEvent buildEdgeEvent(TenantId tenantId, EdgeEventActionType edgeEventAction, UUID entityId, EdgeEventType edgeEventType, JsonNode entityBody) { + EdgeEvent edgeEvent = new EdgeEvent(); + edgeEvent.setTenantId(tenantId); + edgeEvent.setAction(edgeEventAction); + edgeEvent.setEntityId(entityId); + edgeEvent.setType(edgeEventType); + edgeEvent.setBody(entityBody); + return edgeEvent; + } + + private UUID getUUIDFromMsgData(TbMsg msg) { + JsonNode data = JacksonUtil.toJsonNode(msg.getData()).get("id"); + String id = JacksonUtil.convertValue(data.get("id"), String.class); + return UUID.fromString(id); + } + + private EdgeEventActionType getEdgeEventActionTypeByMsgType(String msgType) { + EdgeEventActionType actionType; + if (SessionMsgType.POST_TELEMETRY_REQUEST.name().equals(msgType)) { + actionType = EdgeEventActionType.TIMESERIES_UPDATED; + } else if (DataConstants.ATTRIBUTES_UPDATED.equals(msgType)) { + actionType = EdgeEventActionType.ATTRIBUTES_UPDATED; + } else if (SessionMsgType.POST_ATTRIBUTES_REQUEST.name().equals(msgType)) { + actionType = EdgeEventActionType.POST_ATTRIBUTES; + } else { + actionType = EdgeEventActionType.ATTRIBUTES_DELETED; + } + return actionType; + } + + private boolean isSupportedOriginator(EntityType entityType) { + switch (entityType) { + case DEVICE: + case ASSET: + case ENTITY_VIEW: + case DASHBOARD: + case TENANT: + case CUSTOMER: + case EDGE: + return true; + default: + return false; + } + } + + private boolean isSupportedMsgType(String msgType) { + return SessionMsgType.POST_TELEMETRY_REQUEST.name().equals(msgType) + || SessionMsgType.POST_ATTRIBUTES_REQUEST.name().equals(msgType) + || DataConstants.ATTRIBUTES_UPDATED.equals(msgType) + || DataConstants.ATTRIBUTES_DELETED.equals(msgType) + || DataConstants.ALARM.equals(msgType); + } + + @Override + public void destroy() { + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java index 4193d83270..f4c2411a33 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java @@ -16,9 +16,13 @@ package org.thingsboard.rule.engine.filter; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.ScriptEngine; +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.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java index fe362b4b93..f677b90158 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java @@ -17,8 +17,13 @@ package org.thingsboard.rule.engine.filter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.ScriptEngine; +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.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java index 0c253e9c76..720cd76ae0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java @@ -16,8 +16,12 @@ package org.thingsboard.rule.engine.filter; 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.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java index 1db60cebab..2148568925 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java @@ -16,8 +16,12 @@ package org.thingsboard.rule.engine.filter; 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.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java index 1c22b357ad..398e2a3a6f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java @@ -16,8 +16,13 @@ package org.thingsboard.rule.engine.filter; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -67,6 +72,9 @@ public class TbOriginatorTypeSwitchNode implements TbNode { case ENTITY_VIEW: relationType = "Entity View"; break; + case EDGE: + relationType = "Edge"; + break; case RULE_CHAIN: relationType = "Rule chain"; break; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java index 17c49adfc7..8599a50ad8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java @@ -26,7 +26,11 @@ import com.google.protobuf.ByteString; import com.google.pubsub.v1.ProjectTopicName; import com.google.pubsub.v1.PubsubMessage; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.rule.engine.api.*; +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.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -36,8 +40,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.concurrent.TimeUnit; -import static org.thingsboard.common.util.DonAsynchron.withCallback; - @Slf4j @RuleNode( type = ComponentType.EXTERNAL, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java index f0f7756356..69c5a3ac74 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java @@ -15,31 +15,13 @@ */ package org.thingsboard.rule.engine.geo; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; -import org.locationtech.spatial4j.context.jts.JtsSpatialContext; -import org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory; -import org.locationtech.spatial4j.shape.Point; -import org.locationtech.spatial4j.shape.Shape; -import org.locationtech.spatial4j.shape.ShapeFactory; -import org.locationtech.spatial4j.shape.SpatialRelation; -import org.springframework.util.StringUtils; 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.util.TbNodeUtils; -import org.thingsboard.rule.engine.filter.TbMsgTypeFilterNodeConfiguration; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -import java.util.Collections; -import java.util.List; - /** * Created by ashvayka on 19.01.18. */ diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java index 8372ebdd10..c8415e3181 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java @@ -19,8 +19,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; +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.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java index f4e651288c..9797e3142e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java @@ -18,11 +18,11 @@ package org.thingsboard.rule.engine.metadata; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java index e3303d074e..c4c8295823 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.ContactBased; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -118,6 +119,18 @@ public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode { + if (edge != null) { + if (!edge.getCustomerId().isNullUid()) { + return ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), edge.getCustomerId()); + } else { + throw new RuntimeException("Edge with name '" + edge.getName() + "' is not assigned to Customer."); + } + } else { + return Futures.immediateFuture(null); + } + }, MoreExecutors.directExecutor()); default: throw new RuntimeException("Entity with entityType '" + msg.getOriginator().getEntityType() + "' is not supported."); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java index 81e221683e..f988becb94 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java @@ -17,14 +17,13 @@ package org.thingsboard.rule.engine.metadata; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.util.EntitiesRelatedDeviceIdAsyncLoader; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java index 438de99846..cdcc354611 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java @@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import static org.thingsboard.common.util.DonAsynchron.withCallback; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; /** * Created by ashvayka on 19.01.18. diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java index 0310b1bbe4..e9a382bb4d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java @@ -16,10 +16,12 @@ package org.thingsboard.rule.engine.metadata; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader; - import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java index f484a5551c..3c3fa8e21a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java @@ -47,7 +47,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL; import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.FETCH_MODE_FIRST; import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.MAX_FETCH_SIZE; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 4c732e0d97..4f4dc7ab60 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -20,7 +20,6 @@ import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.ssl.SslContext; import io.netty.util.concurrent.Future; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; import org.springframework.util.StringUtils; import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java index a8e32145d0..ceee362fc8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java @@ -31,8 +31,8 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; -import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType; import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; +import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.msg.TbMsg; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java index f481c875a9..fea622bf30 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java @@ -16,17 +16,24 @@ package org.thingsboard.rule.engine.rabbitmq; import com.google.common.util.concurrent.ListenableFuture; -import com.rabbitmq.client.*; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.MessageProperties; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +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.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import java.nio.charset.Charset; -import java.util.concurrent.ExecutionException; import static org.thingsboard.common.util.DonAsynchron.withCallback; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java index 2f82ba64a1..408e27e50b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java @@ -17,14 +17,13 @@ package org.thingsboard.rule.engine.rpc; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; 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.util.TbNodeUtils; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java index 42d45ca401..07952359a1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java @@ -16,13 +16,16 @@ package org.thingsboard.rule.engine.rpc; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -30,6 +33,7 @@ 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.TbRelationTypes; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index d92fd24741..dc657cfce6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -18,13 +18,13 @@ package org.thingsboard.rule.engine.telemetry; import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; 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.server.common.data.TenantProfile; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationBeginNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationBeginNode.java index 40c4bc4410..ba264ccc51 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationBeginNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationBeginNode.java @@ -22,12 +22,9 @@ 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.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; - @Slf4j @RuleNode( type = ComponentType.ACTION, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationEndNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationEndNode.java index 9b35f81e93..0e85f473bb 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationEndNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationEndNode.java @@ -22,14 +22,9 @@ 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.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -import java.util.concurrent.ExecutionException; - -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; - @Slf4j @RuleNode( type = ComponentType.ACTION, diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js index 4eea9d1764..3d86444d63 100644 --- a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js +++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js @@ -12,5 +12,5 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ***************************************************************************** */var y=function(e,t){return(y=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(e[r]=t[r])})(e,t)};function b(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function r(){this.constructor=e}y(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}function h(e,t,r,n){var a,o=arguments.length,i=o<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,r):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)i=Reflect.decorate(e,t,r,n);else for(var l=e.length-1;l>=0;l--)(a=e[l])&&(i=(o<3?a(i):o>3?a(t,r,i):a(t,r))||i);return o>3&&i&&Object.defineProperty(t,r,i),i}function C(e,t){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(e,t)}Object.create;function v(e){var t="function"==typeof Symbol&&Symbol.iterator,r=t&&e[t],n=0;if(r)return r.call(e);if(e&&"number"==typeof e.length)return{next:function(){return e&&n>=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}Object.create;var F,x=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.emptyConfigForm},r.prototype.onConfigurationSet=function(e){this.emptyConfigForm=this.fb.group({})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-node-empty-config",template:"

"}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),T=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.attributeScopes=Object.keys(a.AttributeScope),n.telemetryTypeTranslationsMap=a.telemetryTypeTranslations,n}return b(r,e),r.prototype.configForm=function(){return this.attributesConfigForm},r.prototype.onConfigurationSet=function(e){this.attributesConfigForm=this.fb.group({scope:[e?e.scope:null,[i.Validators.required]],notifyDevice:[!e||e.scope,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-attributes-config",template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.notify-device\' | translate }}\n \n
tb.rulenode.notify-device-hint
\n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),q=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.timeseriesConfigForm},r.prototype.onConfigurationSet=function(e){this.timeseriesConfigForm=this.fb.group({defaultTTL:[e?e.defaultTTL:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-timeseries-config",template:'
\n \n tb.rulenode.default-ttl\n \n \n {{ \'tb.rulenode.default-ttl-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-default-ttl-message\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),S=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.rpcRequestConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcRequestConfigForm=this.fb.group({timeoutInSeconds:[e?e.timeoutInSeconds:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-rpc-request-config",template:'
\n \n tb.rulenode.timeout-sec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-message\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),I=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.logConfigForm},r.prototype.onConfigurationSet=function(e){this.logConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.logConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"string",this.translate.instant("tb.rulenode.to-string"),"ToString",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.logConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-log-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),k=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.assignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.assignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required,i.Validators.pattern(/.*\S.*/)]],createCustomerIfNotExists:[!!e&&e.createCustomerIfNotExists,[]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.prepareOutputConfig=function(e){return e.customerNamePattern=e.customerNamePattern.trim(),e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.create-customer-if-not-exists\' | translate }}\n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),N=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.clearAlarmConfigForm},r.prototype.onConfigurationSet=function(e){this.clearAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],alarmType:[e?e.alarmType:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.clearAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.clearAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-clear-alarm-config",template:'
\n \n \n \n
\n \n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),V=function(e){function r(t,r,n,o){var i=e.call(this,t)||this;return i.store=t,i.fb=r,i.nodeScriptTestService=n,i.translate=o,i.alarmSeverities=Object.keys(a.AlarmSeverity),i.alarmSeverityTranslationMap=a.alarmSeverityTranslations,i.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],i}return b(r,e),r.prototype.configForm=function(){return this.createAlarmConfigForm},r.prototype.onConfigurationSet=function(e){this.createAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],useMessageAlarmData:[!!e&&e.useMessageAlarmData,[]],alarmType:[e?e.alarmType:null,[]],severity:[e?e.severity:null,[]],propagate:[!!e&&e.propagate,[]],relationTypes:[e?e.relationTypes:null,[]]})},r.prototype.validatorTriggers=function(){return["useMessageAlarmData"]},r.prototype.updateValidators=function(e){this.createAlarmConfigForm.get("useMessageAlarmData").value?(this.createAlarmConfigForm.get("alarmType").setValidators([]),this.createAlarmConfigForm.get("severity").setValidators([])):(this.createAlarmConfigForm.get("alarmType").setValidators([i.Validators.required]),this.createAlarmConfigForm.get("severity").setValidators([i.Validators.required])),this.createAlarmConfigForm.get("alarmType").updateValueAndValidity({emitEvent:e}),this.createAlarmConfigForm.get("severity").updateValueAndValidity({emitEvent:e})},r.prototype.testScript=function(){var e=this,t=this.createAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.createAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.removeKey=function(e,t){var r=this.createAlarmConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.createAlarmConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.createAlarmConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.createAlarmConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-create-alarm-config",template:'
\n \n \n \n
\n \n
\n \n {{ \'tb.rulenode.use-message-alarm-data\' | translate }}\n \n
\n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n \n tb.rulenode.alarm-severity\n \n \n {{ alarmSeverityTranslationMap.get(severity) | translate }}\n \n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n
\n \n {{ \'tb.rulenode.propagate\' | translate }}\n \n
\n \n tb.rulenode.relation-types-list\n \n \n {{key}}\n close\n \n \n \n tb.rulenode.relation-types-list-hint\n \n
\n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),E=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return b(r,e),r.prototype.configForm=function(){return this.createRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.createRelationConfigForm=this.fb.group({direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[i.Validators.required]],entityNamePattern:[e?e.entityNamePattern:null,[]],entityTypePattern:[e?e.entityTypePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],createEntityIfNotExists:[!!e&&e.createEntityIfNotExists,[]],removeCurrentRelations:[!!e&&e.removeCurrentRelations,[]],changeOriginatorToRelatedEntity:[!!e&&e.changeOriginatorToRelatedEntity,[]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["entityType"]},r.prototype.updateValidators=function(e){var t=this.createRelationConfigForm.get("entityType").value;t?this.createRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required,i.Validators.pattern(/.*\S.*/)]):this.createRelationConfigForm.get("entityNamePattern").setValidators([]),!t||t!==a.EntityType.DEVICE&&t!==a.EntityType.ASSET?this.createRelationConfigForm.get("entityTypePattern").setValidators([]):this.createRelationConfigForm.get("entityTypePattern").setValidators([i.Validators.required,i.Validators.pattern(/.*\S.*/)]),this.createRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e}),this.createRelationConfigForm.get("entityTypePattern").updateValueAndValidity({emitEvent:e})},r.prototype.prepareOutputConfig=function(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e.entityTypePattern=e.entityTypePattern?e.entityTypePattern.trim():null,e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-create-relation-config",template:'
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-type-pattern\n \n \n {{ \'tb.rulenode.entity-type-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.create-entity-if-not-exists\' | translate }}\n \n
tb.rulenode.create-entity-if-not-exists-hint
\n
\n \n {{ \'tb.rulenode.remove-current-relations\' | translate }}\n \n
tb.rulenode.remove-current-relations-hint
\n \n {{ \'tb.rulenode.change-originator-to-related-entity\' | translate }}\n \n
tb.rulenode.change-originator-to-related-entity-hint
\n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),A=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.msgDelayConfigForm},r.prototype.onConfigurationSet=function(e){this.msgDelayConfigForm=this.fb.group({useMetadataPeriodInSecondsPatterns:[!!e&&e.useMetadataPeriodInSecondsPatterns,[]],periodInSeconds:[e?e.periodInSeconds:null,[]],periodInSecondsPattern:[e?e.periodInSecondsPattern:null,[]],maxPendingMsgs:[e?e.maxPendingMsgs:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(1e5)]]})},r.prototype.validatorTriggers=function(){return["useMetadataPeriodInSecondsPatterns"]},r.prototype.updateValidators=function(e){this.msgDelayConfigForm.get("useMetadataPeriodInSecondsPatterns").value?(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([i.Validators.required]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([])):(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([i.Validators.required,i.Validators.min(0)])),this.msgDelayConfigForm.get("periodInSecondsPattern").updateValueAndValidity({emitEvent:e}),this.msgDelayConfigForm.get("periodInSeconds").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-msg-delay-config",template:'
\n \n {{ \'tb.rulenode.use-metadata-period-in-seconds-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-period-in-seconds-patterns-hint
\n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-0-seconds-message\' | translate }}\n \n \n \n \n tb.rulenode.period-in-seconds-pattern\n \n \n {{ \'tb.rulenode.period-in-seconds-pattern-required\' | translate }}\n \n \n \n \n \n tb.rulenode.max-pending-messages\n \n \n {{ \'tb.rulenode.max-pending-messages-required\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),L=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return b(r,e),r.prototype.configForm=function(){return this.deleteRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.deleteRelationConfigForm=this.fb.group({deleteForSingleEntity:[!!e&&e.deleteForSingleEntity,[]],direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[]],entityNamePattern:[e?e.entityNamePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["deleteForSingleEntity","entityType"]},r.prototype.updateValidators=function(e){var t=this.deleteRelationConfigForm.get("deleteForSingleEntity").value,r=this.deleteRelationConfigForm.get("entityType").value;t?this.deleteRelationConfigForm.get("entityType").setValidators([i.Validators.required]):this.deleteRelationConfigForm.get("entityType").setValidators([]),t&&r?this.deleteRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required,i.Validators.pattern(/.*\S.*/)]):this.deleteRelationConfigForm.get("entityNamePattern").setValidators([]),this.deleteRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:!1}),this.deleteRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e})},r.prototype.prepareOutputConfig=function(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-delete-relation-config",template:'
\n \n {{ \'tb.rulenode.delete-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.delete-relation-hint
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),P=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.generatorConfigForm},r.prototype.onConfigurationSet=function(e){this.generatorConfigForm=this.fb.group({msgCount:[e?e.msgCount:null,[i.Validators.required,i.Validators.min(0)]],periodInSeconds:[e?e.periodInSeconds:null,[i.Validators.required,i.Validators.min(1)]],originator:[e?e.originator:null,[]],jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.prepareInputConfig=function(e){return e&&(e.originatorId&&e.originatorType?e.originator={id:e.originatorId,entityType:e.originatorType}:e.originator=null,delete e.originatorId,delete e.originatorType),e},r.prototype.prepareOutputConfig=function(e){return e.originator?(e.originatorId=e.originator.id,e.originatorType=e.originator.entityType):(e.originatorId=null,e.originatorType=null),delete e.originator,e},r.prototype.testScript=function(){var e=this,t=this.generatorConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"generate",this.translate.instant("tb.rulenode.generator"),"Generate",["prevMsg","prevMetadata","prevMsgType"],this.ruleNodeId).subscribe((function(t){t&&e.generatorConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-generator-config",template:'
\n \n tb.rulenode.message-count\n \n \n {{ \'tb.rulenode.message-count-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-message-count-message\' | translate }}\n \n \n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-seconds-message\' | translate }}\n \n \n
\n \n \n \n
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent);!function(e){e.CUSTOMER="CUSTOMER",e.TENANT="TENANT",e.RELATED="RELATED",e.ALARM_ORIGINATOR="ALARM_ORIGINATOR"}(F||(F={}));var M,w=new Map([[F.CUSTOMER,"tb.rulenode.originator-customer"],[F.TENANT,"tb.rulenode.originator-tenant"],[F.RELATED,"tb.rulenode.originator-related"],[F.ALARM_ORIGINATOR,"tb.rulenode.originator-alarm-originator"]]);!function(e){e.CIRCLE="CIRCLE",e.POLYGON="POLYGON"}(M||(M={}));var R,D=new Map([[M.CIRCLE,"tb.rulenode.perimeter-circle"],[M.POLYGON,"tb.rulenode.perimeter-polygon"]]);!function(e){e.MILLISECONDS="MILLISECONDS",e.SECONDS="SECONDS",e.MINUTES="MINUTES",e.HOURS="HOURS",e.DAYS="DAYS"}(R||(R={}));var O,K=new Map([[R.MILLISECONDS,"tb.rulenode.time-unit-milliseconds"],[R.SECONDS,"tb.rulenode.time-unit-seconds"],[R.MINUTES,"tb.rulenode.time-unit-minutes"],[R.HOURS,"tb.rulenode.time-unit-hours"],[R.DAYS,"tb.rulenode.time-unit-days"]]);!function(e){e.METER="METER",e.KILOMETER="KILOMETER",e.FOOT="FOOT",e.MILE="MILE",e.NAUTICAL_MILE="NAUTICAL_MILE"}(O||(O={}));var B,G=new Map([[O.METER,"tb.rulenode.range-unit-meter"],[O.KILOMETER,"tb.rulenode.range-unit-kilometer"],[O.FOOT,"tb.rulenode.range-unit-foot"],[O.MILE,"tb.rulenode.range-unit-mile"],[O.NAUTICAL_MILE,"tb.rulenode.range-unit-nautical-mile"]]);!function(e){e.TITLE="TITLE",e.COUNTRY="COUNTRY",e.STATE="STATE",e.ZIP="ZIP",e.ADDRESS="ADDRESS",e.ADDRESS2="ADDRESS2",e.PHONE="PHONE",e.EMAIL="EMAIL",e.ADDITIONAL_INFO="ADDITIONAL_INFO"}(B||(B={}));var H,U,j,z=new Map([[B.TITLE,"tb.rulenode.entity-details-title"],[B.COUNTRY,"tb.rulenode.entity-details-country"],[B.STATE,"tb.rulenode.entity-details-state"],[B.ZIP,"tb.rulenode.entity-details-zip"],[B.ADDRESS,"tb.rulenode.entity-details-address"],[B.ADDRESS2,"tb.rulenode.entity-details-address2"],[B.PHONE,"tb.rulenode.entity-details-phone"],[B.EMAIL,"tb.rulenode.entity-details-email"],[B.ADDITIONAL_INFO,"tb.rulenode.entity-details-additional_info"]]);!function(e){e.FIRST="FIRST",e.LAST="LAST",e.ALL="ALL"}(H||(H={})),function(e){e.ASC="ASC",e.DESC="DESC"}(U||(U={})),function(e){e.STANDARD="STANDARD",e.FIFO="FIFO"}(j||(j={}));var Q,_=new Map([[j.STANDARD,"tb.rulenode.sqs-queue-standard"],[j.FIFO,"tb.rulenode.sqs-queue-fifo"]]),$=["anonymous","basic","cert.PEM"],W=new Map([["anonymous","tb.rulenode.credentials-anonymous"],["basic","tb.rulenode.credentials-basic"],["cert.PEM","tb.rulenode.credentials-pem"]]),J=["sas","cert.PEM"],Y=new Map([["sas","tb.rulenode.credentials-sas"],["cert.PEM","tb.rulenode.credentials-pem"]]);!function(e){e.GET="GET",e.POST="POST",e.PUT="PUT",e.DELETE="DELETE"}(Q||(Q={}));var Z=["US-ASCII","ISO-8859-1","UTF-8","UTF-16BE","UTF-16LE","UTF-16"],X=new Map([["US-ASCII","tb.rulenode.charset-us-ascii"],["ISO-8859-1","tb.rulenode.charset-iso-8859-1"],["UTF-8","tb.rulenode.charset-utf-8"],["UTF-16BE","tb.rulenode.charset-utf-16be"],["UTF-16LE","tb.rulenode.charset-utf-16le"],["UTF-16","tb.rulenode.charset-utf-16"]]),ee=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=D,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=G,n.timeUnits=Object.keys(R),n.timeUnitsTranslationMap=K,n}return b(r,e),r.prototype.configForm=function(){return this.geoActionConfigForm},r.prototype.onConfigurationSet=function(e){this.geoActionConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]],minInsideDuration:[e?e.minInsideDuration:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minInsideDurationTimeUnit:[e?e.minInsideDurationTimeUnit:null,[i.Validators.required]],minOutsideDuration:[e?e.minOutsideDuration:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minOutsideDurationTimeUnit:[e?e.minOutsideDurationTimeUnit:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoActionConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoActionConfigForm.get("perimeterType").value;t?this.geoActionConfigForm.get("perimeterType").setValidators([]):this.geoActionConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.CIRCLE?(this.geoActionConfigForm.get("centerLatitude").setValidators([]),this.geoActionConfigForm.get("centerLongitude").setValidators([]),this.geoActionConfigForm.get("range").setValidators([]),this.geoActionConfigForm.get("rangeUnit").setValidators([])):(this.geoActionConfigForm.get("centerLatitude").setValidators([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoActionConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoActionConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoActionConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoActionConfigForm.get("polygonsDefinition").setValidators([]):this.geoActionConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoActionConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),this.geoActionConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-gps-geofencing-config",template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.min-inside-duration\n \n \n {{ \'tb.rulenode.min-inside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-inside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.min-outside-duration\n \n \n {{ \'tb.rulenode.min-outside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-outside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.msgCountConfigForm},r.prototype.onConfigurationSet=function(e){this.msgCountConfigForm=this.fb.group({interval:[e?e.interval:null,[i.Validators.required,i.Validators.min(1)]],telemetryPrefix:[e?e.telemetryPrefix:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-msg-count-config",template:'
\n \n tb.rulenode.interval-seconds\n \n \n {{ \'tb.rulenode.interval-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-interval-seconds-message\' | translate }}\n \n \n \n tb.rulenode.output-timeseries-key-prefix\n \n \n {{ \'tb.rulenode.output-timeseries-key-prefix-required\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.rpcReplyConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcReplyConfigForm=this.fb.group({requestIdMetaDataAttribute:[e?e.requestIdMetaDataAttribute:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-rpc-reply-config",template:'
\n \n tb.rulenode.request-id-metadata-attribute\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ne=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.saveToCustomTableConfigForm},r.prototype.onConfigurationSet=function(e){this.saveToCustomTableConfigForm=this.fb.group({tableName:[e?e.tableName:null,[i.Validators.required,i.Validators.pattern(/.*\S.*/)]],fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.prototype.prepareOutputConfig=function(e){return e.tableName=e.tableName.trim(),e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-custom-table-config",template:'
\n \n tb.rulenode.custom-table-name\n \n \n {{ \'tb.rulenode.custom-table-name-required\' | translate }}\n \n tb.rulenode.custom-table-hint\n \n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ae=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.translate=r,o.injector=n,o.fb=a,o.propagateChange=null,o.valueChangeSubscription=null,o}var a;return b(r,e),a=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){this.ngControl=this.injector.get(i.NgControl),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.kvListFormGroup=this.fb.group({}),this.kvListFormGroup.addControl("keyVals",this.fb.array([]))},r.prototype.keyValsFormArray=function(){return this.kvListFormGroup.get("keyVals")},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.kvListFormGroup.disable({emitEvent:!1}):this.kvListFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t,r,n=this;this.valueChangeSubscription&&this.valueChangeSubscription.unsubscribe();var a=[];if(e)try{for(var o=v(Object.keys(e)),l=o.next();!l.done;l=o.next()){var s=l.value;Object.prototype.hasOwnProperty.call(e,s)&&a.push(this.fb.group({key:[s,[i.Validators.required]],value:[e[s],[i.Validators.required]]}))}}catch(e){t={error:e}}finally{try{l&&!l.done&&(r=o.return)&&r.call(o)}finally{if(t)throw t.error}}this.kvListFormGroup.setControl("keyVals",this.fb.array(a)),this.valueChangeSubscription=this.kvListFormGroup.valueChanges.subscribe((function(){n.updateModel()}))},r.prototype.removeKeyVal=function(e){this.kvListFormGroup.get("keyVals").removeAt(e)},r.prototype.addKeyVal=function(){this.kvListFormGroup.get("keyVals").push(this.fb.group({key:["",[i.Validators.required]],value:["",[i.Validators.required]]}))},r.prototype.validate=function(e){return!this.kvListFormGroup.get("keyVals").value.length&&this.required?{kvMapRequired:!0}:this.kvListFormGroup.valid?null:{kvFieldsRequired:!0}},r.prototype.updateModel=function(){var e=this.kvListFormGroup.get("keyVals").value;if(this.required&&!e.length||!this.kvListFormGroup.valid)this.propagateChange(null);else{var t={};e.forEach((function(e){t[e.key]=e.value})),this.propagateChange(t)}},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:t.Injector},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.Input(),C("design:type",String)],r.prototype,"requiredText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"keyText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"keyRequiredText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"valText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"valRequiredText",void 0),h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),r=a=h([t.Component({selector:"tb-kv-map-config",template:'
\n
\n {{ keyText | translate }}\n {{ valText | translate }}\n \n
\n
\n
\n \n \n \n \n {{ keyRequiredText | translate }}\n \n \n \n \n \n \n {{ valRequiredText | translate }}\n \n \n \n
\n
\n \n
\n \n
\n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return a})),multi:!0},{provide:i.NG_VALIDATORS,useExisting:t.forwardRef((function(){return a})),multi:!0}],styles:[":host .tb-kv-map-config{margin-bottom:16px}:host .tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}:host .tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:rgba(0,0,0,.54);font-size:12px;font-weight:700;white-space:nowrap}:host .tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}:host .tb-kv-map-config .body .row{padding-top:5px;max-height:40px}:host .tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell{margin:0;max-height:40px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell .mat-form-field-infix{border-top:0}:host ::ng-deep .tb-kv-map-config .body button.mat-button{margin:0}"]}),C("design:paramtypes",[o.Store,n.TranslateService,t.Injector,i.FormBuilder])],r)}(a.PageComponent),oe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n.propagateChange=null,n}var n;return b(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.deviceRelationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],relationType:[null],deviceTypes:[null,[i.Validators.required]]}),this.deviceRelationsQueryFormGroup.valueChanges.subscribe((function(t){e.deviceRelationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.deviceRelationsQueryFormGroup.disable({emitEvent:!1}):this.deviceRelationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.deviceRelationsQueryFormGroup.reset(e,{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=h([t.Component({selector:"tb-device-relations-query-config",template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-type
\n \n \n
device.device-types
\n \n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),ie=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.propagateChange=null,n}var n;return b(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.relationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],filters:[null]}),this.relationsQueryFormGroup.valueChanges.subscribe((function(t){e.relationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.relationsQueryFormGroup.disable({emitEvent:!1}):this.relationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.relationsQueryFormGroup.reset(e||{},{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=h([t.Component({selector:"tb-relations-query-config",template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-filters
\n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),le=function(e){function r(t,r,n,o){var i,l,s=e.call(this,t)||this;s.store=t,s.translate=r,s.truncate=n,s.fb=o,s.placeholder="tb.rulenode.message-type",s.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],s.messageTypes=[],s.messageTypesList=[],s.searchText="",s.propagateChange=function(e){},s.messageTypeConfigForm=s.fb.group({messageType:[null]});try{for(var u=v(Object.keys(a.MessageType)),d=u.next();!d.done;d=u.next()){var p=d.value;s.messageTypesList.push({name:a.messageTypeNames.get(a.MessageType[p]),value:p})}}catch(e){i={error:e}}finally{try{d&&!d.done&&(l=u.return)&&l.call(u)}finally{if(i)throw i.error}}return s}var l;return b(r,e),l=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.ngOnInit=function(){var e=this;this.filteredMessageTypes=this.messageTypeConfigForm.get("messageType").valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(t){return e.fetchMessageTypes(t)})),f.share())},r.prototype.ngAfterViewInit=function(){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.messageTypeConfigForm.disable({emitEvent:!1}):this.messageTypeConfigForm.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t=this;this.searchText="",this.messageTypes.length=0,e&&e.forEach((function(e){var r=t.messageTypesList.find((function(t){return t.value===e}));r?t.messageTypes.push({name:r.name,value:r.value}):t.messageTypes.push({name:e,value:e})}))},r.prototype.displayMessageTypeFn=function(e){return e?e.name:void 0},r.prototype.textIsNotEmpty=function(e){return!!(e&&null!=e&&e.length>0)},r.prototype.createMessageType=function(e,t){e.preventDefault(),this.transformMessageType(t)},r.prototype.add=function(e){this.transformMessageType(e.value)},r.prototype.fetchMessageTypes=function(e){if(this.searchText=e,this.searchText&&this.searchText.length){var t=this.searchText.toUpperCase();return c.of(this.messageTypesList.filter((function(e){return e.name.toUpperCase().includes(t)})))}return c.of(this.messageTypesList)},r.prototype.transformMessageType=function(e){if((e||"").trim()){var t=null,r=e.trim(),n=this.messageTypesList.find((function(e){return e.name===r}));(t=n?{name:n.name,value:n.value}:{name:r,value:r})&&this.addMessageType(t)}this.clear("")},r.prototype.remove=function(e){var t=this.messageTypes.indexOf(e);t>=0&&(this.messageTypes.splice(t,1),this.updateModel())},r.prototype.selected=function(e){this.addMessageType(e.option.value),this.clear("")},r.prototype.addMessageType=function(e){-1===this.messageTypes.findIndex((function(t){return t.value===e.value}))&&(this.messageTypes.push(e),this.updateModel())},r.prototype.onFocus=function(){this.messageTypeConfigForm.get("messageType").updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.messageTypeInput.nativeElement.value=e,this.messageTypeConfigForm.get("messageType").patchValue(null,{emitEvent:!0}),setTimeout((function(){t.messageTypeInput.nativeElement.blur(),t.messageTypeInput.nativeElement.focus()}),0)},r.prototype.updateModel=function(){var e=this.messageTypes.map((function(e){return e.value}));this.required?(this.chipList.errorState=!e.length,this.propagateChange(e.length>0?e:null)):(this.chipList.errorState=!1,this.propagateChange(e))},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:a.TruncatePipe},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),h([t.Input(),C("design:type",String)],r.prototype,"label",void 0),h([t.Input(),C("design:type",Object)],r.prototype,"placeholder",void 0),h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.ViewChild("chipList",{static:!1}),C("design:type",d.MatChipList)],r.prototype,"chipList",void 0),h([t.ViewChild("messageTypeAutocomplete",{static:!1}),C("design:type",p.MatAutocomplete)],r.prototype,"matAutocomplete",void 0),h([t.ViewChild("messageTypeInput",{static:!1}),C("design:type",t.ElementRef)],r.prototype,"messageTypeInput",void 0),r=l=h([t.Component({selector:"tb-message-types-config",template:'\n {{ label }}\n \n \n {{messageType.name}}\n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-message-types-found\n
\n \n \n {{ translate.get(\'tb.rulenode.no-message-type-matching\',\n {messageType: truncate.transform(searchText, true, 6, '...')}) | async }}\n \n \n \n tb.rulenode.create-new-message-type\n \n
\n
\n
\n \n {{ \'tb.rulenode.message-types-required\' | translate }}\n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return l})),multi:!0}]}),C("design:paramtypes",[o.Store,n.TranslateService,a.TruncatePipe,i.FormBuilder])],r)}(a.PageComponent),se=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.subscriptions=[],n.disableCertPemCredentials=!1,n.allCredentialsTypes=$,n.credentialsTypeTranslationsMap=W,n.propagateChange=null,n}var n;return b(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.credentialsConfigFormGroup=this.fb.group({type:[null,[i.Validators.required]],username:[null,[]],password:[null,[]],caCert:[null,[]],caCertFileName:[null,[]],privateKey:[null,[]],privateKeyFileName:[null,[]],cert:[null,[]],certFileName:[null,[]]}),this.subscriptions.push(this.credentialsConfigFormGroup.valueChanges.pipe(f.distinctUntilChanged()).subscribe((function(){e.updateView()}))),this.subscriptions.push(this.credentialsConfigFormGroup.get("type").valueChanges.subscribe((function(){e.credentialsTypeChanged()})))},r.prototype.ngOnChanges=function(e){var t,r,n=this;try{for(var a=v(Object.keys(e)),o=a.next();!o.done;o=a.next()){var i=o.value,l=e[i];if(!l.firstChange&&l.currentValue!==l.previousValue)if(l.currentValue&&"disableCertPemCredentials"===i)"cert.PEM"===this.credentialsConfigFormGroup.get("type").value&&setTimeout((function(){n.credentialsConfigFormGroup.get("type").patchValue("anonymous",{emitEvent:!0})}))}}catch(e){t={error:e}}finally{try{o&&!o.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}},r.prototype.ngOnDestroy=function(){this.subscriptions.forEach((function(e){return e.unsubscribe()}))},r.prototype.writeValue=function(e){s.isDefinedAndNotNull(e)&&(this.credentialsConfigFormGroup.reset(e,{emitEvent:!1}),this.updateValidators(!1))},r.prototype.setDisabledState=function(e){e?this.credentialsConfigFormGroup.disable():(this.credentialsConfigFormGroup.enable(),this.updateValidators())},r.prototype.updateView=function(){var e=this.credentialsConfigFormGroup.value,t=e.type;switch(t){case"anonymous":e={type:t};break;case"basic":e={type:t,username:e.username,password:e.password};break;case"cert.PEM":delete e.username}this.propagateChange(e)},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.validate=function(e){return this.credentialsConfigFormGroup.valid?null:{credentialsConfig:{valid:!1}}},r.prototype.credentialsTypeChanged=function(){this.credentialsConfigFormGroup.patchValue({username:null,password:null,caCert:null,caCertFileName:null,privateKey:null,privateKeyFileName:null,cert:null,certFileName:null}),this.updateValidators()},r.prototype.updateValidators=function(e){void 0===e&&(e=!1);var t=this.credentialsConfigFormGroup.get("type").value;switch(e&&this.credentialsConfigFormGroup.reset({type:t},{emitEvent:!1}),this.credentialsConfigFormGroup.setValidators([]),this.credentialsConfigFormGroup.get("username").setValidators([]),this.credentialsConfigFormGroup.get("password").setValidators([]),t){case"anonymous":break;case"basic":this.credentialsConfigFormGroup.get("username").setValidators([i.Validators.required]),this.credentialsConfigFormGroup.get("password").setValidators([i.Validators.required]);break;case"cert.PEM":this.credentialsConfigFormGroup.setValidators([this.requiredFilesSelected(i.Validators.required,[["caCert","caCertFileName"],["privateKey","privateKeyFileName","cert","certFileName"]])])}this.credentialsConfigFormGroup.get("username").updateValueAndValidity({emitEvent:e}),this.credentialsConfigFormGroup.get("password").updateValueAndValidity({emitEvent:e}),this.credentialsConfigFormGroup.updateValueAndValidity({emitEvent:e})},r.prototype.requiredFilesSelected=function(e,t){return void 0===t&&(t=null),function(r){return t||(t=[Object.keys(r.controls)]),(null==r?void 0:r.controls)&&t.some((function(t){return t.every((function(t){return!e(r.controls[t])}))}))?null:{notAllRequiredFilesSelected:!0}}},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),h([t.Input(),C("design:type",Object)],r.prototype,"disableCertPemCredentials",void 0),r=n=h([t.Component({selector:"tb-credentials-config",template:'
\n \n \n tb.rulenode.credentials\n \n {{ credentialsTypeTranslationsMap.get(credentialsConfigFormGroup.get(\'type\').value) | translate }}\n \n \n \n \n tb.rulenode.credentials-type\n \n \n {{ credentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.username\n \n \n {{ \'tb.rulenode.username-required\' | translate }}\n \n \n \n tb.rulenode.password\n \n \n {{ \'tb.rulenode.password-required\' | translate }}\n \n \n \n \n
{{ \'tb.rulenode.credentials-pem-hint\' | translate }}
\n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n
\n
\n
\n
\n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0},{provide:i.NG_VALIDATORS,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),me=function(){function e(e){this.sanitizer=e}return e.prototype.transform=function(e){return this.sanitizer.bypassSecurityTrustHtml(e)},e.ctorParameters=function(){return[{type:g.DomSanitizer}]},e=h([t.Pipe({name:"safeHtml"}),C("design:paramtypes",[g.DomSanitizer])],e)}(),ue=function(){function e(){}return e=h([t.NgModule({declarations:[ae,oe,ie,le,se,me],imports:[r.CommonModule,a.SharedModule,l.HomeComponentsModule],exports:[ae,oe,ie,le,se,me]})],e)}(),de=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.unassignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.unassignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required,i.Validators.pattern(/.*\S.*/)]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.prepareOutputConfig=function(e){return e.customerNamePattern=e.customerNamePattern.trim(),e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-un-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),pe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.snsConfigForm},r.prototype.onConfigurationSet=function(e){this.snsConfigForm=this.fb.group({topicArnPattern:[e?e.topicArnPattern:null,[i.Validators.required]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-sns-config",template:'
\n \n tb.rulenode.topic-arn-pattern\n \n \n {{ \'tb.rulenode.topic-arn-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ce=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.sqsQueueType=j,n.sqsQueueTypes=Object.keys(j),n.sqsQueueTypeTranslationsMap=_,n}return b(r,e),r.prototype.configForm=function(){return this.sqsConfigForm},r.prototype.onConfigurationSet=function(e){this.sqsConfigForm=this.fb.group({queueType:[e?e.queueType:null,[i.Validators.required]],queueUrlPattern:[e?e.queueUrlPattern:null,[i.Validators.required]],delaySeconds:[e?e.delaySeconds:null,[i.Validators.min(0),i.Validators.max(900)]],messageAttributes:[e?e.messageAttributes:null,[]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-sqs-config",template:'
\n \n tb.rulenode.queue-type\n \n \n {{ sqsQueueTypeTranslationsMap.get(type) | translate }}\n \n \n \n \n tb.rulenode.queue-url-pattern\n \n \n {{ \'tb.rulenode.queue-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.delay-seconds\n \n \n {{ \'tb.rulenode.min-delay-seconds-message\' | translate }}\n \n \n {{ \'tb.rulenode.max-delay-seconds-message\' | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),fe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.pubSubConfigForm},r.prototype.onConfigurationSet=function(e){this.pubSubConfigForm=this.fb.group({projectId:[e?e.projectId:null,[i.Validators.required]],topicName:[e?e.topicName:null,[i.Validators.required]],serviceAccountKey:[e?e.serviceAccountKey:null,[i.Validators.required]],serviceAccountKeyFileName:[e?e.serviceAccountKeyFileName:null,[i.Validators.required]],messageAttributes:[e?e.messageAttributes:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-pub-sub-config",template:'
\n \n tb.rulenode.gcp-project-id\n \n \n {{ \'tb.rulenode.gcp-project-id-required\' | translate }}\n \n \n \n tb.rulenode.pubsub-topic-name\n \n \n {{ \'tb.rulenode.pubsub-topic-name-required\' | translate }}\n \n \n \n \n \n
\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ge=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.ackValues=["all","-1","0","1"],n.ToByteStandartCharsetTypesValues=Z,n.ToByteStandartCharsetTypeTranslationMap=X,n}return b(r,e),r.prototype.configForm=function(){return this.kafkaConfigForm},r.prototype.onConfigurationSet=function(e){this.kafkaConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],bootstrapServers:[e?e.bootstrapServers:null,[i.Validators.required]],retries:[e?e.retries:null,[i.Validators.min(0)]],batchSize:[e?e.batchSize:null,[i.Validators.min(0)]],linger:[e?e.linger:null,[i.Validators.min(0)]],bufferMemory:[e?e.bufferMemory:null,[i.Validators.min(0)]],acks:[e?e.acks:null,[i.Validators.required]],keySerializer:[e?e.keySerializer:null,[i.Validators.required]],valueSerializer:[e?e.valueSerializer:null,[i.Validators.required]],otherProperties:[e?e.otherProperties:null,[]],addMetadataKeyValuesAsKafkaHeaders:[!!e&&e.addMetadataKeyValuesAsKafkaHeaders,[]],kafkaHeadersCharset:[e?e.kafkaHeadersCharset:null,[]]})},r.prototype.validatorTriggers=function(){return["addMetadataKeyValuesAsKafkaHeaders"]},r.prototype.updateValidators=function(e){this.kafkaConfigForm.get("addMetadataKeyValuesAsKafkaHeaders").value?this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([i.Validators.required]):this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([]),this.kafkaConfigForm.get("kafkaHeadersCharset").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-kafka-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.bootstrap-servers\n \n \n {{ \'tb.rulenode.bootstrap-servers-required\' | translate }}\n \n \n \n tb.rulenode.retries\n \n \n {{ \'tb.rulenode.min-retries-message\' | translate }}\n \n \n \n tb.rulenode.batch-size-bytes\n \n \n {{ \'tb.rulenode.min-batch-size-bytes-message\' | translate }}\n \n \n \n tb.rulenode.linger-ms\n \n \n {{ \'tb.rulenode.min-linger-ms-message\' | translate }}\n \n \n \n tb.rulenode.buffer-memory-bytes\n \n \n {{ \'tb.rulenode.min-buffer-memory-bytes-message\' | translate }}\n \n \n \n tb.rulenode.acks\n \n \n {{ ackValue }}\n \n \n \n \n tb.rulenode.key-serializer\n \n \n {{ \'tb.rulenode.key-serializer-required\' | translate }}\n \n \n \n tb.rulenode.value-serializer\n \n \n {{ \'tb.rulenode.value-serializer-required\' | translate }}\n \n \n \n \n \n \n {{ \'tb.rulenode.add-metadata-key-values-as-kafka-headers\' | translate }}\n \n
tb.rulenode.add-metadata-key-values-as-kafka-headers-hint
\n \n tb.rulenode.charset-encoding\n \n \n {{ ToByteStandartCharsetTypeTranslationMap.get(charset) | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ye=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.mqttConfigForm},r.prototype.onConfigurationSet=function(e){this.mqttConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:[e?e.credentials:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-mqtt-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n \n tb.rulenode.connect-timeout\n \n \n {{ \'tb.rulenode.connect-timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n
\n \n tb.rulenode.client-id\n \n \n \n {{ \'tb.rulenode.clean-session\' | translate }}\n \n \n {{ \'tb.rulenode.enable-ssl\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),be=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],n}return b(r,e),r.prototype.configForm=function(){return this.rabbitMqConfigForm},r.prototype.onConfigurationSet=function(e){this.rabbitMqConfigForm=this.fb.group({exchangeNamePattern:[e?e.exchangeNamePattern:null,[]],routingKeyPattern:[e?e.routingKeyPattern:null,[]],messageProperties:[e?e.messageProperties:null,[]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],virtualHost:[e?e.virtualHost:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]],automaticRecoveryEnabled:[!!e&&e.automaticRecoveryEnabled,[]],connectionTimeout:[e?e.connectionTimeout:null,[i.Validators.min(0)]],handshakeTimeout:[e?e.handshakeTimeout:null,[i.Validators.min(0)]],clientProperties:[e?e.clientProperties:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-rabbit-mq-config",template:'
\n \n tb.rulenode.exchange-name-pattern\n \n \n \n tb.rulenode.routing-key-pattern\n \n \n \n tb.rulenode.message-properties\n \n \n {{ property }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n
\n \n tb.rulenode.virtual-host\n \n \n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n {{ \'tb.rulenode.automatic-recovery\' | translate }}\n \n \n tb.rulenode.connection-timeout-ms\n \n \n {{ \'tb.rulenode.min-connection-timeout-ms-message\' | translate }}\n \n \n \n tb.rulenode.handshake-timeout-ms\n \n \n {{ \'tb.rulenode.min-handshake-timeout-ms-message\' | translate }}\n \n \n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),he=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.proxySchemes=["http","https"],n.httpRequestTypes=Object.keys(Q),n}return b(r,e),r.prototype.configForm=function(){return this.restApiCallConfigForm},r.prototype.onConfigurationSet=function(e){this.restApiCallConfigForm=this.fb.group({restEndpointUrlPattern:[e?e.restEndpointUrlPattern:null,[i.Validators.required]],requestMethod:[e?e.requestMethod:null,[i.Validators.required]],useSimpleClientHttpFactory:[!!e&&e.useSimpleClientHttpFactory,[]],enableProxy:[!!e&&e.enableProxy,[]],useSystemProxyProperties:[!!e&&e.enableProxy,[]],proxyScheme:[e?e.proxyHost:null,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],readTimeoutMs:[e?e.readTimeoutMs:null,[]],maxParallelRequestsCount:[e?e.maxParallelRequestsCount:null,[i.Validators.min(0)]],headers:[e?e.headers:null,[]],useRedisQueueForMsgPersistence:[!!e&&e.useRedisQueueForMsgPersistence,[]],trimQueue:[!!e&&e.trimQueue,[]],maxQueueSize:[e?e.maxQueueSize:null,[]],credentials:[e?e.credentials:null,[]]})},r.prototype.validatorTriggers=function(){return["useSimpleClientHttpFactory","useRedisQueueForMsgPersistence","enableProxy","useSystemProxyProperties"]},r.prototype.updateValidators=function(e){var t=this.restApiCallConfigForm.get("useSimpleClientHttpFactory").value,r=this.restApiCallConfigForm.get("useRedisQueueForMsgPersistence").value,n=this.restApiCallConfigForm.get("enableProxy").value,a=this.restApiCallConfigForm.get("useSystemProxyProperties").value;n&&!a?(this.restApiCallConfigForm.get("proxyHost").setValidators(n?[i.Validators.required]:[]),this.restApiCallConfigForm.get("proxyPort").setValidators(n?[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]:[])):(this.restApiCallConfigForm.get("proxyHost").setValidators([]),this.restApiCallConfigForm.get("proxyPort").setValidators([]),t?this.restApiCallConfigForm.get("readTimeoutMs").setValidators([]):this.restApiCallConfigForm.get("readTimeoutMs").setValidators([i.Validators.min(0)])),r?this.restApiCallConfigForm.get("maxQueueSize").setValidators([i.Validators.min(0)]):this.restApiCallConfigForm.get("maxQueueSize").setValidators([]),this.restApiCallConfigForm.get("readTimeoutMs").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("maxQueueSize").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("credentials").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-rest-api-call-config",template:'
\n \n tb.rulenode.endpoint-url-pattern\n \n \n {{ \'tb.rulenode.endpoint-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.request-method\n \n \n {{ requestType }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n \n {{ \'tb.rulenode.use-simple-client-http-factory\' | translate }}\n \n
\n \n {{ \'tb.rulenode.use-system-proxy-properties\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-scheme\n \n \n {{ proxyScheme }}\n \n \n \n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n
\n \n tb.rulenode.read-timeout\n \n tb.rulenode.read-timeout-hint\n \n \n tb.rulenode.max-parallel-requests-count\n \n tb.rulenode.max-parallel-requests-count-hint\n \n \n
\n \n \n \n {{ \'tb.rulenode.use-redis-queue\' | translate }}\n \n
\n \n {{ \'tb.rulenode.trim-redis-queue\' | translate }}\n \n \n tb.rulenode.redis-queue-max-size\n \n \n
\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ce=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.smtpProtocols=["smtp","smtps"],n.tlsVersions=["TLSv1","TLSv1.1","TLSv1.2","TLSv1.3"],n}return b(r,e),r.prototype.configForm=function(){return this.sendEmailConfigForm},r.prototype.onConfigurationSet=function(e){this.sendEmailConfigForm=this.fb.group({useSystemSmtpSettings:[!!e&&e.useSystemSmtpSettings,[]],smtpProtocol:[e?e.smtpProtocol:null,[]],smtpHost:[e?e.smtpHost:null,[]],smtpPort:[e?e.smtpPort:null,[]],timeout:[e?e.timeout:null,[]],enableTls:[!!e&&e.enableTls,[]],tlsVersion:[e?e.tlsVersion:null,[]],enableProxy:[!!e&&e.enableProxy,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]]})},r.prototype.validatorTriggers=function(){return["useSystemSmtpSettings","enableProxy"]},r.prototype.updateValidators=function(e){var t=this.sendEmailConfigForm.get("useSystemSmtpSettings").value,r=this.sendEmailConfigForm.get("enableProxy").value;t?(this.sendEmailConfigForm.get("smtpProtocol").setValidators([]),this.sendEmailConfigForm.get("smtpHost").setValidators([]),this.sendEmailConfigForm.get("smtpPort").setValidators([]),this.sendEmailConfigForm.get("timeout").setValidators([]),this.sendEmailConfigForm.get("proxyHost").setValidators([]),this.sendEmailConfigForm.get("proxyPort").setValidators([])):(this.sendEmailConfigForm.get("smtpProtocol").setValidators([i.Validators.required]),this.sendEmailConfigForm.get("smtpHost").setValidators([i.Validators.required]),this.sendEmailConfigForm.get("smtpPort").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]),this.sendEmailConfigForm.get("timeout").setValidators([i.Validators.required,i.Validators.min(0)]),this.sendEmailConfigForm.get("proxyHost").setValidators(r?[i.Validators.required]:[]),this.sendEmailConfigForm.get("proxyPort").setValidators(r?[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]:[])),this.sendEmailConfigForm.get("smtpProtocol").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpPort").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("timeout").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-send-email-config",template:'
\n \n {{ \'tb.rulenode.use-system-smtp-settings\' | translate }}\n \n
\n \n tb.rulenode.smtp-protocol\n \n \n {{ smtpProtocol.toUpperCase() }}\n \n \n \n
\n \n tb.rulenode.smtp-host\n \n \n {{ \'tb.rulenode.smtp-host-required\' | translate }}\n \n \n \n tb.rulenode.smtp-port\n \n \n {{ \'tb.rulenode.smtp-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.timeout-msec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-msec-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.enable-tls\' | translate }}\n \n \n tb.rulenode.tls-version\n \n \n {{ tlsVersion }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ve=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.serviceType=a.ServiceType.TB_RULE_ENGINE,n}return b(r,e),r.prototype.configForm=function(){return this.checkPointConfigForm},r.prototype.onConfigurationSet=function(e){this.checkPointConfigForm=this.fb.group({queueName:[e?e.queueName:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-check-point-config",template:'
\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Fe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allAzureIotHubCredentialsTypes=J,n.azureIotHubCredentialsTypeTranslationsMap=Y,n}return b(r,e),r.prototype.configForm=function(){return this.azureIotHubConfigForm},r.prototype.onConfigurationSet=function(e){this.azureIotHubConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[i.Validators.required]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:this.fb.group({type:[e&&e.credentials?e.credentials.type:null,[i.Validators.required]],sasKey:[e&&e.credentials?e.credentials.sasKey:null,[]],caCert:[e&&e.credentials?e.credentials.caCert:null,[]],caCertFileName:[e&&e.credentials?e.credentials.caCertFileName:null,[]],privateKey:[e&&e.credentials?e.credentials.privateKey:null,[]],privateKeyFileName:[e&&e.credentials?e.credentials.privateKeyFileName:null,[]],cert:[e&&e.credentials?e.credentials.cert:null,[]],certFileName:[e&&e.credentials?e.credentials.certFileName:null,[]],password:[e&&e.credentials?e.credentials.password:null,[]]})})},r.prototype.prepareOutputConfig=function(e){var t=e.credentials.type;return"sas"===t&&(e.credentials={type:t,sasKey:e.credentials.sasKey,caCert:e.credentials.caCert,caCertFileName:e.credentials.caCertFileName}),e},r.prototype.validatorTriggers=function(){return["credentials.type"]},r.prototype.updateValidators=function(e){var t=this.azureIotHubConfigForm.get("credentials"),r=t.get("type").value;switch(e&&t.reset({type:r},{emitEvent:!1}),t.get("sasKey").setValidators([]),t.get("privateKey").setValidators([]),t.get("privateKeyFileName").setValidators([]),t.get("cert").setValidators([]),t.get("certFileName").setValidators([]),r){case"sas":t.get("sasKey").setValidators([i.Validators.required]);break;case"cert.PEM":t.get("privateKey").setValidators([i.Validators.required]),t.get("privateKeyFileName").setValidators([i.Validators.required]),t.get("cert").setValidators([i.Validators.required]),t.get("certFileName").setValidators([i.Validators.required])}t.get("sasKey").updateValueAndValidity({emitEvent:e}),t.get("privateKey").updateValueAndValidity({emitEvent:e}),t.get("privateKeyFileName").updateValueAndValidity({emitEvent:e}),t.get("cert").updateValueAndValidity({emitEvent:e}),t.get("certFileName").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-azure-iot-hub-config",template:'
\n \n tb.rulenode.topic\n \n \n {{ \'tb.rulenode.topic-required\' | translate }}\n \n \n \n tb.rulenode.hostname\n \n \n {{ \'tb.rulenode.hostname-required\' | translate }}\n \n \n \n tb.rulenode.device-id\n \n \n {{ \'tb.rulenode.device-id-required\' | translate }}\n \n \n \n \n \n tb.rulenode.credentials\n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(azureIotHubConfigForm.get(\'credentials.type\').value) | translate }}\n \n \n
\n \n tb.rulenode.credentials-type\n \n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.sas-key\n \n \n {{ \'tb.rulenode.sas-key-required\' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n
\n
\n
\n
\n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),xe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.deviceProfile},r.prototype.onConfigurationSet=function(e){this.deviceProfile=this.fb.group({persistAlarmRulesState:[!!e&&e.persistAlarmRulesState,i.Validators.required],fetchAlarmRulesStateOnStart:[!!e&&e.fetchAlarmRulesStateOnStart,i.Validators.required]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-device-profile-config",template:'
\n \n {{ \'tb.rulenode.persist-alarm-rules\' | translate }}\n \n \n {{ \'tb.rulenode.fetch-alarm-rules\' | translate }}\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.sendSmsConfigForm},r.prototype.onConfigurationSet=function(e){this.sendSmsConfigForm=this.fb.group({numbersToTemplate:[e?e.numbersToTemplate:null,[i.Validators.required]],smsMessageTemplate:[e?e.smsMessageTemplate:null,[i.Validators.required]],useSystemSmsSettings:[!!e&&e.useSystemSmsSettings,[]],smsProviderConfiguration:[e?e.smsProviderConfiguration:null,[]]})},r.prototype.validatorTriggers=function(){return["useSystemSmsSettings"]},r.prototype.updateValidators=function(e){this.sendSmsConfigForm.get("useSystemSmsSettings").value?this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([]):this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([i.Validators.required]),this.sendSmsConfigForm.get("smsProviderConfiguration").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-send-sms-config",template:'
\n \n tb.rulenode.numbers-to-template\n \n \n {{ \'tb.rulenode.numbers-to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.sms-message-template\n \n \n {{ \'tb.rulenode.sms-message-template-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.use-system-sms-settings\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),qe=function(){function e(){}return e=h([t.NgModule({declarations:[T,q,S,I,k,N,V,E,A,L,P,ee,te,re,ne,de,pe,ce,fe,ge,ye,be,he,Ce,ve,Fe,xe,Te],imports:[r.CommonModule,a.SharedModule,l.HomeComponentsModule,ue],exports:[T,q,S,I,k,N,V,E,A,L,P,ee,te,re,ne,de,pe,ce,fe,ge,ye,be,he,Ce,ve,Fe,xe,Te]})],e)}(),Se=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.checkMessageConfigForm},r.prototype.onConfigurationSet=function(e){this.checkMessageConfigForm=this.fb.group({messageNames:[e?e.messageNames:null,[]],metadataNames:[e?e.metadataNames:null,[]],checkAllKeys:[!!e&&e.checkAllKeys,[]]})},r.prototype.validateConfig=function(){var e=this.checkMessageConfigForm.get("messageNames").value,t=this.checkMessageConfigForm.get("metadataNames").value;return e.length>0||t.length>0},r.prototype.removeMessageName=function(e){var t=this.checkMessageConfigForm.get("messageNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("messageNames").setValue(t,{emitEvent:!0}))},r.prototype.removeMetadataName=function(e){var t=this.checkMessageConfigForm.get("metadataNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("metadataNames").setValue(t,{emitEvent:!0}))},r.prototype.addMessageName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("messageNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("messageNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.prototype.addMetadataName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("metadataNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("metadataNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-check-message-config",template:'
\n \n \n \n \n \n {{messageName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n \n \n \n \n {{metadataName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n {{ \'tb.rulenode.check-all-keys\' | translate }}\n \n
tb.rulenode.check-all-keys-hint
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ie=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.entitySearchDirection=Object.keys(a.EntitySearchDirection),n.entitySearchDirectionTranslationsMap=a.entitySearchDirectionTranslations,n}return b(r,e),r.prototype.configForm=function(){return this.checkRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.checkRelationConfigForm=this.fb.group({checkForSingleEntity:[!!e&&e.checkForSingleEntity,[]],direction:[e?e.direction:null,[]],entityType:[e?e.entityType:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],entityId:[e?e.entityId:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],relationType:[e?e.relationType:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["checkForSingleEntity"]},r.prototype.updateValidators=function(e){var t=this.checkRelationConfigForm.get("checkForSingleEntity").value;this.checkRelationConfigForm.get("entityType").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:e}),this.checkRelationConfigForm.get("entityId").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityId").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-check-relation-config",template:'
\n \n {{ \'tb.rulenode.check-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.check-relation-hint
\n \n relation.direction\n \n \n {{ entitySearchDirectionTranslationsMap.get(direction) | translate }}\n \n \n \n
\n \n \n \n \n
\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ke=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=D,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=G,n}return b(r,e),r.prototype.configForm=function(){return this.geoFilterConfigForm},r.prototype.onConfigurationSet=function(e){this.geoFilterConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoFilterConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoFilterConfigForm.get("perimeterType").value;t?this.geoFilterConfigForm.get("perimeterType").setValidators([]):this.geoFilterConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.CIRCLE?(this.geoFilterConfigForm.get("centerLatitude").setValidators([]),this.geoFilterConfigForm.get("centerLongitude").setValidators([]),this.geoFilterConfigForm.get("range").setValidators([]),this.geoFilterConfigForm.get("rangeUnit").setValidators([])):(this.geoFilterConfigForm.get("centerLatitude").setValidators([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoFilterConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoFilterConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoFilterConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoFilterConfigForm.get("polygonsDefinition").setValidators([]):this.geoFilterConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoFilterConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),this.geoFilterConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-gps-geofencing-config",template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ne=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.messageTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.messageTypeConfigForm=this.fb.group({messageTypes:[e?e.messageTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-message-type-config",template:'
\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ve=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allowedEntityTypes=[a.EntityType.DEVICE,a.EntityType.ASSET,a.EntityType.ENTITY_VIEW,a.EntityType.TENANT,a.EntityType.CUSTOMER,a.EntityType.USER,a.EntityType.DASHBOARD,a.EntityType.RULE_CHAIN,a.EntityType.RULE_NODE],n}return b(r,e),r.prototype.configForm=function(){return this.originatorTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorTypeConfigForm=this.fb.group({originatorTypes:[e?e.originatorTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-originator-type-config",template:'
\n \n \n \n
\n',styles:[":host ::ng-deep tb-entity-type-list .mat-form-field-flex{padding-top:0}:host ::ng-deep tb-entity-type-list .mat-form-field-infix{border-top:0}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ee=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"filter",this.translate.instant("tb.rulenode.filter"),"Filter",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-filter-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Ae=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.switchConfigForm},r.prototype.onConfigurationSet=function(e){this.switchConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.switchConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"switch",this.translate.instant("tb.rulenode.switch"),"Switch",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.switchConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-filter-node-switch-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Le=function(e){function r(t,r,n){var o,l,s=e.call(this,t)||this;s.store=t,s.translate=r,s.fb=n,s.alarmStatusTranslationsMap=a.alarmStatusTranslations,s.alarmStatusList=[],s.searchText="",s.displayStatusFn=s.displayStatus.bind(s);try{for(var m=v(Object.keys(a.AlarmStatus)),u=m.next();!u.done;u=m.next()){var d=u.value;s.alarmStatusList.push(a.AlarmStatus[d])}}catch(e){o={error:e}}finally{try{u&&!u.done&&(l=m.return)&&l.call(m)}finally{if(o)throw o.error}}return s.statusFormControl=new i.FormControl(""),s.filteredAlarmStatus=s.statusFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return s.fetchAlarmStatus(e)})),f.share()),s}return b(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.alarmStatusConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.statusFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.alarmStatusConfigForm=this.fb.group({alarmStatusList:[e?e.alarmStatusList:null,[i.Validators.required]]})},r.prototype.displayStatus=function(e){return e?this.translate.instant(a.alarmStatusTranslations.get(e)):void 0},r.prototype.fetchAlarmStatus=function(e){var t=this,r=this.getAlarmStatusList();if(this.searchText=e,this.searchText&&this.searchText.length){var n=this.searchText.toUpperCase();return c.of(r.filter((function(e){return t.translate.instant(a.alarmStatusTranslations.get(a.AlarmStatus[e])).toUpperCase().includes(n)})))}return c.of(r)},r.prototype.alarmStatusSelected=function(e){this.addAlarmStatus(e.option.value),this.clear("")},r.prototype.removeAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))}},r.prototype.addAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))},r.prototype.getAlarmStatusList=function(){var e=this;return this.alarmStatusList.filter((function(t){return-1===e.alarmStatusConfigForm.get("alarmStatusList").value.indexOf(t)}))},r.prototype.onAlarmStatusInputFocus=function(){this.statusFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.alarmStatusInput.nativeElement.value=e,this.statusFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.alarmStatusInput.nativeElement.blur(),t.alarmStatusInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},h([t.ViewChild("alarmStatusInput",{static:!1}),C("design:type",t.ElementRef)],r.prototype,"alarmStatusInput",void 0),r=h([t.Component({selector:"tb-filter-node-check-alarm-status-config",template:'
\n \n tb.rulenode.alarm-status-filter\n \n \n \n {{alarmStatusTranslationsMap.get(alarmStatus) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-alarm-status-matching\n
\n
\n
\n
\n
\n \n
\n\n\n\n'}),C("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Pe=function(){function e(){}return e=h([t.NgModule({declarations:[Se,Ie,ke,Ne,Ve,Ee,Ae,Le],imports:[r.CommonModule,a.SharedModule,ue],exports:[Se,Ie,ke,Ne,Ve,Ee,Ae,Le]})],e)}(),Me=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.customerAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.customerAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-customer-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),we=function(e){function r(t,r,n){var a,o,l=e.call(this,t)||this;l.store=t,l.translate=r,l.fb=n,l.entityDetailsTranslationsMap=z,l.entityDetailsList=[],l.searchText="",l.displayDetailsFn=l.displayDetails.bind(l);try{for(var s=v(Object.keys(B)),m=s.next();!m.done;m=s.next()){var u=m.value;l.entityDetailsList.push(B[u])}}catch(e){a={error:e}}finally{try{m&&!m.done&&(o=s.return)&&o.call(s)}finally{if(a)throw a.error}}return l.detailsFormControl=new i.FormControl(""),l.filteredEntityDetails=l.detailsFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return l.fetchEntityDetails(e)})),f.share()),l}return b(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.entityDetailsConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.detailsFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.entityDetailsConfigForm=this.fb.group({detailsList:[e?e.detailsList:null,[i.Validators.required]],addToMetadata:[!!e&&e.addToMetadata,[]]})},r.prototype.displayDetails=function(e){return e?this.translate.instant(z.get(e)):void 0},r.prototype.fetchEntityDetails=function(e){var t=this;if(this.searchText=e,this.searchText&&this.searchText.length){var r=this.searchText.toUpperCase();return c.of(this.entityDetailsList.filter((function(e){return t.translate.instant(z.get(B[e])).toUpperCase().includes(r)})))}return c.of(this.entityDetailsList)},r.prototype.detailsFieldSelected=function(e){this.addDetailsField(e.option.value),this.clear("")},r.prototype.removeDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.entityDetailsConfigForm.get("detailsList").setValue(t))}},r.prototype.addDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.entityDetailsConfigForm.get("detailsList").setValue(t))},r.prototype.onEntityDetailsInputFocus=function(){this.detailsFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.detailsInput.nativeElement.value=e,this.detailsFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.detailsInput.nativeElement.blur(),t.detailsInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},h([t.ViewChild("detailsInput",{static:!1}),C("design:type",t.ElementRef)],r.prototype,"detailsInput",void 0),r=h([t.Component({selector:"tb-enrichment-node-entity-details-config",template:'
\n \n tb.rulenode.entity-details\n \n \n \n {{entityDetailsTranslationsMap.get(details) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-entity-details-matching\n
\n
\n
\n
\n
\n \n \n {{ \'tb.rulenode.add-to-metadata\' | translate }}\n \n
tb.rulenode.add-to-metadata-hint
\n
\n',styles:[":host ::ng-deep mat-form-field.entity-fields-list .mat-form-field-wrapper{margin-bottom:-1.25em}"]}),C("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.deviceAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.deviceAttributesConfigForm=this.fb.group({deviceRelationsQuery:[e?e.deviceRelationsQuery:null,[i.Validators.required]],tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.deviceAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.deviceAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.deviceAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.deviceAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-device-attributes-config",template:'
\n \n \n \n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
tb.rulenode.get-latest-value-with-ts-hint
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),De=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.originatorAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorAttributesConfigForm=this.fb.group({tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.originatorAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.originatorAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.originatorAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.originatorAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-originator-attributes-config",template:'
\n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
tb.rulenode.get-latest-value-with-ts-hint
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Oe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.originatorFieldsConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorFieldsConfigForm=this.fb.group({fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-originator-fields-config",template:'
\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ke=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n.fetchMode=H,n.fetchModes=Object.keys(H),n.samplingOrders=Object.keys(U),n.timeUnits=Object.keys(R),n.timeUnitsTranslationMap=K,n}return b(r,e),r.prototype.configForm=function(){return this.getTelemetryFromDatabaseConfigForm},r.prototype.onConfigurationSet=function(e){this.getTelemetryFromDatabaseConfigForm=this.fb.group({latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],fetchMode:[e?e.fetchMode:null,[i.Validators.required]],orderBy:[e?e.orderBy:null,[]],limit:[e?e.limit:null,[]],useMetadataIntervalPatterns:[!!e&&e.useMetadataIntervalPatterns,[]],startInterval:[e?e.startInterval:null,[]],startIntervalTimeUnit:[e?e.startIntervalTimeUnit:null,[]],endInterval:[e?e.endInterval:null,[]],endIntervalTimeUnit:[e?e.endIntervalTimeUnit:null,[]],startIntervalPattern:[e?e.startIntervalPattern:null,[]],endIntervalPattern:[e?e.endIntervalPattern:null,[]]})},r.prototype.validatorTriggers=function(){return["fetchMode","useMetadataIntervalPatterns"]},r.prototype.updateValidators=function(e){var t=this.getTelemetryFromDatabaseConfigForm.get("fetchMode").value,r=this.getTelemetryFromDatabaseConfigForm.get("useMetadataIntervalPatterns").value;t&&t===H.ALL?(this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([i.Validators.required,i.Validators.min(2),i.Validators.max(1e3)])):(this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([])),r?(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([i.Validators.required])):(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([])),this.getTelemetryFromDatabaseConfigForm.get("orderBy").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("limit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").updateValueAndValidity({emitEvent:e})},r.prototype.removeKey=function(e,t){var r=this.getTelemetryFromDatabaseConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.getTelemetryFromDatabaseConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-get-telemetry-from-database",template:'
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n tb.rulenode.fetch-mode\n \n \n {{ mode }}\n \n \n tb.rulenode.fetch-mode-hint\n \n
\n \n tb.rulenode.order-by\n \n \n {{ order }}\n \n \n tb.rulenode.order-by-hint\n \n \n tb.rulenode.limit\n \n tb.rulenode.limit-hint\n \n
\n \n {{ \'tb.rulenode.use-metadata-interval-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-interval-patterns-hint
\n
\n
\n \n tb.rulenode.start-interval\n \n \n {{ \'tb.rulenode.start-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.start-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.end-interval\n \n \n {{ \'tb.rulenode.end-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.end-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n \n tb.rulenode.start-interval-pattern\n \n \n {{ \'tb.rulenode.start-interval-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.end-interval-pattern\n \n \n {{ \'tb.rulenode.end-interval-pattern-required\' | translate }}\n \n \n \n \n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Be=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.relatedAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.relatedAttributesConfigForm=this.fb.group({relationsQuery:[e?e.relationsQuery:null,[i.Validators.required]],telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-related-attributes-config",template:'
\n \n \n \n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ge=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.tenantAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.tenantAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-tenant-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),He=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.calculateDeltaConfigForm},r.prototype.onConfigurationSet=function(e){this.calculateDeltaConfigForm=this.fb.group({inputValueKey:[e?e.inputValueKey:null,[i.Validators.required]],outputValueKey:[e?e.outputValueKey:null,[i.Validators.required]],useCache:[e?e.useCache:null,[]],addPeriodBetweenMsgs:[!!e&&e.addPeriodBetweenMsgs,[]],periodValueKey:[e?e.periodValueKey:null,[]],round:[e?e.round:null,[i.Validators.min(0),i.Validators.max(15)]],tellFailureIfDeltaIsNegative:[e?e.tellFailureIfDeltaIsNegative:null,[]]})},r.prototype.updateValidators=function(e){this.calculateDeltaConfigForm.get("addPeriodBetweenMsgs").value?this.calculateDeltaConfigForm.get("periodValueKey").setValidators([i.Validators.required]):this.calculateDeltaConfigForm.get("periodValueKey").setValidators([]),this.calculateDeltaConfigForm.get("periodValueKey").updateValueAndValidity({emitEvent:e})},r.prototype.validatorTriggers=function(){return["addPeriodBetweenMsgs"]},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-calculate-delta-config",template:'
\n
\n \n tb.rulenode.input-value-key\n \n \n {{ \'tb.rulenode.input-value-key-required\' | translate }}\n \n \n \n tb.rulenode.output-value-key\n \n \n {{ \'tb.rulenode.output-value-key-required\' | translate }}\n \n \n \n tb.rulenode.round\n \n \n {{ \'tb.rulenode.round-range\' | translate }}\n \n \n {{ \'tb.rulenode.round-range\' | translate }}\n \n \n
\n \n {{ \'tb.rulenode.use-cache\' | translate }}\n \n \n {{ \'tb.rulenode.tell-failure-if-delta-is-negative\' | translate }}\n \n \n {{ \'tb.rulenode.add-period-between-msgs\' | translate }}\n \n \n tb.rulenode.period-value-key\n \n \n {{ \'tb.rulenode.period-value-key-required\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ue=function(){function e(){}return e=h([t.NgModule({declarations:[Me,we,Re,De,Oe,Ke,Be,Ge,He],imports:[r.CommonModule,a.SharedModule,ue],exports:[Me,we,Re,De,Oe,Ke,Be,Ge,He]})],e)}(),je=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.originatorSource=F,n.originatorSources=Object.keys(F),n.originatorSourceTranslationMap=w,n}return b(r,e),r.prototype.configForm=function(){return this.changeOriginatorConfigForm},r.prototype.onConfigurationSet=function(e){this.changeOriginatorConfigForm=this.fb.group({originatorSource:[e?e.originatorSource:null,[i.Validators.required]],relationsQuery:[e?e.relationsQuery:null,[]]})},r.prototype.validatorTriggers=function(){return["originatorSource"]},r.prototype.updateValidators=function(e){var t=this.changeOriginatorConfigForm.get("originatorSource").value;t&&t===F.RELATED?this.changeOriginatorConfigForm.get("relationsQuery").setValidators([i.Validators.required]):this.changeOriginatorConfigForm.get("relationsQuery").setValidators([]),this.changeOriginatorConfigForm.get("relationsQuery").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-transformation-node-change-originator-config",template:'
\n \n tb.rulenode.originator-source\n \n \n {{ originatorSourceTranslationMap.get(source) | translate }}\n \n \n \n
\n \n \n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ze=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"update",this.translate.instant("tb.rulenode.transformer"),"Transform",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-transformation-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Qe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.toEmailConfigForm},r.prototype.onConfigurationSet=function(e){this.toEmailConfigForm=this.fb.group({fromTemplate:[e?e.fromTemplate:null,[i.Validators.required]],toTemplate:[e?e.toTemplate:null,[i.Validators.required]],ccTemplate:[e?e.ccTemplate:null,[]],bccTemplate:[e?e.bccTemplate:null,[]],subjectTemplate:[e?e.subjectTemplate:null,[i.Validators.required]],bodyTemplate:[e?e.bodyTemplate:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-transformation-node-to-email-config",template:'
\n \n tb.rulenode.from-template\n \n \n {{ \'tb.rulenode.from-template-required\' | translate }}\n \n \n \n \n tb.rulenode.to-template\n \n \n {{ \'tb.rulenode.to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.cc-template\n \n \n \n \n tb.rulenode.bcc-template\n \n \n \n \n tb.rulenode.subject-template\n \n \n {{ \'tb.rulenode.subject-template-required\' | translate }}\n \n \n \n \n tb.rulenode.body-template\n \n \n {{ \'tb.rulenode.body-template-required\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),_e=function(){function e(){}return e=h([t.NgModule({declarations:[je,ze,Qe],imports:[r.CommonModule,a.SharedModule,ue],exports:[je,ze,Qe]})],e)}(),$e=function(){function e(e){!function(e){e.setTranslation("en_US",{tb:{rulenode:{"create-entity-if-not-exists":"Create new entity if not exists","create-entity-if-not-exists-hint":"Create a new entity set above if it does not exist.","entity-name-pattern":"Name pattern","entity-name-pattern-required":"Name pattern is required","entity-type-pattern":"Type pattern","entity-type-pattern-required":"Type pattern is required","entity-cache-expiration":"Entities cache expiration time (sec)","entity-cache-expiration-hint":"Specifies maximum time interval allowed to store found entity records. 0 value means that records will never expire.","entity-cache-expiration-required":"Entities cache expiration time is required.","entity-cache-expiration-range":"Entities cache expiration time should be greater than or equal to 0.","customer-name-pattern":"Customer name pattern","customer-name-pattern-required":"Customer name pattern is required","create-customer-if-not-exists":"Create new customer if not exists","customer-cache-expiration":"Customers cache expiration time (sec)","customer-cache-expiration-hint":"Specifies maximum time interval allowed to store found customer records. 0 value means that records will never expire.","customer-cache-expiration-required":"Customers cache expiration time is required.","customer-cache-expiration-range":"Customers cache expiration time should be greater than or equal to 0.","start-interval":"Start Interval","end-interval":"End Interval","start-interval-time-unit":"Start Interval Time Unit","end-interval-time-unit":"End Interval Time Unit","fetch-mode":"Fetch mode","fetch-mode-hint":"If selected fetch mode 'ALL' you able to choose telemetry sampling order.","order-by":"Order by","order-by-hint":"Select to choose telemetry sampling order.",limit:"Limit","limit-hint":"Min limit value is 2, max - 1000. In case you want to fetch a single entry, select fetch mode 'FIRST' or 'LAST'.","time-unit-milliseconds":"Milliseconds","time-unit-seconds":"Seconds","time-unit-minutes":"Minutes","time-unit-hours":"Hours","time-unit-days":"Days","time-value-range":"Time value should be in a range from 1 to 2147483647.","start-interval-value-required":"Start interval value is required.","end-interval-value-required":"End interval value is required.",filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","notify-device":"Notify Device","notify-device-hint":"If the message arrives from the device, we will push it back to the device by default.","latest-timeseries":"Latest timeseries","data-keys":"Message data","metadata-keys":"Message metadata","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","relation-type-pattern":"Relation type pattern","relation-type-pattern-required":"Relation type pattern is required","relation-types-list":"Relation types to propagate","relation-types-list-hint":"If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","fields-mapping":"Fields mapping","fields-mapping-required":"At least one field mapping should be specified.","source-field":"Source field","source-field-required":"Source field is required.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","originator-alarm-originator":"Alarm Originator","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","use-metadata-period-in-seconds-patterns":"Use period in seconds pattern","use-metadata-period-in-seconds-patterns-hint":"If selected, rule node use period in seconds interval pattern from message metadata or data assuming that intervals are in the seconds.","period-in-seconds-pattern":"Period in seconds pattern","period-in-seconds-pattern-required":"Period in seconds pattern is required","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required","alarm-status-filter":"Alarm status filter","alarm-status-list-empty":"Alarm status list is empty","no-alarm-status-matching":"No alarm status matching were found.",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":'Comma separated address list, use ${metadataKey} for value from metadata, $[messageKey] for value from message body',"cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","body-template":"Body Template","body-template-required":"Body Template is required","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","request-method":"Request method","use-simple-client-http-factory":"Use simple client HTTP factory","read-timeout":"Read timeout in millis","read-timeout-hint":"The value of 0 means an infinite timeout","max-parallel-requests-count":"Max number of parallel requests","max-parallel-requests-count-hint":"The value of 0 specifies no limit in parallel processing",headers:"Headers","headers-hint":'Use ${metadataKey} for value from metadata, $[messageKey] for value from message body in header/value fields',header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required",topic:"Topic","topic-required":"Topic is required","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","gcp-project-id":"GCP project ID","gcp-project-id-required":"GCP project ID is required","gcp-service-account-key":"GCP service account key file","gcp-service-account-key-required":"GCP service account key file is required","pubsub-topic-name":"Topic name","pubsub-topic-name-required":"Topic name is required","message-attributes":"Message attributes","message-attributes-hint":'Use ${metadataKey} for value from metadata, $[messageKey] for value from message body in name/value fields',"connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","device-id":"Device ID","device-id-required":"Device ID is required.","clean-session":"Clean session","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","credentials-pem-hint":"At least Server CA certificate file or a pair of Client certificate and Client private key files are required","credentials-sas":"Shared Access Signature","sas-key":"SAS Key","sas-key-required":"SAS Key is required.",hostname:"Hostname","hostname-required":"Hostname is required.","azure-ca-cert":"CA certificate file","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"Server CA certificate file *","private-key":"Client private key file *",cert:"Client certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","use-metadata-interval-patterns":"Use interval patterns","use-metadata-interval-patterns-hint":"If selected, rule node use start and end interval patterns from message metadata or data assuming that intervals are in the milliseconds.","use-message-alarm-data":"Use message alarm data","check-all-keys":"Check that all selected keys are present","check-all-keys-hint":"If selected, checks that all specified keys are present in the message data and metadata.","check-relation-to-specific-entity":"Check relation to specific entity","check-relation-hint":"Checks existence of relation to specific entity or to any entity based on direction and relation type.","delete-relation-to-specific-entity":"Delete relation to specific entity","delete-relation-hint":"Deletes relation from the originator of the incoming message to the specified entity or list of entities based on direction and type.","remove-current-relations":"Remove current relations","remove-current-relations-hint":"Removes current relations from the originator of the incoming message based on direction and type.","change-originator-to-related-entity":"Change originator to related entity","change-originator-to-related-entity-hint":"Used to process submitted message as a message from another entity.","start-interval-pattern":"Start interval pattern","end-interval-pattern":"End interval pattern","start-interval-pattern-required":"Start interval pattern is required","end-interval-pattern-required":"End interval pattern is required","smtp-protocol":"Protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS","tls-version":"TLS version","enable-proxy":"Enable proxy","use-system-proxy-properties":"Use system proxy properties","proxy-host":"Proxy host","proxy-host-required":"Proxy host is required.","proxy-port":"Proxy port","proxy-port-required":"Proxy port is required.","proxy-port-range":"Proxy port should be in a range from 1 to 65535.","proxy-user":"Proxy user","proxy-password":"Proxy password","proxy-scheme":"Proxy scheme","numbers-to-template":"Phone Numbers To Template","numbers-to-template-required":"Phone Numbers To Template is required","numbers-to-template-hint":'Comma separated Phone Numbers, use ${metadataKey} for value from metadata, $[messageKey] for value from message body',"sms-message-template":"SMS message Template","sms-message-template-required":"SMS message Template is required","use-system-sms-settings":"Use system SMS provider settings","min-period-0-seconds-message":"Only 0 second minimum period is allowed.","max-pending-messages":"Maximum pending messages","max-pending-messages-required":"Maximum pending messages is required.","max-pending-messages-range":"Maximum pending messages should be in a range from 1 to 100000.","originator-types-filter":"Originator types filter","interval-seconds":"Interval in seconds","interval-seconds-required":"Interval is required.","min-interval-seconds-message":"Only 1 second minimum interval is allowed.","output-timeseries-key-prefix":"Output timeseries key prefix","output-timeseries-key-prefix-required":"Output timeseries key prefix required.","separator-hint":'You should press "enter" to complete field input.',"entity-details":"Select entity details:","entity-details-title":"Title","entity-details-country":"Country","entity-details-state":"State","entity-details-zip":"Zip","entity-details-address":"Address","entity-details-address2":"Address2","entity-details-additional_info":"Additional Info","entity-details-phone":"Phone","entity-details-email":"Email","add-to-metadata":"Add selected details to message metadata","add-to-metadata-hint":"If selected, adds the selected details keys to the message metadata instead of message data.","entity-details-list-empty":"No entity details selected.","no-entity-details-matching":"No entity details matching were found.","custom-table-name":"Custom table name","custom-table-name-required":"Table Name is required","custom-table-hint":"You should enter the table name without prefix 'cs_tb_'.","message-field":"Message field","message-field-required":"Message field is required.","table-col":"Table column","table-col-required":"Table column is required.","latitude-key-name":"Latitude key name","longitude-key-name":"Longitude key name","latitude-key-name-required":"Latitude key name is required.","longitude-key-name-required":"Longitude key name is required.","fetch-perimeter-info-from-message-metadata":"Fetch perimeter information from message metadata","perimeter-circle":"Circle","perimeter-polygon":"Polygon","perimeter-type":"Perimeter type","circle-center-latitude":"Center latitude","circle-center-latitude-required":"Center latitude is required.","circle-center-longitude":"Center longitude","circle-center-longitude-required":"Center longitude is required.","range-unit-meter":"Meter","range-unit-kilometer":"Kilometer","range-unit-foot":"Foot","range-unit-mile":"Mile","range-unit-nautical-mile":"Nautical mile","range-units":"Range units",range:"Range","range-required":"Range is required.","polygon-definition":"Polygon definition","polygon-definition-required":"Polygon definition is required.","polygon-definition-hint":"Please, use the following format for manual definition of polygon: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].","min-inside-duration":"Minimal inside duration","min-inside-duration-value-required":"Minimal inside duration is required","min-inside-duration-time-unit":"Minimal inside duration time unit","min-outside-duration":"Minimal outside duration","min-outside-duration-value-required":"Minimal outside duration is required","min-outside-duration-time-unit":"Minimal outside duration time unit","tell-failure-if-absent":"Tell Failure","tell-failure-if-absent-hint":'If at least one selected key doesn\'t exist the outbound message will report "Failure".',"get-latest-value-with-ts":"Fetch Latest telemetry with Timestamp","get-latest-value-with-ts-hint":'If selected, latest telemetry values will be added to the outbound message metadata with timestamp, e.g: "temp": "{\\"ts\\":1574329385897,\\"value\\":42}"',"use-redis-queue":"Use redis queue for message persistence","trim-redis-queue":"Trim redis queue","redis-queue-max-size":"Redis queue max size","add-metadata-key-values-as-kafka-headers":"Add Message metadata key-value pairs to Kafka record headers","add-metadata-key-values-as-kafka-headers-hint":"If selected, key-value pairs from message metadata will be added to the outgoing records headers as byte arrays with predefined charset encoding.","charset-encoding":"Charset encoding","charset-encoding-required":"Charset encoding is required.","charset-us-ascii":"US-ASCII","charset-iso-8859-1":"ISO-8859-1","charset-utf-8":"UTF-8","charset-utf-16be":"UTF-16BE","charset-utf-16le":"UTF-16LE","charset-utf-16":"UTF-16","select-queue-hint":"The queue name can be selected from a drop-down list or add a custom name.","persist-alarm-rules":"Persist state of alarm rules","fetch-alarm-rules":"Fetch state of alarm rules","input-value-key":"Input value key","input-value-key-required":"Input value key is required.","output-value-key":"Output value key","output-value-key-required":"Output value key is required.",round:"Decimals","round-range":"Decimals should be in a range from 0 to 15.","use-cache":"Use cache for latest value","tell-failure-if-delta-is-negative":"Tell Failure if delta is negative","add-period-between-msgs":"Add period between messages","period-value-key":"Period value key","period-key-required":"Period value key is required.","general-pattern-hint":'Hint: use ${metadataKey} for value from metadata, $[messageKey] for value from message body'},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}},!0)}(e)}return e.ctorParameters=function(){return[{type:n.TranslateService}]},e=h([t.NgModule({declarations:[x],imports:[r.CommonModule,a.SharedModule],exports:[qe,Pe,Ue,_e,x]}),C("design:paramtypes",[n.TranslateService])],e)}();e.RuleNodeCoreConfigModule=$e,e.ɵa=x,e.ɵb=qe,e.ɵba=ve,e.ɵbb=Fe,e.ɵbc=xe,e.ɵbd=Te,e.ɵbe=ue,e.ɵbf=ae,e.ɵbg=oe,e.ɵbh=ie,e.ɵbi=le,e.ɵbj=se,e.ɵbk=me,e.ɵbl=Pe,e.ɵbm=Se,e.ɵbn=Ie,e.ɵbo=ke,e.ɵbp=Ne,e.ɵbq=Ve,e.ɵbr=Ee,e.ɵbs=Ae,e.ɵbt=Le,e.ɵbu=Ue,e.ɵbv=Me,e.ɵbw=we,e.ɵbx=Re,e.ɵby=De,e.ɵbz=Oe,e.ɵc=T,e.ɵca=Ke,e.ɵcb=Be,e.ɵcc=Ge,e.ɵcd=He,e.ɵce=_e,e.ɵcf=je,e.ɵcg=ze,e.ɵch=Qe,e.ɵd=q,e.ɵe=S,e.ɵf=I,e.ɵg=k,e.ɵh=N,e.ɵi=V,e.ɵj=E,e.ɵk=A,e.ɵl=L,e.ɵm=P,e.ɵn=ee,e.ɵo=te,e.ɵp=re,e.ɵq=ne,e.ɵr=de,e.ɵs=pe,e.ɵt=ce,e.ɵu=fe,e.ɵv=ge,e.ɵw=ye,e.ɵx=be,e.ɵy=he,e.ɵz=Ce,Object.defineProperty(e,"__esModule",{value:!0})})); + ***************************************************************************** */var y=function(e,t){return(y=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(e[r]=t[r])})(e,t)};function b(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function r(){this.constructor=e}y(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}function h(e,t,r,n){var a,o=arguments.length,i=o<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,r):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)i=Reflect.decorate(e,t,r,n);else for(var l=e.length-1;l>=0;l--)(a=e[l])&&(i=(o<3?a(i):o>3?a(t,r,i):a(t,r))||i);return o>3&&i&&Object.defineProperty(t,r,i),i}function C(e,t){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(e,t)}Object.create;function v(e){var t="function"==typeof Symbol&&Symbol.iterator,r=t&&e[t],n=0;if(r)return r.call(e);if(e&&"number"==typeof e.length)return{next:function(){return e&&n>=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}Object.create;var F,x=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.emptyConfigForm},r.prototype.onConfigurationSet=function(e){this.emptyConfigForm=this.fb.group({})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-node-empty-config",template:"
"}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),T=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.attributeScopes=Object.keys(a.AttributeScope),n.telemetryTypeTranslationsMap=a.telemetryTypeTranslations,n}return b(r,e),r.prototype.configForm=function(){return this.attributesConfigForm},r.prototype.onConfigurationSet=function(e){this.attributesConfigForm=this.fb.group({scope:[e?e.scope:null,[i.Validators.required]],notifyDevice:[!e||e.scope,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-attributes-config",template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.notify-device\' | translate }}\n \n
tb.rulenode.notify-device-hint
\n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),q=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.timeseriesConfigForm},r.prototype.onConfigurationSet=function(e){this.timeseriesConfigForm=this.fb.group({defaultTTL:[e?e.defaultTTL:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-timeseries-config",template:'
\n \n tb.rulenode.default-ttl\n \n \n {{ \'tb.rulenode.default-ttl-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-default-ttl-message\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),S=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.rpcRequestConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcRequestConfigForm=this.fb.group({timeoutInSeconds:[e?e.timeoutInSeconds:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-rpc-request-config",template:'
\n \n tb.rulenode.timeout-sec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-message\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),I=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.logConfigForm},r.prototype.onConfigurationSet=function(e){this.logConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.logConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"string",this.translate.instant("tb.rulenode.to-string"),"ToString",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.logConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-log-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),k=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.assignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.assignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required,i.Validators.pattern(/.*\S.*/)]],createCustomerIfNotExists:[!!e&&e.createCustomerIfNotExists,[]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.prepareOutputConfig=function(e){return e.customerNamePattern=e.customerNamePattern.trim(),e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.create-customer-if-not-exists\' | translate }}\n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),N=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.clearAlarmConfigForm},r.prototype.onConfigurationSet=function(e){this.clearAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],alarmType:[e?e.alarmType:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.clearAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.clearAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-clear-alarm-config",template:'
\n \n \n \n
\n \n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),V=function(e){function r(t,r,n,o){var i=e.call(this,t)||this;return i.store=t,i.fb=r,i.nodeScriptTestService=n,i.translate=o,i.alarmSeverities=Object.keys(a.AlarmSeverity),i.alarmSeverityTranslationMap=a.alarmSeverityTranslations,i.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],i}return b(r,e),r.prototype.configForm=function(){return this.createAlarmConfigForm},r.prototype.onConfigurationSet=function(e){var t=this;this.createAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],useMessageAlarmData:[!!e&&e.useMessageAlarmData,[]],alarmType:[e?e.alarmType:null,[]],severity:[e?e.severity:null,[]],propagate:[!!e&&e.propagate,[]],relationTypes:[e?e.relationTypes:null,[]],dynamicSeverity:!1}),this.createAlarmConfigForm.get("dynamicSeverity").valueChanges.subscribe((function(e){e?t.createAlarmConfigForm.get("severity").patchValue("",{emitEvent:!1}):t.createAlarmConfigForm.get("severity").patchValue(t.alarmSeverities[0],{emitEvent:!1})}))},r.prototype.validatorTriggers=function(){return["useMessageAlarmData"]},r.prototype.updateValidators=function(e){this.createAlarmConfigForm.get("useMessageAlarmData").value?(this.createAlarmConfigForm.get("alarmType").setValidators([]),this.createAlarmConfigForm.get("severity").setValidators([])):(this.createAlarmConfigForm.get("alarmType").setValidators([i.Validators.required]),this.createAlarmConfigForm.get("severity").setValidators([i.Validators.required])),this.createAlarmConfigForm.get("alarmType").updateValueAndValidity({emitEvent:e}),this.createAlarmConfigForm.get("severity").updateValueAndValidity({emitEvent:e})},r.prototype.testScript=function(){var e=this,t=this.createAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.createAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.removeKey=function(e,t){var r=this.createAlarmConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.createAlarmConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.createAlarmConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.createAlarmConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-create-alarm-config",template:'
\n \n \n \n
\n \n
\n
\n \n {{ \'tb.rulenode.use-message-alarm-data\' | translate }}\n \n \n {{ \'tb.rulenode.use-dynamically-change-the-severity-of-alar\' | translate }}\n \n
\n
\n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n \n tb.rulenode.alarm-severity\n \n \n {{ alarmSeverityTranslationMap.get(severity) | translate }}\n \n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.alarm-severity\' | translate }}\n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.propagate\' | translate }}\n \n
\n \n tb.rulenode.relation-types-list\n \n \n {{key}}\n close\n \n \n \n tb.rulenode.relation-types-list-hint\n \n
\n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),E=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return b(r,e),r.prototype.configForm=function(){return this.createRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.createRelationConfigForm=this.fb.group({direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[i.Validators.required]],entityNamePattern:[e?e.entityNamePattern:null,[]],entityTypePattern:[e?e.entityTypePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],createEntityIfNotExists:[!!e&&e.createEntityIfNotExists,[]],removeCurrentRelations:[!!e&&e.removeCurrentRelations,[]],changeOriginatorToRelatedEntity:[!!e&&e.changeOriginatorToRelatedEntity,[]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["entityType"]},r.prototype.updateValidators=function(e){var t=this.createRelationConfigForm.get("entityType").value;t?this.createRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required,i.Validators.pattern(/.*\S.*/)]):this.createRelationConfigForm.get("entityNamePattern").setValidators([]),!t||t!==a.EntityType.DEVICE&&t!==a.EntityType.ASSET?this.createRelationConfigForm.get("entityTypePattern").setValidators([]):this.createRelationConfigForm.get("entityTypePattern").setValidators([i.Validators.required,i.Validators.pattern(/.*\S.*/)]),this.createRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e}),this.createRelationConfigForm.get("entityTypePattern").updateValueAndValidity({emitEvent:e})},r.prototype.prepareOutputConfig=function(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e.entityTypePattern=e.entityTypePattern?e.entityTypePattern.trim():null,e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-create-relation-config",template:'
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-type-pattern\n \n \n {{ \'tb.rulenode.entity-type-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.create-entity-if-not-exists\' | translate }}\n \n
tb.rulenode.create-entity-if-not-exists-hint
\n
\n \n {{ \'tb.rulenode.remove-current-relations\' | translate }}\n \n
tb.rulenode.remove-current-relations-hint
\n \n {{ \'tb.rulenode.change-originator-to-related-entity\' | translate }}\n \n
tb.rulenode.change-originator-to-related-entity-hint
\n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),A=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.msgDelayConfigForm},r.prototype.onConfigurationSet=function(e){this.msgDelayConfigForm=this.fb.group({useMetadataPeriodInSecondsPatterns:[!!e&&e.useMetadataPeriodInSecondsPatterns,[]],periodInSeconds:[e?e.periodInSeconds:null,[]],periodInSecondsPattern:[e?e.periodInSecondsPattern:null,[]],maxPendingMsgs:[e?e.maxPendingMsgs:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(1e5)]]})},r.prototype.validatorTriggers=function(){return["useMetadataPeriodInSecondsPatterns"]},r.prototype.updateValidators=function(e){this.msgDelayConfigForm.get("useMetadataPeriodInSecondsPatterns").value?(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([i.Validators.required]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([])):(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([i.Validators.required,i.Validators.min(0)])),this.msgDelayConfigForm.get("periodInSecondsPattern").updateValueAndValidity({emitEvent:e}),this.msgDelayConfigForm.get("periodInSeconds").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-msg-delay-config",template:'
\n \n {{ \'tb.rulenode.use-metadata-period-in-seconds-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-period-in-seconds-patterns-hint
\n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-0-seconds-message\' | translate }}\n \n \n \n \n tb.rulenode.period-in-seconds-pattern\n \n \n {{ \'tb.rulenode.period-in-seconds-pattern-required\' | translate }}\n \n \n \n \n \n tb.rulenode.max-pending-messages\n \n \n {{ \'tb.rulenode.max-pending-messages-required\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),L=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return b(r,e),r.prototype.configForm=function(){return this.deleteRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.deleteRelationConfigForm=this.fb.group({deleteForSingleEntity:[!!e&&e.deleteForSingleEntity,[]],direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[]],entityNamePattern:[e?e.entityNamePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["deleteForSingleEntity","entityType"]},r.prototype.updateValidators=function(e){var t=this.deleteRelationConfigForm.get("deleteForSingleEntity").value,r=this.deleteRelationConfigForm.get("entityType").value;t?this.deleteRelationConfigForm.get("entityType").setValidators([i.Validators.required]):this.deleteRelationConfigForm.get("entityType").setValidators([]),t&&r?this.deleteRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required,i.Validators.pattern(/.*\S.*/)]):this.deleteRelationConfigForm.get("entityNamePattern").setValidators([]),this.deleteRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:!1}),this.deleteRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e})},r.prototype.prepareOutputConfig=function(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-delete-relation-config",template:'
\n \n {{ \'tb.rulenode.delete-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.delete-relation-hint
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),P=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.generatorConfigForm},r.prototype.onConfigurationSet=function(e){this.generatorConfigForm=this.fb.group({msgCount:[e?e.msgCount:null,[i.Validators.required,i.Validators.min(0)]],periodInSeconds:[e?e.periodInSeconds:null,[i.Validators.required,i.Validators.min(1)]],originator:[e?e.originator:null,[]],jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.prepareInputConfig=function(e){return e&&(e.originatorId&&e.originatorType?e.originator={id:e.originatorId,entityType:e.originatorType}:e.originator=null,delete e.originatorId,delete e.originatorType),e},r.prototype.prepareOutputConfig=function(e){return e.originator?(e.originatorId=e.originator.id,e.originatorType=e.originator.entityType):(e.originatorId=null,e.originatorType=null),delete e.originator,e},r.prototype.testScript=function(){var e=this,t=this.generatorConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"generate",this.translate.instant("tb.rulenode.generator"),"Generate",["prevMsg","prevMetadata","prevMsgType"],this.ruleNodeId).subscribe((function(t){t&&e.generatorConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-generator-config",template:'
\n \n tb.rulenode.message-count\n \n \n {{ \'tb.rulenode.message-count-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-message-count-message\' | translate }}\n \n \n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-seconds-message\' | translate }}\n \n \n
\n \n \n \n
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent);!function(e){e.CUSTOMER="CUSTOMER",e.TENANT="TENANT",e.RELATED="RELATED",e.ALARM_ORIGINATOR="ALARM_ORIGINATOR"}(F||(F={}));var M,w=new Map([[F.CUSTOMER,"tb.rulenode.originator-customer"],[F.TENANT,"tb.rulenode.originator-tenant"],[F.RELATED,"tb.rulenode.originator-related"],[F.ALARM_ORIGINATOR,"tb.rulenode.originator-alarm-originator"]]);!function(e){e.CIRCLE="CIRCLE",e.POLYGON="POLYGON"}(M||(M={}));var R,D=new Map([[M.CIRCLE,"tb.rulenode.perimeter-circle"],[M.POLYGON,"tb.rulenode.perimeter-polygon"]]);!function(e){e.MILLISECONDS="MILLISECONDS",e.SECONDS="SECONDS",e.MINUTES="MINUTES",e.HOURS="HOURS",e.DAYS="DAYS"}(R||(R={}));var O,K=new Map([[R.MILLISECONDS,"tb.rulenode.time-unit-milliseconds"],[R.SECONDS,"tb.rulenode.time-unit-seconds"],[R.MINUTES,"tb.rulenode.time-unit-minutes"],[R.HOURS,"tb.rulenode.time-unit-hours"],[R.DAYS,"tb.rulenode.time-unit-days"]]);!function(e){e.METER="METER",e.KILOMETER="KILOMETER",e.FOOT="FOOT",e.MILE="MILE",e.NAUTICAL_MILE="NAUTICAL_MILE"}(O||(O={}));var B,G=new Map([[O.METER,"tb.rulenode.range-unit-meter"],[O.KILOMETER,"tb.rulenode.range-unit-kilometer"],[O.FOOT,"tb.rulenode.range-unit-foot"],[O.MILE,"tb.rulenode.range-unit-mile"],[O.NAUTICAL_MILE,"tb.rulenode.range-unit-nautical-mile"]]);!function(e){e.TITLE="TITLE",e.COUNTRY="COUNTRY",e.STATE="STATE",e.ZIP="ZIP",e.ADDRESS="ADDRESS",e.ADDRESS2="ADDRESS2",e.PHONE="PHONE",e.EMAIL="EMAIL",e.ADDITIONAL_INFO="ADDITIONAL_INFO"}(B||(B={}));var H,U,j,z=new Map([[B.TITLE,"tb.rulenode.entity-details-title"],[B.COUNTRY,"tb.rulenode.entity-details-country"],[B.STATE,"tb.rulenode.entity-details-state"],[B.ZIP,"tb.rulenode.entity-details-zip"],[B.ADDRESS,"tb.rulenode.entity-details-address"],[B.ADDRESS2,"tb.rulenode.entity-details-address2"],[B.PHONE,"tb.rulenode.entity-details-phone"],[B.EMAIL,"tb.rulenode.entity-details-email"],[B.ADDITIONAL_INFO,"tb.rulenode.entity-details-additional_info"]]);!function(e){e.FIRST="FIRST",e.LAST="LAST",e.ALL="ALL"}(H||(H={})),function(e){e.ASC="ASC",e.DESC="DESC"}(U||(U={})),function(e){e.STANDARD="STANDARD",e.FIFO="FIFO"}(j||(j={}));var Q,_=new Map([[j.STANDARD,"tb.rulenode.sqs-queue-standard"],[j.FIFO,"tb.rulenode.sqs-queue-fifo"]]),$=["anonymous","basic","cert.PEM"],W=new Map([["anonymous","tb.rulenode.credentials-anonymous"],["basic","tb.rulenode.credentials-basic"],["cert.PEM","tb.rulenode.credentials-pem"]]),J=["sas","cert.PEM"],Y=new Map([["sas","tb.rulenode.credentials-sas"],["cert.PEM","tb.rulenode.credentials-pem"]]);!function(e){e.GET="GET",e.POST="POST",e.PUT="PUT",e.DELETE="DELETE"}(Q||(Q={}));var Z=["US-ASCII","ISO-8859-1","UTF-8","UTF-16BE","UTF-16LE","UTF-16"],X=new Map([["US-ASCII","tb.rulenode.charset-us-ascii"],["ISO-8859-1","tb.rulenode.charset-iso-8859-1"],["UTF-8","tb.rulenode.charset-utf-8"],["UTF-16BE","tb.rulenode.charset-utf-16be"],["UTF-16LE","tb.rulenode.charset-utf-16le"],["UTF-16","tb.rulenode.charset-utf-16"]]),ee=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=D,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=G,n.timeUnits=Object.keys(R),n.timeUnitsTranslationMap=K,n}return b(r,e),r.prototype.configForm=function(){return this.geoActionConfigForm},r.prototype.onConfigurationSet=function(e){this.geoActionConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]],minInsideDuration:[e?e.minInsideDuration:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minInsideDurationTimeUnit:[e?e.minInsideDurationTimeUnit:null,[i.Validators.required]],minOutsideDuration:[e?e.minOutsideDuration:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minOutsideDurationTimeUnit:[e?e.minOutsideDurationTimeUnit:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoActionConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoActionConfigForm.get("perimeterType").value;t?this.geoActionConfigForm.get("perimeterType").setValidators([]):this.geoActionConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.CIRCLE?(this.geoActionConfigForm.get("centerLatitude").setValidators([]),this.geoActionConfigForm.get("centerLongitude").setValidators([]),this.geoActionConfigForm.get("range").setValidators([]),this.geoActionConfigForm.get("rangeUnit").setValidators([])):(this.geoActionConfigForm.get("centerLatitude").setValidators([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoActionConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoActionConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoActionConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoActionConfigForm.get("polygonsDefinition").setValidators([]):this.geoActionConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoActionConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),this.geoActionConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-gps-geofencing-config",template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.min-inside-duration\n \n \n {{ \'tb.rulenode.min-inside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-inside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.min-outside-duration\n \n \n {{ \'tb.rulenode.min-outside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-outside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.msgCountConfigForm},r.prototype.onConfigurationSet=function(e){this.msgCountConfigForm=this.fb.group({interval:[e?e.interval:null,[i.Validators.required,i.Validators.min(1)]],telemetryPrefix:[e?e.telemetryPrefix:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-msg-count-config",template:'
\n \n tb.rulenode.interval-seconds\n \n \n {{ \'tb.rulenode.interval-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-interval-seconds-message\' | translate }}\n \n \n \n tb.rulenode.output-timeseries-key-prefix\n \n \n {{ \'tb.rulenode.output-timeseries-key-prefix-required\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.rpcReplyConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcReplyConfigForm=this.fb.group({requestIdMetaDataAttribute:[e?e.requestIdMetaDataAttribute:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-rpc-reply-config",template:'
\n \n tb.rulenode.request-id-metadata-attribute\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ne=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.saveToCustomTableConfigForm},r.prototype.onConfigurationSet=function(e){this.saveToCustomTableConfigForm=this.fb.group({tableName:[e?e.tableName:null,[i.Validators.required,i.Validators.pattern(/.*\S.*/)]],fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.prototype.prepareOutputConfig=function(e){return e.tableName=e.tableName.trim(),e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-custom-table-config",template:'
\n \n tb.rulenode.custom-table-name\n \n \n {{ \'tb.rulenode.custom-table-name-required\' | translate }}\n \n tb.rulenode.custom-table-hint\n \n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ae=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.translate=r,o.injector=n,o.fb=a,o.propagateChange=null,o.valueChangeSubscription=null,o}var a;return b(r,e),a=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){this.ngControl=this.injector.get(i.NgControl),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.kvListFormGroup=this.fb.group({}),this.kvListFormGroup.addControl("keyVals",this.fb.array([]))},r.prototype.keyValsFormArray=function(){return this.kvListFormGroup.get("keyVals")},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.kvListFormGroup.disable({emitEvent:!1}):this.kvListFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t,r,n=this;this.valueChangeSubscription&&this.valueChangeSubscription.unsubscribe();var a=[];if(e)try{for(var o=v(Object.keys(e)),l=o.next();!l.done;l=o.next()){var s=l.value;Object.prototype.hasOwnProperty.call(e,s)&&a.push(this.fb.group({key:[s,[i.Validators.required]],value:[e[s],[i.Validators.required]]}))}}catch(e){t={error:e}}finally{try{l&&!l.done&&(r=o.return)&&r.call(o)}finally{if(t)throw t.error}}this.kvListFormGroup.setControl("keyVals",this.fb.array(a)),this.valueChangeSubscription=this.kvListFormGroup.valueChanges.subscribe((function(){n.updateModel()}))},r.prototype.removeKeyVal=function(e){this.kvListFormGroup.get("keyVals").removeAt(e)},r.prototype.addKeyVal=function(){this.kvListFormGroup.get("keyVals").push(this.fb.group({key:["",[i.Validators.required]],value:["",[i.Validators.required]]}))},r.prototype.validate=function(e){return!this.kvListFormGroup.get("keyVals").value.length&&this.required?{kvMapRequired:!0}:this.kvListFormGroup.valid?null:{kvFieldsRequired:!0}},r.prototype.updateModel=function(){var e=this.kvListFormGroup.get("keyVals").value;if(this.required&&!e.length||!this.kvListFormGroup.valid)this.propagateChange(null);else{var t={};e.forEach((function(e){t[e.key]=e.value})),this.propagateChange(t)}},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:t.Injector},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.Input(),C("design:type",String)],r.prototype,"requiredText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"keyText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"keyRequiredText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"valText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"valRequiredText",void 0),h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),r=a=h([t.Component({selector:"tb-kv-map-config",template:'
\n
\n {{ keyText | translate }}\n {{ valText | translate }}\n \n
\n
\n
\n \n \n \n \n {{ keyRequiredText | translate }}\n \n \n \n \n \n \n {{ valRequiredText | translate }}\n \n \n \n
\n
\n \n
\n \n
\n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return a})),multi:!0},{provide:i.NG_VALIDATORS,useExisting:t.forwardRef((function(){return a})),multi:!0}],styles:[":host .tb-kv-map-config{margin-bottom:16px}:host .tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}:host .tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:rgba(0,0,0,.54);font-size:12px;font-weight:700;white-space:nowrap}:host .tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}:host .tb-kv-map-config .body .row{padding-top:5px;max-height:40px}:host .tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell{margin:0;max-height:40px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell .mat-form-field-infix{border-top:0}:host ::ng-deep .tb-kv-map-config .body button.mat-button{margin:0}"]}),C("design:paramtypes",[o.Store,n.TranslateService,t.Injector,i.FormBuilder])],r)}(a.PageComponent),oe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n.propagateChange=null,n}var n;return b(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.deviceRelationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],relationType:[null],deviceTypes:[null,[i.Validators.required]]}),this.deviceRelationsQueryFormGroup.valueChanges.subscribe((function(t){e.deviceRelationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.deviceRelationsQueryFormGroup.disable({emitEvent:!1}):this.deviceRelationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.deviceRelationsQueryFormGroup.reset(e,{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=h([t.Component({selector:"tb-device-relations-query-config",template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-type
\n \n \n
device.device-types
\n \n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),ie=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.propagateChange=null,n}var n;return b(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.relationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],filters:[null]}),this.relationsQueryFormGroup.valueChanges.subscribe((function(t){e.relationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.relationsQueryFormGroup.disable({emitEvent:!1}):this.relationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.relationsQueryFormGroup.reset(e||{},{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=h([t.Component({selector:"tb-relations-query-config",template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-filters
\n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),le=function(e){function r(t,r,n,o){var i,l,s=e.call(this,t)||this;s.store=t,s.translate=r,s.truncate=n,s.fb=o,s.placeholder="tb.rulenode.message-type",s.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],s.messageTypes=[],s.messageTypesList=[],s.searchText="",s.propagateChange=function(e){},s.messageTypeConfigForm=s.fb.group({messageType:[null]});try{for(var u=v(Object.keys(a.MessageType)),d=u.next();!d.done;d=u.next()){var p=d.value;s.messageTypesList.push({name:a.messageTypeNames.get(a.MessageType[p]),value:p})}}catch(e){i={error:e}}finally{try{d&&!d.done&&(l=u.return)&&l.call(u)}finally{if(i)throw i.error}}return s}var l;return b(r,e),l=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.ngOnInit=function(){var e=this;this.filteredMessageTypes=this.messageTypeConfigForm.get("messageType").valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(t){return e.fetchMessageTypes(t)})),f.share())},r.prototype.ngAfterViewInit=function(){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.messageTypeConfigForm.disable({emitEvent:!1}):this.messageTypeConfigForm.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t=this;this.searchText="",this.messageTypes.length=0,e&&e.forEach((function(e){var r=t.messageTypesList.find((function(t){return t.value===e}));r?t.messageTypes.push({name:r.name,value:r.value}):t.messageTypes.push({name:e,value:e})}))},r.prototype.displayMessageTypeFn=function(e){return e?e.name:void 0},r.prototype.textIsNotEmpty=function(e){return!!(e&&null!=e&&e.length>0)},r.prototype.createMessageType=function(e,t){e.preventDefault(),this.transformMessageType(t)},r.prototype.add=function(e){this.transformMessageType(e.value)},r.prototype.fetchMessageTypes=function(e){if(this.searchText=e,this.searchText&&this.searchText.length){var t=this.searchText.toUpperCase();return c.of(this.messageTypesList.filter((function(e){return e.name.toUpperCase().includes(t)})))}return c.of(this.messageTypesList)},r.prototype.transformMessageType=function(e){if((e||"").trim()){var t=null,r=e.trim(),n=this.messageTypesList.find((function(e){return e.name===r}));(t=n?{name:n.name,value:n.value}:{name:r,value:r})&&this.addMessageType(t)}this.clear("")},r.prototype.remove=function(e){var t=this.messageTypes.indexOf(e);t>=0&&(this.messageTypes.splice(t,1),this.updateModel())},r.prototype.selected=function(e){this.addMessageType(e.option.value),this.clear("")},r.prototype.addMessageType=function(e){-1===this.messageTypes.findIndex((function(t){return t.value===e.value}))&&(this.messageTypes.push(e),this.updateModel())},r.prototype.onFocus=function(){this.messageTypeConfigForm.get("messageType").updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.messageTypeInput.nativeElement.value=e,this.messageTypeConfigForm.get("messageType").patchValue(null,{emitEvent:!0}),setTimeout((function(){t.messageTypeInput.nativeElement.blur(),t.messageTypeInput.nativeElement.focus()}),0)},r.prototype.updateModel=function(){var e=this.messageTypes.map((function(e){return e.value}));this.required?(this.chipList.errorState=!e.length,this.propagateChange(e.length>0?e:null)):(this.chipList.errorState=!1,this.propagateChange(e))},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:a.TruncatePipe},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),h([t.Input(),C("design:type",String)],r.prototype,"label",void 0),h([t.Input(),C("design:type",Object)],r.prototype,"placeholder",void 0),h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.ViewChild("chipList",{static:!1}),C("design:type",d.MatChipList)],r.prototype,"chipList",void 0),h([t.ViewChild("messageTypeAutocomplete",{static:!1}),C("design:type",p.MatAutocomplete)],r.prototype,"matAutocomplete",void 0),h([t.ViewChild("messageTypeInput",{static:!1}),C("design:type",t.ElementRef)],r.prototype,"messageTypeInput",void 0),r=l=h([t.Component({selector:"tb-message-types-config",template:'\n {{ label }}\n \n \n {{messageType.name}}\n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-message-types-found\n
\n \n \n {{ translate.get(\'tb.rulenode.no-message-type-matching\',\n {messageType: truncate.transform(searchText, true, 6, '...')}) | async }}\n \n \n \n tb.rulenode.create-new-message-type\n \n
\n
\n
\n \n {{ \'tb.rulenode.message-types-required\' | translate }}\n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return l})),multi:!0}]}),C("design:paramtypes",[o.Store,n.TranslateService,a.TruncatePipe,i.FormBuilder])],r)}(a.PageComponent),se=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.subscriptions=[],n.disableCertPemCredentials=!1,n.allCredentialsTypes=$,n.credentialsTypeTranslationsMap=W,n.propagateChange=null,n}var n;return b(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.credentialsConfigFormGroup=this.fb.group({type:[null,[i.Validators.required]],username:[null,[]],password:[null,[]],caCert:[null,[]],caCertFileName:[null,[]],privateKey:[null,[]],privateKeyFileName:[null,[]],cert:[null,[]],certFileName:[null,[]]}),this.subscriptions.push(this.credentialsConfigFormGroup.valueChanges.pipe(f.distinctUntilChanged()).subscribe((function(){e.updateView()}))),this.subscriptions.push(this.credentialsConfigFormGroup.get("type").valueChanges.subscribe((function(){e.credentialsTypeChanged()})))},r.prototype.ngOnChanges=function(e){var t,r,n=this;try{for(var a=v(Object.keys(e)),o=a.next();!o.done;o=a.next()){var i=o.value,l=e[i];if(!l.firstChange&&l.currentValue!==l.previousValue)if(l.currentValue&&"disableCertPemCredentials"===i)"cert.PEM"===this.credentialsConfigFormGroup.get("type").value&&setTimeout((function(){n.credentialsConfigFormGroup.get("type").patchValue("anonymous",{emitEvent:!0})}))}}catch(e){t={error:e}}finally{try{o&&!o.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}},r.prototype.ngOnDestroy=function(){this.subscriptions.forEach((function(e){return e.unsubscribe()}))},r.prototype.writeValue=function(e){s.isDefinedAndNotNull(e)&&(this.credentialsConfigFormGroup.reset(e,{emitEvent:!1}),this.updateValidators(!1))},r.prototype.setDisabledState=function(e){e?this.credentialsConfigFormGroup.disable():(this.credentialsConfigFormGroup.enable(),this.updateValidators())},r.prototype.updateView=function(){var e=this.credentialsConfigFormGroup.value,t=e.type;switch(t){case"anonymous":e={type:t};break;case"basic":e={type:t,username:e.username,password:e.password};break;case"cert.PEM":delete e.username}this.propagateChange(e)},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.validate=function(e){return this.credentialsConfigFormGroup.valid?null:{credentialsConfig:{valid:!1}}},r.prototype.credentialsTypeChanged=function(){this.credentialsConfigFormGroup.patchValue({username:null,password:null,caCert:null,caCertFileName:null,privateKey:null,privateKeyFileName:null,cert:null,certFileName:null}),this.updateValidators()},r.prototype.updateValidators=function(e){void 0===e&&(e=!1);var t=this.credentialsConfigFormGroup.get("type").value;switch(e&&this.credentialsConfigFormGroup.reset({type:t},{emitEvent:!1}),this.credentialsConfigFormGroup.setValidators([]),this.credentialsConfigFormGroup.get("username").setValidators([]),this.credentialsConfigFormGroup.get("password").setValidators([]),t){case"anonymous":break;case"basic":this.credentialsConfigFormGroup.get("username").setValidators([i.Validators.required]),this.credentialsConfigFormGroup.get("password").setValidators([i.Validators.required]);break;case"cert.PEM":this.credentialsConfigFormGroup.setValidators([this.requiredFilesSelected(i.Validators.required,[["caCert","caCertFileName"],["privateKey","privateKeyFileName","cert","certFileName"]])])}this.credentialsConfigFormGroup.get("username").updateValueAndValidity({emitEvent:e}),this.credentialsConfigFormGroup.get("password").updateValueAndValidity({emitEvent:e}),this.credentialsConfigFormGroup.updateValueAndValidity({emitEvent:e})},r.prototype.requiredFilesSelected=function(e,t){return void 0===t&&(t=null),function(r){return t||(t=[Object.keys(r.controls)]),(null==r?void 0:r.controls)&&t.some((function(t){return t.every((function(t){return!e(r.controls[t])}))}))?null:{notAllRequiredFilesSelected:!0}}},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),h([t.Input(),C("design:type",Object)],r.prototype,"disableCertPemCredentials",void 0),r=n=h([t.Component({selector:"tb-credentials-config",template:'
\n \n \n tb.rulenode.credentials\n \n {{ credentialsTypeTranslationsMap.get(credentialsConfigFormGroup.get(\'type\').value) | translate }}\n \n \n \n \n tb.rulenode.credentials-type\n \n \n {{ credentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.username\n \n \n {{ \'tb.rulenode.username-required\' | translate }}\n \n \n \n tb.rulenode.password\n \n \n {{ \'tb.rulenode.password-required\' | translate }}\n \n \n \n \n
{{ \'tb.rulenode.credentials-pem-hint\' | translate }}
\n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n
\n
\n
\n
\n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0},{provide:i.NG_VALIDATORS,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),me=function(){function e(e){this.sanitizer=e}return e.prototype.transform=function(e){return this.sanitizer.bypassSecurityTrustHtml(e)},e.ctorParameters=function(){return[{type:g.DomSanitizer}]},e=h([t.Pipe({name:"safeHtml"}),C("design:paramtypes",[g.DomSanitizer])],e)}(),ue=function(){function e(){}return e=h([t.NgModule({declarations:[ae,oe,ie,le,se,me],imports:[r.CommonModule,a.SharedModule,l.HomeComponentsModule],exports:[ae,oe,ie,le,se,me]})],e)}(),de=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.unassignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.unassignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required,i.Validators.pattern(/.*\S.*/)]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.prepareOutputConfig=function(e){return e.customerNamePattern=e.customerNamePattern.trim(),e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-un-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),pe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.snsConfigForm},r.prototype.onConfigurationSet=function(e){this.snsConfigForm=this.fb.group({topicArnPattern:[e?e.topicArnPattern:null,[i.Validators.required]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-sns-config",template:'
\n \n tb.rulenode.topic-arn-pattern\n \n \n {{ \'tb.rulenode.topic-arn-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ce=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.sqsQueueType=j,n.sqsQueueTypes=Object.keys(j),n.sqsQueueTypeTranslationsMap=_,n}return b(r,e),r.prototype.configForm=function(){return this.sqsConfigForm},r.prototype.onConfigurationSet=function(e){this.sqsConfigForm=this.fb.group({queueType:[e?e.queueType:null,[i.Validators.required]],queueUrlPattern:[e?e.queueUrlPattern:null,[i.Validators.required]],delaySeconds:[e?e.delaySeconds:null,[i.Validators.min(0),i.Validators.max(900)]],messageAttributes:[e?e.messageAttributes:null,[]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-sqs-config",template:'
\n \n tb.rulenode.queue-type\n \n \n {{ sqsQueueTypeTranslationsMap.get(type) | translate }}\n \n \n \n \n tb.rulenode.queue-url-pattern\n \n \n {{ \'tb.rulenode.queue-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.delay-seconds\n \n \n {{ \'tb.rulenode.min-delay-seconds-message\' | translate }}\n \n \n {{ \'tb.rulenode.max-delay-seconds-message\' | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),fe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.pubSubConfigForm},r.prototype.onConfigurationSet=function(e){this.pubSubConfigForm=this.fb.group({projectId:[e?e.projectId:null,[i.Validators.required]],topicName:[e?e.topicName:null,[i.Validators.required]],serviceAccountKey:[e?e.serviceAccountKey:null,[i.Validators.required]],serviceAccountKeyFileName:[e?e.serviceAccountKeyFileName:null,[i.Validators.required]],messageAttributes:[e?e.messageAttributes:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-pub-sub-config",template:'
\n \n tb.rulenode.gcp-project-id\n \n \n {{ \'tb.rulenode.gcp-project-id-required\' | translate }}\n \n \n \n tb.rulenode.pubsub-topic-name\n \n \n {{ \'tb.rulenode.pubsub-topic-name-required\' | translate }}\n \n \n \n \n \n
\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ge=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.ackValues=["all","-1","0","1"],n.ToByteStandartCharsetTypesValues=Z,n.ToByteStandartCharsetTypeTranslationMap=X,n}return b(r,e),r.prototype.configForm=function(){return this.kafkaConfigForm},r.prototype.onConfigurationSet=function(e){this.kafkaConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],bootstrapServers:[e?e.bootstrapServers:null,[i.Validators.required]],retries:[e?e.retries:null,[i.Validators.min(0)]],batchSize:[e?e.batchSize:null,[i.Validators.min(0)]],linger:[e?e.linger:null,[i.Validators.min(0)]],bufferMemory:[e?e.bufferMemory:null,[i.Validators.min(0)]],acks:[e?e.acks:null,[i.Validators.required]],keySerializer:[e?e.keySerializer:null,[i.Validators.required]],valueSerializer:[e?e.valueSerializer:null,[i.Validators.required]],otherProperties:[e?e.otherProperties:null,[]],addMetadataKeyValuesAsKafkaHeaders:[!!e&&e.addMetadataKeyValuesAsKafkaHeaders,[]],kafkaHeadersCharset:[e?e.kafkaHeadersCharset:null,[]]})},r.prototype.validatorTriggers=function(){return["addMetadataKeyValuesAsKafkaHeaders"]},r.prototype.updateValidators=function(e){this.kafkaConfigForm.get("addMetadataKeyValuesAsKafkaHeaders").value?this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([i.Validators.required]):this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([]),this.kafkaConfigForm.get("kafkaHeadersCharset").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-kafka-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.bootstrap-servers\n \n \n {{ \'tb.rulenode.bootstrap-servers-required\' | translate }}\n \n \n \n tb.rulenode.retries\n \n \n {{ \'tb.rulenode.min-retries-message\' | translate }}\n \n \n \n tb.rulenode.batch-size-bytes\n \n \n {{ \'tb.rulenode.min-batch-size-bytes-message\' | translate }}\n \n \n \n tb.rulenode.linger-ms\n \n \n {{ \'tb.rulenode.min-linger-ms-message\' | translate }}\n \n \n \n tb.rulenode.buffer-memory-bytes\n \n \n {{ \'tb.rulenode.min-buffer-memory-bytes-message\' | translate }}\n \n \n \n tb.rulenode.acks\n \n \n {{ ackValue }}\n \n \n \n \n tb.rulenode.key-serializer\n \n \n {{ \'tb.rulenode.key-serializer-required\' | translate }}\n \n \n \n tb.rulenode.value-serializer\n \n \n {{ \'tb.rulenode.value-serializer-required\' | translate }}\n \n \n \n \n \n \n {{ \'tb.rulenode.add-metadata-key-values-as-kafka-headers\' | translate }}\n \n
tb.rulenode.add-metadata-key-values-as-kafka-headers-hint
\n \n tb.rulenode.charset-encoding\n \n \n {{ ToByteStandartCharsetTypeTranslationMap.get(charset) | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ye=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.mqttConfigForm},r.prototype.onConfigurationSet=function(e){this.mqttConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:[e?e.credentials:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-mqtt-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n \n tb.rulenode.connect-timeout\n \n \n {{ \'tb.rulenode.connect-timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n
\n \n tb.rulenode.client-id\n \n \n \n {{ \'tb.rulenode.clean-session\' | translate }}\n \n \n {{ \'tb.rulenode.enable-ssl\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),be=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],n}return b(r,e),r.prototype.configForm=function(){return this.rabbitMqConfigForm},r.prototype.onConfigurationSet=function(e){this.rabbitMqConfigForm=this.fb.group({exchangeNamePattern:[e?e.exchangeNamePattern:null,[]],routingKeyPattern:[e?e.routingKeyPattern:null,[]],messageProperties:[e?e.messageProperties:null,[]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],virtualHost:[e?e.virtualHost:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]],automaticRecoveryEnabled:[!!e&&e.automaticRecoveryEnabled,[]],connectionTimeout:[e?e.connectionTimeout:null,[i.Validators.min(0)]],handshakeTimeout:[e?e.handshakeTimeout:null,[i.Validators.min(0)]],clientProperties:[e?e.clientProperties:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-rabbit-mq-config",template:'
\n \n tb.rulenode.exchange-name-pattern\n \n \n \n tb.rulenode.routing-key-pattern\n \n \n \n tb.rulenode.message-properties\n \n \n {{ property }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n
\n \n tb.rulenode.virtual-host\n \n \n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n {{ \'tb.rulenode.automatic-recovery\' | translate }}\n \n \n tb.rulenode.connection-timeout-ms\n \n \n {{ \'tb.rulenode.min-connection-timeout-ms-message\' | translate }}\n \n \n \n tb.rulenode.handshake-timeout-ms\n \n \n {{ \'tb.rulenode.min-handshake-timeout-ms-message\' | translate }}\n \n \n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),he=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.proxySchemes=["http","https"],n.httpRequestTypes=Object.keys(Q),n}return b(r,e),r.prototype.configForm=function(){return this.restApiCallConfigForm},r.prototype.onConfigurationSet=function(e){this.restApiCallConfigForm=this.fb.group({restEndpointUrlPattern:[e?e.restEndpointUrlPattern:null,[i.Validators.required]],requestMethod:[e?e.requestMethod:null,[i.Validators.required]],useSimpleClientHttpFactory:[!!e&&e.useSimpleClientHttpFactory,[]],enableProxy:[!!e&&e.enableProxy,[]],useSystemProxyProperties:[!!e&&e.enableProxy,[]],proxyScheme:[e?e.proxyHost:null,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],readTimeoutMs:[e?e.readTimeoutMs:null,[]],maxParallelRequestsCount:[e?e.maxParallelRequestsCount:null,[i.Validators.min(0)]],headers:[e?e.headers:null,[]],useRedisQueueForMsgPersistence:[!!e&&e.useRedisQueueForMsgPersistence,[]],trimQueue:[!!e&&e.trimQueue,[]],maxQueueSize:[e?e.maxQueueSize:null,[]],credentials:[e?e.credentials:null,[]]})},r.prototype.validatorTriggers=function(){return["useSimpleClientHttpFactory","useRedisQueueForMsgPersistence","enableProxy","useSystemProxyProperties"]},r.prototype.updateValidators=function(e){var t=this.restApiCallConfigForm.get("useSimpleClientHttpFactory").value,r=this.restApiCallConfigForm.get("useRedisQueueForMsgPersistence").value,n=this.restApiCallConfigForm.get("enableProxy").value,a=this.restApiCallConfigForm.get("useSystemProxyProperties").value;n&&!a?(this.restApiCallConfigForm.get("proxyHost").setValidators(n?[i.Validators.required]:[]),this.restApiCallConfigForm.get("proxyPort").setValidators(n?[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]:[])):(this.restApiCallConfigForm.get("proxyHost").setValidators([]),this.restApiCallConfigForm.get("proxyPort").setValidators([]),t?this.restApiCallConfigForm.get("readTimeoutMs").setValidators([]):this.restApiCallConfigForm.get("readTimeoutMs").setValidators([i.Validators.min(0)])),r?this.restApiCallConfigForm.get("maxQueueSize").setValidators([i.Validators.min(0)]):this.restApiCallConfigForm.get("maxQueueSize").setValidators([]),this.restApiCallConfigForm.get("readTimeoutMs").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("maxQueueSize").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("credentials").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-rest-api-call-config",template:'
\n \n tb.rulenode.endpoint-url-pattern\n \n \n {{ \'tb.rulenode.endpoint-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.request-method\n \n \n {{ requestType }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n \n {{ \'tb.rulenode.use-simple-client-http-factory\' | translate }}\n \n
\n \n {{ \'tb.rulenode.use-system-proxy-properties\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-scheme\n \n \n {{ proxyScheme }}\n \n \n \n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n
\n \n tb.rulenode.read-timeout\n \n tb.rulenode.read-timeout-hint\n \n \n tb.rulenode.max-parallel-requests-count\n \n tb.rulenode.max-parallel-requests-count-hint\n \n \n
\n \n \n \n {{ \'tb.rulenode.use-redis-queue\' | translate }}\n \n
\n \n {{ \'tb.rulenode.trim-redis-queue\' | translate }}\n \n \n tb.rulenode.redis-queue-max-size\n \n \n
\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ce=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.smtpProtocols=["smtp","smtps"],n.tlsVersions=["TLSv1","TLSv1.1","TLSv1.2","TLSv1.3"],n}return b(r,e),r.prototype.configForm=function(){return this.sendEmailConfigForm},r.prototype.onConfigurationSet=function(e){this.sendEmailConfigForm=this.fb.group({useSystemSmtpSettings:[!!e&&e.useSystemSmtpSettings,[]],smtpProtocol:[e?e.smtpProtocol:null,[]],smtpHost:[e?e.smtpHost:null,[]],smtpPort:[e?e.smtpPort:null,[]],timeout:[e?e.timeout:null,[]],enableTls:[!!e&&e.enableTls,[]],tlsVersion:[e?e.tlsVersion:null,[]],enableProxy:[!!e&&e.enableProxy,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]]})},r.prototype.validatorTriggers=function(){return["useSystemSmtpSettings","enableProxy"]},r.prototype.updateValidators=function(e){var t=this.sendEmailConfigForm.get("useSystemSmtpSettings").value,r=this.sendEmailConfigForm.get("enableProxy").value;t?(this.sendEmailConfigForm.get("smtpProtocol").setValidators([]),this.sendEmailConfigForm.get("smtpHost").setValidators([]),this.sendEmailConfigForm.get("smtpPort").setValidators([]),this.sendEmailConfigForm.get("timeout").setValidators([]),this.sendEmailConfigForm.get("proxyHost").setValidators([]),this.sendEmailConfigForm.get("proxyPort").setValidators([])):(this.sendEmailConfigForm.get("smtpProtocol").setValidators([i.Validators.required]),this.sendEmailConfigForm.get("smtpHost").setValidators([i.Validators.required]),this.sendEmailConfigForm.get("smtpPort").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]),this.sendEmailConfigForm.get("timeout").setValidators([i.Validators.required,i.Validators.min(0)]),this.sendEmailConfigForm.get("proxyHost").setValidators(r?[i.Validators.required]:[]),this.sendEmailConfigForm.get("proxyPort").setValidators(r?[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]:[])),this.sendEmailConfigForm.get("smtpProtocol").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpPort").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("timeout").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-send-email-config",template:'
\n \n {{ \'tb.rulenode.use-system-smtp-settings\' | translate }}\n \n
\n \n tb.rulenode.smtp-protocol\n \n \n {{ smtpProtocol.toUpperCase() }}\n \n \n \n
\n \n tb.rulenode.smtp-host\n \n \n {{ \'tb.rulenode.smtp-host-required\' | translate }}\n \n \n \n tb.rulenode.smtp-port\n \n \n {{ \'tb.rulenode.smtp-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.timeout-msec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-msec-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.enable-tls\' | translate }}\n \n \n tb.rulenode.tls-version\n \n \n {{ tlsVersion }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ve=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.serviceType=a.ServiceType.TB_RULE_ENGINE,n}return b(r,e),r.prototype.configForm=function(){return this.checkPointConfigForm},r.prototype.onConfigurationSet=function(e){this.checkPointConfigForm=this.fb.group({queueName:[e?e.queueName:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-check-point-config",template:'
\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Fe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allAzureIotHubCredentialsTypes=J,n.azureIotHubCredentialsTypeTranslationsMap=Y,n}return b(r,e),r.prototype.configForm=function(){return this.azureIotHubConfigForm},r.prototype.onConfigurationSet=function(e){this.azureIotHubConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[i.Validators.required]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:this.fb.group({type:[e&&e.credentials?e.credentials.type:null,[i.Validators.required]],sasKey:[e&&e.credentials?e.credentials.sasKey:null,[]],caCert:[e&&e.credentials?e.credentials.caCert:null,[]],caCertFileName:[e&&e.credentials?e.credentials.caCertFileName:null,[]],privateKey:[e&&e.credentials?e.credentials.privateKey:null,[]],privateKeyFileName:[e&&e.credentials?e.credentials.privateKeyFileName:null,[]],cert:[e&&e.credentials?e.credentials.cert:null,[]],certFileName:[e&&e.credentials?e.credentials.certFileName:null,[]],password:[e&&e.credentials?e.credentials.password:null,[]]})})},r.prototype.prepareOutputConfig=function(e){var t=e.credentials.type;return"sas"===t&&(e.credentials={type:t,sasKey:e.credentials.sasKey,caCert:e.credentials.caCert,caCertFileName:e.credentials.caCertFileName}),e},r.prototype.validatorTriggers=function(){return["credentials.type"]},r.prototype.updateValidators=function(e){var t=this.azureIotHubConfigForm.get("credentials"),r=t.get("type").value;switch(e&&t.reset({type:r},{emitEvent:!1}),t.get("sasKey").setValidators([]),t.get("privateKey").setValidators([]),t.get("privateKeyFileName").setValidators([]),t.get("cert").setValidators([]),t.get("certFileName").setValidators([]),r){case"sas":t.get("sasKey").setValidators([i.Validators.required]);break;case"cert.PEM":t.get("privateKey").setValidators([i.Validators.required]),t.get("privateKeyFileName").setValidators([i.Validators.required]),t.get("cert").setValidators([i.Validators.required]),t.get("certFileName").setValidators([i.Validators.required])}t.get("sasKey").updateValueAndValidity({emitEvent:e}),t.get("privateKey").updateValueAndValidity({emitEvent:e}),t.get("privateKeyFileName").updateValueAndValidity({emitEvent:e}),t.get("cert").updateValueAndValidity({emitEvent:e}),t.get("certFileName").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-azure-iot-hub-config",template:'
\n \n tb.rulenode.topic\n \n \n {{ \'tb.rulenode.topic-required\' | translate }}\n \n \n \n tb.rulenode.hostname\n \n \n {{ \'tb.rulenode.hostname-required\' | translate }}\n \n \n \n tb.rulenode.device-id\n \n \n {{ \'tb.rulenode.device-id-required\' | translate }}\n \n \n \n \n \n tb.rulenode.credentials\n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(azureIotHubConfigForm.get(\'credentials.type\').value) | translate }}\n \n \n
\n \n tb.rulenode.credentials-type\n \n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.sas-key\n \n \n {{ \'tb.rulenode.sas-key-required\' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n
\n
\n
\n
\n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),xe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.deviceProfile},r.prototype.onConfigurationSet=function(e){this.deviceProfile=this.fb.group({persistAlarmRulesState:[!!e&&e.persistAlarmRulesState,i.Validators.required],fetchAlarmRulesStateOnStart:[!!e&&e.fetchAlarmRulesStateOnStart,i.Validators.required]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-device-profile-config",template:'
\n \n {{ \'tb.rulenode.persist-alarm-rules\' | translate }}\n \n \n {{ \'tb.rulenode.fetch-alarm-rules\' | translate }}\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.sendSmsConfigForm},r.prototype.onConfigurationSet=function(e){this.sendSmsConfigForm=this.fb.group({numbersToTemplate:[e?e.numbersToTemplate:null,[i.Validators.required]],smsMessageTemplate:[e?e.smsMessageTemplate:null,[i.Validators.required]],useSystemSmsSettings:[!!e&&e.useSystemSmsSettings,[]],smsProviderConfiguration:[e?e.smsProviderConfiguration:null,[]]})},r.prototype.validatorTriggers=function(){return["useSystemSmsSettings"]},r.prototype.updateValidators=function(e){this.sendSmsConfigForm.get("useSystemSmsSettings").value?this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([]):this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([i.Validators.required]),this.sendSmsConfigForm.get("smsProviderConfiguration").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-send-sms-config",template:'
\n \n tb.rulenode.numbers-to-template\n \n \n {{ \'tb.rulenode.numbers-to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.sms-message-template\n \n \n {{ \'tb.rulenode.sms-message-template-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.use-system-sms-settings\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),qe=function(){function e(){}return e=h([t.NgModule({declarations:[T,q,S,I,k,N,V,E,A,L,P,ee,te,re,ne,de,pe,ce,fe,ge,ye,be,he,Ce,ve,Fe,xe,Te],imports:[r.CommonModule,a.SharedModule,l.HomeComponentsModule,ue],exports:[T,q,S,I,k,N,V,E,A,L,P,ee,te,re,ne,de,pe,ce,fe,ge,ye,be,he,Ce,ve,Fe,xe,Te]})],e)}(),Se=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.checkMessageConfigForm},r.prototype.onConfigurationSet=function(e){this.checkMessageConfigForm=this.fb.group({messageNames:[e?e.messageNames:null,[]],metadataNames:[e?e.metadataNames:null,[]],checkAllKeys:[!!e&&e.checkAllKeys,[]]})},r.prototype.validateConfig=function(){var e=this.checkMessageConfigForm.get("messageNames").value,t=this.checkMessageConfigForm.get("metadataNames").value;return e.length>0||t.length>0},r.prototype.removeMessageName=function(e){var t=this.checkMessageConfigForm.get("messageNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("messageNames").setValue(t,{emitEvent:!0}))},r.prototype.removeMetadataName=function(e){var t=this.checkMessageConfigForm.get("metadataNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("metadataNames").setValue(t,{emitEvent:!0}))},r.prototype.addMessageName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("messageNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("messageNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.prototype.addMetadataName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("metadataNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("metadataNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-check-message-config",template:'
\n \n \n \n \n \n {{messageName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n \n \n \n \n {{metadataName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n {{ \'tb.rulenode.check-all-keys\' | translate }}\n \n
tb.rulenode.check-all-keys-hint
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ie=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.entitySearchDirection=Object.keys(a.EntitySearchDirection),n.entitySearchDirectionTranslationsMap=a.entitySearchDirectionTranslations,n}return b(r,e),r.prototype.configForm=function(){return this.checkRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.checkRelationConfigForm=this.fb.group({checkForSingleEntity:[!!e&&e.checkForSingleEntity,[]],direction:[e?e.direction:null,[]],entityType:[e?e.entityType:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],entityId:[e?e.entityId:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],relationType:[e?e.relationType:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["checkForSingleEntity"]},r.prototype.updateValidators=function(e){var t=this.checkRelationConfigForm.get("checkForSingleEntity").value;this.checkRelationConfigForm.get("entityType").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:e}),this.checkRelationConfigForm.get("entityId").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityId").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-check-relation-config",template:'
\n \n {{ \'tb.rulenode.check-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.check-relation-hint
\n \n relation.direction\n \n \n {{ entitySearchDirectionTranslationsMap.get(direction) | translate }}\n \n \n \n
\n \n \n \n \n
\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ke=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=D,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=G,n}return b(r,e),r.prototype.configForm=function(){return this.geoFilterConfigForm},r.prototype.onConfigurationSet=function(e){this.geoFilterConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoFilterConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoFilterConfigForm.get("perimeterType").value;t?this.geoFilterConfigForm.get("perimeterType").setValidators([]):this.geoFilterConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.CIRCLE?(this.geoFilterConfigForm.get("centerLatitude").setValidators([]),this.geoFilterConfigForm.get("centerLongitude").setValidators([]),this.geoFilterConfigForm.get("range").setValidators([]),this.geoFilterConfigForm.get("rangeUnit").setValidators([])):(this.geoFilterConfigForm.get("centerLatitude").setValidators([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoFilterConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoFilterConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoFilterConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoFilterConfigForm.get("polygonsDefinition").setValidators([]):this.geoFilterConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoFilterConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),this.geoFilterConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-gps-geofencing-config",template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ne=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.messageTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.messageTypeConfigForm=this.fb.group({messageTypes:[e?e.messageTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-message-type-config",template:'
\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ve=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allowedEntityTypes=[a.EntityType.DEVICE,a.EntityType.ASSET,a.EntityType.ENTITY_VIEW,a.EntityType.TENANT,a.EntityType.CUSTOMER,a.EntityType.USER,a.EntityType.DASHBOARD,a.EntityType.RULE_CHAIN,a.EntityType.RULE_NODE],n}return b(r,e),r.prototype.configForm=function(){return this.originatorTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorTypeConfigForm=this.fb.group({originatorTypes:[e?e.originatorTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-originator-type-config",template:'
\n \n \n \n
\n',styles:[":host ::ng-deep tb-entity-type-list .mat-form-field-flex{padding-top:0}:host ::ng-deep tb-entity-type-list .mat-form-field-infix{border-top:0}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ee=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"filter",this.translate.instant("tb.rulenode.filter"),"Filter",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-filter-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Ae=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.switchConfigForm},r.prototype.onConfigurationSet=function(e){this.switchConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.switchConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"switch",this.translate.instant("tb.rulenode.switch"),"Switch",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.switchConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-filter-node-switch-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Le=function(e){function r(t,r,n){var o,l,s=e.call(this,t)||this;s.store=t,s.translate=r,s.fb=n,s.alarmStatusTranslationsMap=a.alarmStatusTranslations,s.alarmStatusList=[],s.searchText="",s.displayStatusFn=s.displayStatus.bind(s);try{for(var m=v(Object.keys(a.AlarmStatus)),u=m.next();!u.done;u=m.next()){var d=u.value;s.alarmStatusList.push(a.AlarmStatus[d])}}catch(e){o={error:e}}finally{try{u&&!u.done&&(l=m.return)&&l.call(m)}finally{if(o)throw o.error}}return s.statusFormControl=new i.FormControl(""),s.filteredAlarmStatus=s.statusFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return s.fetchAlarmStatus(e)})),f.share()),s}return b(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.alarmStatusConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.statusFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.alarmStatusConfigForm=this.fb.group({alarmStatusList:[e?e.alarmStatusList:null,[i.Validators.required]]})},r.prototype.displayStatus=function(e){return e?this.translate.instant(a.alarmStatusTranslations.get(e)):void 0},r.prototype.fetchAlarmStatus=function(e){var t=this,r=this.getAlarmStatusList();if(this.searchText=e,this.searchText&&this.searchText.length){var n=this.searchText.toUpperCase();return c.of(r.filter((function(e){return t.translate.instant(a.alarmStatusTranslations.get(a.AlarmStatus[e])).toUpperCase().includes(n)})))}return c.of(r)},r.prototype.alarmStatusSelected=function(e){this.addAlarmStatus(e.option.value),this.clear("")},r.prototype.removeAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))}},r.prototype.addAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))},r.prototype.getAlarmStatusList=function(){var e=this;return this.alarmStatusList.filter((function(t){return-1===e.alarmStatusConfigForm.get("alarmStatusList").value.indexOf(t)}))},r.prototype.onAlarmStatusInputFocus=function(){this.statusFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.alarmStatusInput.nativeElement.value=e,this.statusFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.alarmStatusInput.nativeElement.blur(),t.alarmStatusInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},h([t.ViewChild("alarmStatusInput",{static:!1}),C("design:type",t.ElementRef)],r.prototype,"alarmStatusInput",void 0),r=h([t.Component({selector:"tb-filter-node-check-alarm-status-config",template:'
\n \n tb.rulenode.alarm-status-filter\n \n \n \n {{alarmStatusTranslationsMap.get(alarmStatus) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-alarm-status-matching\n
\n
\n
\n
\n
\n \n
\n\n\n\n'}),C("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Pe=function(){function e(){}return e=h([t.NgModule({declarations:[Se,Ie,ke,Ne,Ve,Ee,Ae,Le],imports:[r.CommonModule,a.SharedModule,ue],exports:[Se,Ie,ke,Ne,Ve,Ee,Ae,Le]})],e)}(),Me=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.customerAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.customerAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-customer-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),we=function(e){function r(t,r,n){var a,o,l=e.call(this,t)||this;l.store=t,l.translate=r,l.fb=n,l.entityDetailsTranslationsMap=z,l.entityDetailsList=[],l.searchText="",l.displayDetailsFn=l.displayDetails.bind(l);try{for(var s=v(Object.keys(B)),m=s.next();!m.done;m=s.next()){var u=m.value;l.entityDetailsList.push(B[u])}}catch(e){a={error:e}}finally{try{m&&!m.done&&(o=s.return)&&o.call(s)}finally{if(a)throw a.error}}return l.detailsFormControl=new i.FormControl(""),l.filteredEntityDetails=l.detailsFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return l.fetchEntityDetails(e)})),f.share()),l}return b(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.entityDetailsConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.detailsFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.entityDetailsConfigForm=this.fb.group({detailsList:[e?e.detailsList:null,[i.Validators.required]],addToMetadata:[!!e&&e.addToMetadata,[]]})},r.prototype.displayDetails=function(e){return e?this.translate.instant(z.get(e)):void 0},r.prototype.fetchEntityDetails=function(e){var t=this;if(this.searchText=e,this.searchText&&this.searchText.length){var r=this.searchText.toUpperCase();return c.of(this.entityDetailsList.filter((function(e){return t.translate.instant(z.get(B[e])).toUpperCase().includes(r)})))}return c.of(this.entityDetailsList)},r.prototype.detailsFieldSelected=function(e){this.addDetailsField(e.option.value),this.clear("")},r.prototype.removeDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.entityDetailsConfigForm.get("detailsList").setValue(t))}},r.prototype.addDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.entityDetailsConfigForm.get("detailsList").setValue(t))},r.prototype.onEntityDetailsInputFocus=function(){this.detailsFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.detailsInput.nativeElement.value=e,this.detailsFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.detailsInput.nativeElement.blur(),t.detailsInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},h([t.ViewChild("detailsInput",{static:!1}),C("design:type",t.ElementRef)],r.prototype,"detailsInput",void 0),r=h([t.Component({selector:"tb-enrichment-node-entity-details-config",template:'
\n \n tb.rulenode.entity-details\n \n \n \n {{entityDetailsTranslationsMap.get(details) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-entity-details-matching\n
\n
\n
\n
\n
\n \n \n {{ \'tb.rulenode.add-to-metadata\' | translate }}\n \n
tb.rulenode.add-to-metadata-hint
\n
\n',styles:[":host ::ng-deep mat-form-field.entity-fields-list .mat-form-field-wrapper{margin-bottom:-1.25em}"]}),C("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.deviceAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.deviceAttributesConfigForm=this.fb.group({deviceRelationsQuery:[e?e.deviceRelationsQuery:null,[i.Validators.required]],tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.deviceAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.deviceAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.deviceAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.deviceAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-device-attributes-config",template:'
\n \n \n \n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
tb.rulenode.get-latest-value-with-ts-hint
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),De=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.originatorAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorAttributesConfigForm=this.fb.group({tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.originatorAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.originatorAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.originatorAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.originatorAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-originator-attributes-config",template:'
\n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
tb.rulenode.get-latest-value-with-ts-hint
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Oe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.originatorFieldsConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorFieldsConfigForm=this.fb.group({fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-originator-fields-config",template:'
\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ke=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n.fetchMode=H,n.fetchModes=Object.keys(H),n.samplingOrders=Object.keys(U),n.timeUnits=Object.values(R),n.timeUnitsTranslationMap=K,n}return b(r,e),r.prototype.configForm=function(){return this.getTelemetryFromDatabaseConfigForm},r.prototype.onConfigurationSet=function(e){this.getTelemetryFromDatabaseConfigForm=this.fb.group({latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],fetchMode:[e?e.fetchMode:null,[i.Validators.required]],orderBy:[e?e.orderBy:null,[]],limit:[e?e.limit:null,[]],useMetadataIntervalPatterns:[!!e&&e.useMetadataIntervalPatterns,[]],startInterval:[e?e.startInterval:null,[]],startIntervalTimeUnit:[e?e.startIntervalTimeUnit:null,[]],endInterval:[e?e.endInterval:null,[]],endIntervalTimeUnit:[e?e.endIntervalTimeUnit:null,[]],startIntervalPattern:[e?e.startIntervalPattern:null,[]],endIntervalPattern:[e?e.endIntervalPattern:null,[]]})},r.prototype.validatorTriggers=function(){return["fetchMode","useMetadataIntervalPatterns"]},r.prototype.updateValidators=function(e){var t=this.getTelemetryFromDatabaseConfigForm.get("fetchMode").value,r=this.getTelemetryFromDatabaseConfigForm.get("useMetadataIntervalPatterns").value;t&&t===H.ALL?(this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([i.Validators.required,i.Validators.min(2),i.Validators.max(1e3)])):(this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([])),r?(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([i.Validators.required])):(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([])),this.getTelemetryFromDatabaseConfigForm.get("orderBy").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("limit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").updateValueAndValidity({emitEvent:e})},r.prototype.removeKey=function(e,t){var r=this.getTelemetryFromDatabaseConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.getTelemetryFromDatabaseConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-get-telemetry-from-database",template:'
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n tb.rulenode.fetch-mode\n \n \n {{ mode }}\n \n \n tb.rulenode.fetch-mode-hint\n \n
\n \n tb.rulenode.order-by\n \n \n {{ order }}\n \n \n tb.rulenode.order-by-hint\n \n \n tb.rulenode.limit\n \n tb.rulenode.limit-hint\n \n
\n \n {{ \'tb.rulenode.use-metadata-interval-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-interval-patterns-hint
\n
\n
\n \n tb.rulenode.start-interval\n \n \n {{ \'tb.rulenode.start-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.start-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.end-interval\n \n \n {{ \'tb.rulenode.end-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.end-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n \n tb.rulenode.start-interval-pattern\n \n \n {{ \'tb.rulenode.start-interval-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.end-interval-pattern\n \n \n {{ \'tb.rulenode.end-interval-pattern-required\' | translate }}\n \n \n \n \n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Be=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.relatedAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.relatedAttributesConfigForm=this.fb.group({relationsQuery:[e?e.relationsQuery:null,[i.Validators.required]],telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-related-attributes-config",template:'
\n \n \n \n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ge=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.tenantAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.tenantAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-tenant-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),He=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.calculateDeltaConfigForm},r.prototype.onConfigurationSet=function(e){this.calculateDeltaConfigForm=this.fb.group({inputValueKey:[e?e.inputValueKey:null,[i.Validators.required]],outputValueKey:[e?e.outputValueKey:null,[i.Validators.required]],useCache:[e?e.useCache:null,[]],addPeriodBetweenMsgs:[!!e&&e.addPeriodBetweenMsgs,[]],periodValueKey:[e?e.periodValueKey:null,[]],round:[e?e.round:null,[i.Validators.min(0),i.Validators.max(15)]],tellFailureIfDeltaIsNegative:[e?e.tellFailureIfDeltaIsNegative:null,[]]})},r.prototype.updateValidators=function(e){this.calculateDeltaConfigForm.get("addPeriodBetweenMsgs").value?this.calculateDeltaConfigForm.get("periodValueKey").setValidators([i.Validators.required]):this.calculateDeltaConfigForm.get("periodValueKey").setValidators([]),this.calculateDeltaConfigForm.get("periodValueKey").updateValueAndValidity({emitEvent:e})},r.prototype.validatorTriggers=function(){return["addPeriodBetweenMsgs"]},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-calculate-delta-config",template:'
\n
\n \n tb.rulenode.input-value-key\n \n \n {{ \'tb.rulenode.input-value-key-required\' | translate }}\n \n \n \n tb.rulenode.output-value-key\n \n \n {{ \'tb.rulenode.output-value-key-required\' | translate }}\n \n \n \n tb.rulenode.round\n \n \n {{ \'tb.rulenode.round-range\' | translate }}\n \n \n {{ \'tb.rulenode.round-range\' | translate }}\n \n \n
\n \n {{ \'tb.rulenode.use-cache\' | translate }}\n \n \n {{ \'tb.rulenode.tell-failure-if-delta-is-negative\' | translate }}\n \n \n {{ \'tb.rulenode.add-period-between-msgs\' | translate }}\n \n \n tb.rulenode.period-value-key\n \n \n {{ \'tb.rulenode.period-value-key-required\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ue=function(){function e(){}return e=h([t.NgModule({declarations:[Me,we,Re,De,Oe,Ke,Be,Ge,He],imports:[r.CommonModule,a.SharedModule,ue],exports:[Me,we,Re,De,Oe,Ke,Be,Ge,He]})],e)}(),je=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.originatorSource=F,n.originatorSources=Object.keys(F),n.originatorSourceTranslationMap=w,n}return b(r,e),r.prototype.configForm=function(){return this.changeOriginatorConfigForm},r.prototype.onConfigurationSet=function(e){this.changeOriginatorConfigForm=this.fb.group({originatorSource:[e?e.originatorSource:null,[i.Validators.required]],relationsQuery:[e?e.relationsQuery:null,[]]})},r.prototype.validatorTriggers=function(){return["originatorSource"]},r.prototype.updateValidators=function(e){var t=this.changeOriginatorConfigForm.get("originatorSource").value;t&&t===F.RELATED?this.changeOriginatorConfigForm.get("relationsQuery").setValidators([i.Validators.required]):this.changeOriginatorConfigForm.get("relationsQuery").setValidators([]),this.changeOriginatorConfigForm.get("relationsQuery").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-transformation-node-change-originator-config",template:'
\n \n tb.rulenode.originator-source\n \n \n {{ originatorSourceTranslationMap.get(source) | translate }}\n \n \n \n
\n \n \n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ze=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"update",this.translate.instant("tb.rulenode.transformer"),"Transform",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-transformation-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Qe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.toEmailConfigForm},r.prototype.onConfigurationSet=function(e){this.toEmailConfigForm=this.fb.group({fromTemplate:[e?e.fromTemplate:null,[i.Validators.required]],toTemplate:[e?e.toTemplate:null,[i.Validators.required]],ccTemplate:[e?e.ccTemplate:null,[]],bccTemplate:[e?e.bccTemplate:null,[]],subjectTemplate:[e?e.subjectTemplate:null,[i.Validators.required]],bodyTemplate:[e?e.bodyTemplate:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-transformation-node-to-email-config",template:'
\n \n tb.rulenode.from-template\n \n \n {{ \'tb.rulenode.from-template-required\' | translate }}\n \n \n \n \n tb.rulenode.to-template\n \n \n {{ \'tb.rulenode.to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.cc-template\n \n \n \n \n tb.rulenode.bcc-template\n \n \n \n \n tb.rulenode.subject-template\n \n \n {{ \'tb.rulenode.subject-template-required\' | translate }}\n \n \n \n \n tb.rulenode.body-template\n \n \n {{ \'tb.rulenode.body-template-required\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),_e=function(){function e(){}return e=h([t.NgModule({declarations:[je,ze,Qe],imports:[r.CommonModule,a.SharedModule,ue],exports:[je,ze,Qe]})],e)}(),$e=function(){function e(e){!function(e){e.setTranslation("en_US",{tb:{rulenode:{"create-entity-if-not-exists":"Create new entity if not exists","create-entity-if-not-exists-hint":"Create a new entity set above if it does not exist.","entity-name-pattern":"Name pattern","entity-name-pattern-required":"Name pattern is required","entity-type-pattern":"Type pattern","entity-type-pattern-required":"Type pattern is required","entity-cache-expiration":"Entities cache expiration time (sec)","entity-cache-expiration-hint":"Specifies maximum time interval allowed to store found entity records. 0 value means that records will never expire.","entity-cache-expiration-required":"Entities cache expiration time is required.","entity-cache-expiration-range":"Entities cache expiration time should be greater than or equal to 0.","customer-name-pattern":"Customer name pattern","customer-name-pattern-required":"Customer name pattern is required","create-customer-if-not-exists":"Create new customer if not exists","customer-cache-expiration":"Customers cache expiration time (sec)","customer-cache-expiration-hint":"Specifies maximum time interval allowed to store found customer records. 0 value means that records will never expire.","customer-cache-expiration-required":"Customers cache expiration time is required.","customer-cache-expiration-range":"Customers cache expiration time should be greater than or equal to 0.","start-interval":"Start Interval","end-interval":"End Interval","start-interval-time-unit":"Start Interval Time Unit","end-interval-time-unit":"End Interval Time Unit","fetch-mode":"Fetch mode","fetch-mode-hint":"If selected fetch mode 'ALL' you able to choose telemetry sampling order.","order-by":"Order by","order-by-hint":"Select to choose telemetry sampling order.",limit:"Limit","limit-hint":"Min limit value is 2, max - 1000. In case you want to fetch a single entry, select fetch mode 'FIRST' or 'LAST'.","time-unit-milliseconds":"Milliseconds","time-unit-seconds":"Seconds","time-unit-minutes":"Minutes","time-unit-hours":"Hours","time-unit-days":"Days","time-value-range":"Time value should be in a range from 1 to 2147483647.","start-interval-value-required":"Start interval value is required.","end-interval-value-required":"End interval value is required.",filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","notify-device":"Notify Device","notify-device-hint":"If the message arrives from the device, we will push it back to the device by default.","latest-timeseries":"Latest timeseries","timeseries-key":"Timeseries key","data-keys":"Message data","metadata-keys":"Message metadata","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","relation-type-pattern":"Relation type pattern","relation-type-pattern-required":"Relation type pattern is required","relation-types-list":"Relation types to propagate","relation-types-list-hint":"If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","fields-mapping":"Fields mapping","fields-mapping-required":"At least one field mapping should be specified.","source-field":"Source field","source-field-required":"Source field is required.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","originator-alarm-originator":"Alarm Originator","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","use-metadata-period-in-seconds-patterns":"Use period in seconds pattern","use-metadata-period-in-seconds-patterns-hint":"If selected, rule node use period in seconds interval pattern from message metadata or data assuming that intervals are in the seconds.","period-in-seconds-pattern":"Period in seconds pattern","period-in-seconds-pattern-required":"Period in seconds pattern is required","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required","alarm-status-filter":"Alarm status filter","alarm-status-list-empty":"Alarm status list is empty","no-alarm-status-matching":"No alarm status matching were found.",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":'Comma separated address list, use ${metadataKey} for value from metadata, $[messageKey] for value from message body',"cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","body-template":"Body Template","body-template-required":"Body Template is required","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","request-method":"Request method","use-simple-client-http-factory":"Use simple client HTTP factory","read-timeout":"Read timeout in millis","read-timeout-hint":"The value of 0 means an infinite timeout","max-parallel-requests-count":"Max number of parallel requests","max-parallel-requests-count-hint":"The value of 0 specifies no limit in parallel processing",headers:"Headers","headers-hint":'Use ${metadataKey} for value from metadata, $[messageKey] for value from message body in header/value fields',header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required",topic:"Topic","topic-required":"Topic is required","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","gcp-project-id":"GCP project ID","gcp-project-id-required":"GCP project ID is required","gcp-service-account-key":"GCP service account key file","gcp-service-account-key-required":"GCP service account key file is required","pubsub-topic-name":"Topic name","pubsub-topic-name-required":"Topic name is required","message-attributes":"Message attributes","message-attributes-hint":'Use ${metadataKey} for value from metadata, $[messageKey] for value from message body in name/value fields',"connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","device-id":"Device ID","device-id-required":"Device ID is required.","clean-session":"Clean session","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","credentials-pem-hint":"At least Server CA certificate file or a pair of Client certificate and Client private key files are required","credentials-sas":"Shared Access Signature","sas-key":"SAS Key","sas-key-required":"SAS Key is required.",hostname:"Hostname","hostname-required":"Hostname is required.","azure-ca-cert":"CA certificate file","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"Server CA certificate file *","private-key":"Client private key file *",cert:"Client certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","use-metadata-interval-patterns":"Use interval patterns","use-metadata-interval-patterns-hint":"If selected, rule node use start and end interval patterns from message metadata or data assuming that intervals are in the milliseconds.","use-message-alarm-data":"Use message alarm data","use-dynamically-change-the-severity-of-alar":"Use dynamically change the severity of alarm","check-all-keys":"Check that all selected keys are present","check-all-keys-hint":"If selected, checks that all specified keys are present in the message data and metadata.","check-relation-to-specific-entity":"Check relation to specific entity","check-relation-hint":"Checks existence of relation to specific entity or to any entity based on direction and relation type.","delete-relation-to-specific-entity":"Delete relation to specific entity","delete-relation-hint":"Deletes relation from the originator of the incoming message to the specified entity or list of entities based on direction and type.","remove-current-relations":"Remove current relations","remove-current-relations-hint":"Removes current relations from the originator of the incoming message based on direction and type.","change-originator-to-related-entity":"Change originator to related entity","change-originator-to-related-entity-hint":"Used to process submitted message as a message from another entity.","start-interval-pattern":"Start interval pattern","end-interval-pattern":"End interval pattern","start-interval-pattern-required":"Start interval pattern is required","end-interval-pattern-required":"End interval pattern is required","smtp-protocol":"Protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS","tls-version":"TLS version","enable-proxy":"Enable proxy","use-system-proxy-properties":"Use system proxy properties","proxy-host":"Proxy host","proxy-host-required":"Proxy host is required.","proxy-port":"Proxy port","proxy-port-required":"Proxy port is required.","proxy-port-range":"Proxy port should be in a range from 1 to 65535.","proxy-user":"Proxy user","proxy-password":"Proxy password","proxy-scheme":"Proxy scheme","numbers-to-template":"Phone Numbers To Template","numbers-to-template-required":"Phone Numbers To Template is required","numbers-to-template-hint":'Comma separated Phone Numbers, use ${metadataKey} for value from metadata, $[messageKey] for value from message body',"sms-message-template":"SMS message Template","sms-message-template-required":"SMS message Template is required","use-system-sms-settings":"Use system SMS provider settings","min-period-0-seconds-message":"Only 0 second minimum period is allowed.","max-pending-messages":"Maximum pending messages","max-pending-messages-required":"Maximum pending messages is required.","max-pending-messages-range":"Maximum pending messages should be in a range from 1 to 100000.","originator-types-filter":"Originator types filter","interval-seconds":"Interval in seconds","interval-seconds-required":"Interval is required.","min-interval-seconds-message":"Only 1 second minimum interval is allowed.","output-timeseries-key-prefix":"Output timeseries key prefix","output-timeseries-key-prefix-required":"Output timeseries key prefix required.","separator-hint":'You should press "enter" to complete field input.',"entity-details":"Select entity details:","entity-details-title":"Title","entity-details-country":"Country","entity-details-state":"State","entity-details-zip":"Zip","entity-details-address":"Address","entity-details-address2":"Address2","entity-details-additional_info":"Additional Info","entity-details-phone":"Phone","entity-details-email":"Email","add-to-metadata":"Add selected details to message metadata","add-to-metadata-hint":"If selected, adds the selected details keys to the message metadata instead of message data.","entity-details-list-empty":"No entity details selected.","no-entity-details-matching":"No entity details matching were found.","custom-table-name":"Custom table name","custom-table-name-required":"Table Name is required","custom-table-hint":"You should enter the table name without prefix 'cs_tb_'.","message-field":"Message field","message-field-required":"Message field is required.","table-col":"Table column","table-col-required":"Table column is required.","latitude-key-name":"Latitude key name","longitude-key-name":"Longitude key name","latitude-key-name-required":"Latitude key name is required.","longitude-key-name-required":"Longitude key name is required.","fetch-perimeter-info-from-message-metadata":"Fetch perimeter information from message metadata","perimeter-circle":"Circle","perimeter-polygon":"Polygon","perimeter-type":"Perimeter type","circle-center-latitude":"Center latitude","circle-center-latitude-required":"Center latitude is required.","circle-center-longitude":"Center longitude","circle-center-longitude-required":"Center longitude is required.","range-unit-meter":"Meter","range-unit-kilometer":"Kilometer","range-unit-foot":"Foot","range-unit-mile":"Mile","range-unit-nautical-mile":"Nautical mile","range-units":"Range units",range:"Range","range-required":"Range is required.","polygon-definition":"Polygon definition","polygon-definition-required":"Polygon definition is required.","polygon-definition-hint":"Please, use the following format for manual definition of polygon: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].","min-inside-duration":"Minimal inside duration","min-inside-duration-value-required":"Minimal inside duration is required","min-inside-duration-time-unit":"Minimal inside duration time unit","min-outside-duration":"Minimal outside duration","min-outside-duration-value-required":"Minimal outside duration is required","min-outside-duration-time-unit":"Minimal outside duration time unit","tell-failure-if-absent":"Tell Failure","tell-failure-if-absent-hint":'If at least one selected key doesn\'t exist the outbound message will report "Failure".',"get-latest-value-with-ts":"Fetch Latest telemetry with Timestamp","get-latest-value-with-ts-hint":'If selected, latest telemetry values will be added to the outbound message metadata with timestamp, e.g: "temp": "{"ts":1574329385897, "value":42}"',"use-redis-queue":"Use redis queue for message persistence","trim-redis-queue":"Trim redis queue","redis-queue-max-size":"Redis queue max size","add-metadata-key-values-as-kafka-headers":"Add Message metadata key-value pairs to Kafka record headers","add-metadata-key-values-as-kafka-headers-hint":"If selected, key-value pairs from message metadata will be added to the outgoing records headers as byte arrays with predefined charset encoding.","charset-encoding":"Charset encoding","charset-encoding-required":"Charset encoding is required.","charset-us-ascii":"US-ASCII","charset-iso-8859-1":"ISO-8859-1","charset-utf-8":"UTF-8","charset-utf-16be":"UTF-16BE","charset-utf-16le":"UTF-16LE","charset-utf-16":"UTF-16","select-queue-hint":"The queue name can be selected from a drop-down list or add a custom name.","persist-alarm-rules":"Persist state of alarm rules","fetch-alarm-rules":"Fetch state of alarm rules","input-value-key":"Input value key","input-value-key-required":"Input value key is required.","output-value-key":"Output value key","output-value-key-required":"Output value key is required.",round:"Decimals","round-range":"Decimals should be in a range from 0 to 15.","use-cache":"Use cache for latest value","tell-failure-if-delta-is-negative":"Tell Failure if delta is negative","add-period-between-msgs":"Add period between messages","period-value-key":"Period value key","period-key-required":"Period value key is required.","general-pattern-hint":'Hint: use ${metadataKey} for value from metadata, $[messageKey] for value from message body',"alarm-severity-pattern-hint":'Hint: use ${metadataKey} for value from metadata, $[messageKey] for value from message body. Alarm severity should be system (CRITICAL, MAJOR etc.)'},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}},!0)}(e)}return e.ctorParameters=function(){return[{type:n.TranslateService}]},e=h([t.NgModule({declarations:[x],imports:[r.CommonModule,a.SharedModule],exports:[qe,Pe,Ue,_e,x]}),C("design:paramtypes",[n.TranslateService])],e)}();e.RuleNodeCoreConfigModule=$e,e.ɵa=x,e.ɵb=qe,e.ɵba=ve,e.ɵbb=Fe,e.ɵbc=xe,e.ɵbd=Te,e.ɵbe=ue,e.ɵbf=ae,e.ɵbg=oe,e.ɵbh=ie,e.ɵbi=le,e.ɵbj=se,e.ɵbk=me,e.ɵbl=Pe,e.ɵbm=Se,e.ɵbn=Ie,e.ɵbo=ke,e.ɵbp=Ne,e.ɵbq=Ve,e.ɵbr=Ee,e.ɵbs=Ae,e.ɵbt=Le,e.ɵbu=Ue,e.ɵbv=Me,e.ɵbw=we,e.ɵbx=Re,e.ɵby=De,e.ɵbz=Oe,e.ɵc=T,e.ɵca=Ke,e.ɵcb=Be,e.ɵcc=Ge,e.ɵcd=He,e.ɵce=_e,e.ɵcf=je,e.ɵcg=ze,e.ɵch=Qe,e.ɵd=q,e.ɵe=S,e.ɵf=I,e.ɵg=k,e.ɵh=N,e.ɵi=V,e.ɵj=E,e.ɵk=A,e.ɵl=L,e.ɵm=P,e.ɵn=ee,e.ɵo=te,e.ɵp=re,e.ɵq=ne,e.ɵr=de,e.ɵs=pe,e.ɵt=ce,e.ɵu=fe,e.ɵv=ge,e.ɵw=ye,e.ɵx=be,e.ɵy=he,e.ɵz=Ce,Object.defineProperty(e,"__esModule",{value:!0})})); //# sourceMappingURL=rulenode-core-config.umd.min.js.map \ No newline at end of file diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java index cded6530f5..0b800eb22c 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java @@ -384,11 +384,131 @@ public class TbAlarmNodeTest { assertEquals(expectedAlarm, actualAlarm); } + @Test + public void testCreateAlarmWithDynamicSeverityFromMessageBody() throws Exception { + TbCreateAlarmNodeConfiguration config = new TbCreateAlarmNodeConfiguration(); + config.setPropagate(true); + config.setSeverity("$[alarmSeverity]"); + config.setAlarmType("SomeType"); + config.setAlarmDetailsBuildJs("DETAILS"); + config.setDynamicSeverity(true); + ObjectMapper mapper = new ObjectMapper(); + TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); + + when(ctx.createJsScriptEngine("DETAILS")).thenReturn(detailsJs); + + when(ctx.getTenantId()).thenReturn(tenantId); + when(ctx.getAlarmService()).thenReturn(alarmService); + when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor); + + node = new TbCreateAlarmNode(); + node.init(ctx, nodeConfiguration); + + String rawJson = "{\"alarmSeverity\": \"WARNING\", \"passed\": 5}"; + metaData.putValue("key", "value"); + TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + + when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); + when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null)); + doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class)); + + node.onMsg(ctx, msg); + + verify(ctx).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); + successCaptor.getValue().run(); + verify(ctx).tellNext(any(), eq("Created")); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); + ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); + verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + + assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(originator, originatorCaptor.getValue()); + assertEquals("value", metadataCaptor.getValue().getValue("key")); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_NEW_ALARM)); + assertNotSame(metaData, metadataCaptor.getValue()); + + Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); + Alarm expectedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(originator) + .status(ACTIVE_UNACK) + .severity(WARNING) + .propagate(true) + .type("SomeType") + .details(null) + .build(); + + assertEquals(expectedAlarm, actualAlarm); + } + + @Test + public void testCreateAlarmWithDynamicSeverityFromMetadata() throws Exception { + TbCreateAlarmNodeConfiguration config = new TbCreateAlarmNodeConfiguration(); + config.setPropagate(true); + config.setSeverity("${alarmSeverity}"); + config.setAlarmType("SomeType"); + config.setAlarmDetailsBuildJs("DETAILS"); + config.setDynamicSeverity(true); + ObjectMapper mapper = new ObjectMapper(); + TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); + + when(ctx.createJsScriptEngine("DETAILS")).thenReturn(detailsJs); + + when(ctx.getTenantId()).thenReturn(tenantId); + when(ctx.getAlarmService()).thenReturn(alarmService); + when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor); + + node = new TbCreateAlarmNode(); + node.init(ctx, nodeConfiguration); + + metaData.putValue("alarmSeverity", "WARNING"); + TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + + when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); + when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null)); + doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class)); + + node.onMsg(ctx, msg); + + verify(ctx).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); + successCaptor.getValue().run(); + verify(ctx).tellNext(any(), eq("Created")); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); + ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); + verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + + assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(originator, originatorCaptor.getValue()); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_NEW_ALARM)); + assertNotSame(metaData, metadataCaptor.getValue()); + + Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); + Alarm expectedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(originator) + .status(ACTIVE_UNACK) + .severity(WARNING) + .propagate(true) + .type("SomeType") + .details(null) + .build(); + + assertEquals(expectedAlarm, actualAlarm); + } + private void initWithCreateAlarmScript() { try { TbCreateAlarmNodeConfiguration config = new TbCreateAlarmNodeConfiguration(); config.setPropagate(true); - config.setSeverity(CRITICAL); + config.setSeverity(CRITICAL.name()); config.setAlarmType("SomeType"); config.setAlarmDetailsBuildJs("DETAILS"); ObjectMapper mapper = new ObjectMapper(); diff --git a/transport/coap/src/main/java/org/thingsboard/server/coap/ThingsboardCoapTransportApplication.java b/transport/coap/src/main/java/org/thingsboard/server/coap/ThingsboardCoapTransportApplication.java index 7afafcfb73..010a774e0e 100644 --- a/transport/coap/src/main/java/org/thingsboard/server/coap/ThingsboardCoapTransportApplication.java +++ b/transport/coap/src/main/java/org/thingsboard/server/coap/ThingsboardCoapTransportApplication.java @@ -26,7 +26,7 @@ import java.util.Arrays; @SpringBootConfiguration @EnableAsync @EnableScheduling -@ComponentScan({"org.thingsboard.server.coap", "org.thingsboard.server.common", "org.thingsboard.server.transport.coap", "org.thingsboard.server.queue"}) +@ComponentScan({"org.thingsboard.server.coap", "org.thingsboard.server.common", "org.thingsboard.server.coapserver", "org.thingsboard.server.transport.coap", "org.thingsboard.server.queue"}) public class ThingsboardCoapTransportApplication { private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name"; diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 0ffcd27f21..4d5bbf2c6f 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -49,6 +49,10 @@ transport: dtls: # Enable/disable DTLS 1.2 support enabled: "${COAP_DTLS_ENABLED:false}" + # CoAP DTLS bind address + bind_address: "${COAP_DTLS_BIND_ADDRESS:0.0.0.0}" + # CoAP DTLS bind port + bind_port: "${COAP_DTLS_BIND_PORT:5684}" # Secure mode. Allowed values: NO_AUTH, X509 mode: "${COAP_DTLS_SECURE_MODE:NO_AUTH}" # Path to the key store that holds the certificate diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index ec63dcae75..922aed5450 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -119,6 +119,18 @@ redis: # LWM2M server parameters transport: + sessions: + inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" + report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" + json: + # Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON + type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:false}" + # Maximum allowed string value length when processing Telemetry/Attributes JSON (0 value disables string value length check) + max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}" + client_side_rpc: + timeout: "${CLIENT_SIDE_RPC_TIMEOUT:60000}" + # Enable/disable http/mqtt/coap transport protocols (has higher priority than certain protocol's 'enabled' property) + api_enabled: "${TB_TRANSPORT_API_ENABLED:true}" # Local LwM2M transport parameters lwm2m: # Enable/disable lvm2m transport protocol. @@ -180,15 +192,6 @@ transport: # Use redis for Security and Registration stores redis.enabled: "${LWM2M_REDIS_ENABLED:false}" - sessions: - inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" - report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" - json: - # Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON - type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}" - # Maximum allowed string value length when processing Telemetry/Attributes JSON (0 value disables string value length check) - max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}" - queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) kafka: diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index a7e8257a47..782702dc11 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -16,21 +16,20 @@ import { AuthUser, User } from '@shared/models/user.model'; -export interface AuthPayload { - authUser: AuthUser; - userDetails: User; +export interface SysParamsState { userTokenAccessEnabled: boolean; allowedDashboardIds: string[]; - forceFullscreen: boolean; + edgesSupportEnabled: boolean; } -export interface AuthState { - isAuthenticated: boolean; - isUserLoaded: boolean; +export interface AuthPayload extends SysParamsState { authUser: AuthUser; userDetails: User; - userTokenAccessEnabled: boolean; - allowedDashboardIds: string[]; forceFullscreen: boolean; +} + +export interface AuthState extends AuthPayload { + isAuthenticated: boolean; + isUserLoaded: boolean; lastPublicDashboardId: string; } diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index 94dc95c6d2..9e3fe129f6 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -22,7 +22,8 @@ const emptyUserAuthState: AuthPayload = { userDetails: null, userTokenAccessEnabled: false, forceFullscreen: false, - allowedDashboardIds: [] + allowedDashboardIds: [], + edgesSupportEnabled: false }; export const initialState: AuthState = { diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 50160ad2d3..37802a08ed 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -31,7 +31,7 @@ import { ActionAuthAuthenticated, ActionAuthLoadUser, ActionAuthUnauthenticated import { getCurrentAuthState, getCurrentAuthUser } from './auth.selectors'; import { Authority } from '@shared/models/authority.enum'; import { ActionSettingsChangeLanguage } from '@app/core/settings/settings.actions'; -import { AuthPayload, AuthState } from '@core/auth/auth.models'; +import { AuthPayload, AuthState, SysParamsState } from '@core/auth/auth.models'; import { TranslateService } from '@ngx-translate/core'; import { AuthUser } from '@shared/models/user.model'; import { TimeService } from '@core/services/time.service'; @@ -428,16 +428,24 @@ export class AuthService { } } - private loadSystemParams(authPayload: AuthPayload): Observable { - const sources: Array> = [this.loadIsUserTokenAccessEnabled(authPayload.authUser), - this.fetchAllowedDashboardIds(authPayload), - this.timeService.loadMaxDatapointsLimit()]; + public loadIsEdgesSupportEnabled(): Observable { + return this.http.get('/api/edges/enabled', defaultHttpOptions()); + } + + private loadSystemParams(authPayload: AuthPayload): Observable { + const sources = [this.loadIsUserTokenAccessEnabled(authPayload.authUser), + this.fetchAllowedDashboardIds(authPayload), + this.loadIsEdgesSupportEnabled(), + this.timeService.loadMaxDatapointsLimit()]; return forkJoin(sources) .pipe(map((data) => { - const userTokenAccessEnabled: boolean = data[0]; - const allowedDashboardIds: string[] = data[1]; - return {userTokenAccessEnabled, allowedDashboardIds}; - })); + const userTokenAccessEnabled: boolean = data[0] as boolean; + const allowedDashboardIds: string[] = data[1] as string[]; + const edgesSupportEnabled: boolean = data[2] as boolean; + return {userTokenAccessEnabled, allowedDashboardIds, edgesSupportEnabled}; + }, catchError((err) => { + return of({}); + }))); } public refreshJwtToken(loadUserElseStoreJwtToken = true): Observable { diff --git a/ui-ngx/src/app/core/http/asset.service.ts b/ui-ngx/src/app/core/http/asset.service.ts index 0163e69f6a..aa4bb161d1 100644 --- a/ui-ngx/src/app/core/http/asset.service.ts +++ b/ui-ngx/src/app/core/http/asset.service.ts @@ -89,4 +89,20 @@ export class AssetService { return this.http.get(`/api/tenant/assets?assetName=${assetName}`, defaultHttpOptionsFromConfig(config)); } + public assignAssetToEdge(edgeId: string, assetId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/edge/${edgeId}/asset/${assetId}`, null, + defaultHttpOptionsFromConfig(config)); + } + + public unassignAssetFromEdge(edgeId: string, assetId: string, + config?: RequestConfig) { + return this.http.delete(`/api/edge/${edgeId}/asset/${assetId}`, defaultHttpOptionsFromConfig(config)); + } + + public getEdgeAssets(edgeId: string, pageLink: PageLink, type: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/edge/${edgeId}/assets${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/core/http/component-descriptor.service.ts b/ui-ngx/src/app/core/http/component-descriptor.service.ts index edca7a3543..0128131d7d 100644 --- a/ui-ngx/src/app/core/http/component-descriptor.service.ts +++ b/ui-ngx/src/app/core/http/component-descriptor.service.ts @@ -21,14 +21,15 @@ import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-uti import { Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; import { RuleNodeType } from '@shared/models/rule-node.models'; +import { RuleChainType } from '@shared/models/rule-chain.models'; @Injectable({ providedIn: 'root' }) export class ComponentDescriptorService { - private componentsByType: Map> = - new Map>(); + private componentsByTypeByRuleChainType: Map>> = + new Map>>(); private componentsByClazz: Map = new Map(); constructor( @@ -36,14 +37,17 @@ export class ComponentDescriptorService { ) { } - public getComponentDescriptorsByType(componentType: ComponentType, config?: RequestConfig): Observable> { - const existing = this.componentsByType.get(componentType); + public getComponentDescriptorsByType(componentType: ComponentType, ruleChainType: RuleChainType, config?: RequestConfig): Observable> { + if (!this.componentsByTypeByRuleChainType.get(ruleChainType)) { + this.componentsByTypeByRuleChainType.set(ruleChainType, new Map>()); + } + const existing = this.componentsByTypeByRuleChainType.get(ruleChainType).get(componentType); if (existing) { return of(existing); } else { - return this.http.get>(`/api/components/${componentType}`, defaultHttpOptionsFromConfig(config)).pipe( + return this.http.get>(`/api/components/${componentType}&ruleChainType=${ruleChainType}`, defaultHttpOptionsFromConfig(config)).pipe( map((componentDescriptors) => { - this.componentsByType.set(componentType, componentDescriptors); + this.componentsByTypeByRuleChainType.get(ruleChainType).set(componentType, componentDescriptors); componentDescriptors.forEach((componentDescriptor) => { this.componentsByClazz.set(componentDescriptor.clazz, componentDescriptor); }); @@ -53,12 +57,14 @@ export class ComponentDescriptorService { } } - public getComponentDescriptorsByTypes(componentTypes: Array, - config?: RequestConfig): Observable> { + public getComponentDescriptorsByTypes(componentTypes: Array, ruleChainType: RuleChainType, config?: RequestConfig): Observable> { + if (!this.componentsByTypeByRuleChainType.get(ruleChainType)) { + this.componentsByTypeByRuleChainType.set(ruleChainType, new Map>()); + } let result: ComponentDescriptor[] = []; for (let i = componentTypes.length - 1; i >= 0; i--) { const componentType = componentTypes[i]; - const componentDescriptors = this.componentsByType.get(componentType); + const componentDescriptors = this.componentsByTypeByRuleChainType.get(ruleChainType).get(componentType); if (componentDescriptors) { result = result.concat(componentDescriptors); componentTypes.splice(i, 1); @@ -67,14 +73,14 @@ export class ComponentDescriptorService { if (!componentTypes.length) { return of(result); } else { - return this.http.get>(`/api/components?componentTypes=${componentTypes.join(',')}`, + return this.http.get>(`/api/components?componentTypes=${componentTypes.join(',')}&ruleChainType=${ruleChainType}`, defaultHttpOptionsFromConfig(config)).pipe( map((componentDescriptors) => { componentDescriptors.forEach((componentDescriptor) => { - let componentsList = this.componentsByType.get(componentDescriptor.type); + let componentsList = this.componentsByTypeByRuleChainType.get(ruleChainType).get(componentDescriptor.type); if (!componentsList) { componentsList = new Array(); - this.componentsByType.set(componentDescriptor.type, componentsList); + this.componentsByTypeByRuleChainType.get(ruleChainType).set(componentDescriptor.type, componentsList); } componentsList.push(componentDescriptor); this.componentsByClazz.set(componentDescriptor.clazz, componentDescriptor); diff --git a/ui-ngx/src/app/core/http/dashboard.service.ts b/ui-ngx/src/app/core/http/dashboard.service.ts index 3f3e512752..bf7042e985 100644 --- a/ui-ngx/src/app/core/http/dashboard.service.ts +++ b/ui-ngx/src/app/core/http/dashboard.service.ts @@ -170,4 +170,22 @@ export class DashboardService { return this.stDiffObservable; } + public getEdgeDashboards(edgeId: string, pageLink: PageLink, type: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/edge/${edgeId}/dashboards${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)) + } + + public assignDashboardToEdge(edgeId: string, dashboardId: string, + config?: RequestConfig): Observable { + return this.http.post(`/api/edge/${edgeId}/dashboard/${dashboardId}`, null, + defaultHttpOptionsFromConfig(config)); + } + + public unassignDashboardFromEdge(edgeId: string, dashboardId: string, + config?: RequestConfig) { + return this.http.delete(`/api/edge/${edgeId}/dashboard/${dashboardId}`, + defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts index f7e82f3a41..7e3183255a 100644 --- a/ui-ngx/src/app/core/http/device-profile.service.ts +++ b/ui-ngx/src/app/core/http/device-profile.service.ts @@ -22,7 +22,7 @@ import {Observable} from 'rxjs'; import {PageData} from '@shared/models/page/page-data'; import {DeviceProfile, DeviceProfileInfo, DeviceTransportType} from '@shared/models/device.models'; import {isDefinedAndNotNull, isEmptyStr} from '@core/utils'; -import {ObjectLwM2M, ServerSecurityConfig} from '@home/components/profile/device/lwm2m/profile-config.models'; +import {ObjectLwM2M, ServerSecurityConfig} from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models'; import {SortOrder} from '@shared/models/page/sort-order'; @Injectable({ diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts index d1b9ab877b..04cf633e53 100644 --- a/ui-ngx/src/app/core/http/device.service.ts +++ b/ui-ngx/src/app/core/http/device.service.ts @@ -155,4 +155,22 @@ export class DeviceService { return this.http.delete(`/api/customer/device/${deviceName}/claim`, defaultHttpOptionsFromConfig(config)); } + public assignDeviceToEdge(edgeId: string, deviceId: string, + config?: RequestConfig): Observable { + return this.http.post(`/api/edge/${edgeId}/device/${deviceId}`, + defaultHttpOptionsFromConfig(config)); + } + + public unassignDeviceFromEdge(edgeId: string, deviceId: string, + config?: RequestConfig) { + return this.http.delete(`/api/edge/${edgeId}/device/${deviceId}`, + defaultHttpOptionsFromConfig(config)); + } + + public getEdgeDevices(edgeId: string, pageLink: PageLink, type: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/edge/${edgeId}/devices${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)) + } + } diff --git a/ui-ngx/src/app/core/http/edge.service.ts b/ui-ngx/src/app/core/http/edge.service.ts new file mode 100644 index 0000000000..7e4974aec4 --- /dev/null +++ b/ui-ngx/src/app/core/http/edge.service.ts @@ -0,0 +1,111 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { PageLink, TimePageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; +import { EntitySubtype } from '@app/shared/models/entity-type.models'; +import { Edge, EdgeEvent, EdgeInfo, EdgeSearchQuery } from '@shared/models/edge.models'; +import { EntityId } from '@shared/models/id/entity-id'; + +@Injectable({ + providedIn: 'root' +}) +export class EdgeService { + + constructor( + private http: HttpClient + ) { } + + public getEdges(edgeIds: Array, config?: RequestConfig): Observable> { + return this.http.get>(`/api/edges?edgeIds=${edgeIds.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + + public getEdge(edgeId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/edge/${edgeId}`, defaultHttpOptionsFromConfig(config)); + } + + public getEdgeInfo(edgeId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/edge/info/${edgeId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveEdge(edge: Edge, config?: RequestConfig): Observable { + return this.http.post('/api/edge', edge, defaultHttpOptionsFromConfig(config)); + } + + public deleteEdge(edgeId: string, config?: RequestConfig) { + return this.http.delete(`/api/edge/${edgeId}`, defaultHttpOptionsFromConfig(config)); + } + + public getEdgeTypes(config?: RequestConfig): Observable> { + return this.http.get>('/api/edge/types', defaultHttpOptionsFromConfig(config)); + } + + public getCustomerEdgeInfos(customerId: string, pageLink: PageLink, type: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/customer/${customerId}/edgeInfos${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)); + } + + public assignEdgeToCustomer(customerId: string, edgeId: string, + config?: RequestConfig): Observable { + return this.http.post(`/api/customer/${customerId}/edge/${edgeId}`, + defaultHttpOptionsFromConfig(config)); + } + + public unassignEdgeFromCustomer(edgeId: string, config?: RequestConfig) { + return this.http.delete(`/api/customer/edge/${edgeId}`, + defaultHttpOptionsFromConfig(config)); + } + + public makeEdgePublic(edgeId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/customer/public/edge/${edgeId}`, null, + defaultHttpOptionsFromConfig(config)); + } + + public getTenantEdgeInfos(pageLink: PageLink, type: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenant/edgeInfos${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)); + } + + public findByQuery(query: EdgeSearchQuery, config?: RequestConfig): Observable> { + return this.http.post>('/api/edges', query, + defaultHttpOptionsFromConfig(config)); + } + + public getEdgeEvents(entityId: EntityId, pageLink: TimePageLink, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/edge/${entityId.id}/events` + `${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public syncEdge(edgeId: string, config?: RequestConfig) { + return this.http.post(`/api/edge/sync/${edgeId}`, edgeId, defaultHttpOptionsFromConfig(config)); + } + + public findMissingToRelatedRuleChains(edgeId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/edge/missingToRelatedRuleChains/${edgeId}`, defaultHttpOptionsFromConfig(config)); + } + + public findByName(edgeName: string, config?: RequestConfig): Observable { + return this.http.get(`/api/tenant/edges?edgeName=${edgeName}`, defaultHttpOptionsFromConfig(config)); + } +} diff --git a/ui-ngx/src/app/core/http/entity-view.service.ts b/ui-ngx/src/app/core/http/entity-view.service.ts index 4b4bec1a49..5e9b33145d 100644 --- a/ui-ngx/src/app/core/http/entity-view.service.ts +++ b/ui-ngx/src/app/core/http/entity-view.service.ts @@ -83,4 +83,21 @@ export class EntityViewService { return this.http.post>('/api/entityViews', query, defaultHttpOptionsFromConfig(config)); } + public assignEntityViewToEdge(edgeId: string, entityViewId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/edge/${edgeId}/entityView/${entityViewId}`, null, + defaultHttpOptionsFromConfig(config)); + } + + public unassignEntityViewFromEdge(edgeId: string, entityViewId: string, + config?: RequestConfig) { + return this.http.delete(`/api/edge/${edgeId}/entityView/${entityViewId}`, + defaultHttpOptionsFromConfig(config)); + } + + public getEdgeEntityViews(edgeId: string, pageLink: PageLink, type: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/edge/${edgeId}/entityViews${pageLink.toQuery()}&type=${type}`, + defaultHttpOptionsFromConfig(config)) + } + } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 2d9b9808ed..04d2db610d 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -28,7 +28,7 @@ import { UserService } from './user.service'; import { DashboardService } from '@core/http/dashboard.service'; import { Direction } from '@shared/models/page/sort-order'; import { PageData } from '@shared/models/page/page-data'; -import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { getCurrentAuthState, getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Authority } from '@shared/models/authority.enum'; @@ -45,6 +45,7 @@ import { DataKey, Datasource, DatasourceType, KeyInfo } from '@app/shared/models import { UtilsService } from '@core/services/utils.service'; import { AliasFilterType, EntityAlias, EntityAliasFilter, EntityAliasFilterResult } from '@shared/models/alias.models'; import { + EdgeImportEntityData, EntitiesKeysByQuery, entityFields, EntityInfo, @@ -52,7 +53,7 @@ import { ImportEntityData } from '@shared/models/entity.models'; import { EntityRelationService } from '@core/http/entity-relation.service'; -import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils'; +import { deepClone, generateSecret, guid, isDefined, isDefinedAndNotNull } from '@core/utils'; import { Asset } from '@shared/models/asset.models'; import { Device, DeviceCredentialsType } from '@shared/models/device.models'; import { AttributeService } from '@core/http/attribute.service'; @@ -75,6 +76,11 @@ import { } from '@shared/models/query/query.models'; import { alarmFields } from '@shared/models/alarm.models'; import { FirmwareService } from '@core/http/firmware.service'; +import { EdgeService } from "@core/http/edge.service"; +import { Edge, EdgeEventType } from '@shared/models/edge.models'; +import { RuleChainType } from "@shared/models/rule-chain.models"; +import { WidgetService } from "@core/http/widget.service"; +import { DeviceProfileService } from "@core/http/device-profile.service"; @Injectable({ providedIn: 'root' @@ -85,6 +91,7 @@ export class EntityService { private http: HttpClient, private store: Store, private deviceService: DeviceService, + private edgeService: EdgeService, private assetService: AssetService, private entityViewService: EntityViewService, private tenantService: TenantService, @@ -95,6 +102,8 @@ export class EntityService { private entityRelationService: EntityRelationService, private attributeService: AttributeService, private firmwareService: FirmwareService, + private widgetService: WidgetService, + private deviceProfileService: DeviceProfileService, private utils: UtilsService ) { } @@ -109,6 +118,9 @@ export class EntityService { case EntityType.ASSET: observable = this.assetService.getAsset(entityId, config); break; + case EntityType.EDGE: + observable = this.edgeService.getEdge(entityId, config); + break; case EntityType.ENTITY_VIEW: observable = this.entityViewService.getEntityView(entityId, config); break; @@ -179,6 +191,9 @@ export class EntityService { case EntityType.ASSET: observable = this.assetService.getAssets(entityIds, config); break; + case EntityType.EDGE: + observable = this.edgeService.getEdges(entityIds, config); + break; case EntityType.ENTITY_VIEW: observable = this.getEntitiesByIdsObservable( (id) => this.entityViewService.getEntityView(id, config), @@ -287,6 +302,14 @@ export class EntityService { entitiesObservable = this.assetService.getTenantAssetInfos(pageLink, subType, config); } break; + case EntityType.EDGE: + pageLink.sortOrder.property = 'name'; + if (authUser.authority === Authority.CUSTOMER_USER) { + entitiesObservable = this.edgeService.getCustomerEdgeInfos(customerId, pageLink, subType, config); + } else { + entitiesObservable = this.edgeService.getTenantEdgeInfos(pageLink, subType, config); + } + break; case EntityType.ENTITY_VIEW: pageLink.sortOrder.property = 'name'; if (authUser.authority === Authority.CUSTOMER_USER) { @@ -314,7 +337,12 @@ export class EntityService { break; case EntityType.RULE_CHAIN: pageLink.sortOrder.property = 'name'; - entitiesObservable = this.ruleChainService.getRuleChains(pageLink, config); + if (RuleChainType[subType]) { + entitiesObservable = this.ruleChainService.getRuleChains(pageLink, subType as RuleChainType, config); + } else { + // safe fallback to default core type + entitiesObservable = this.ruleChainService.getRuleChains(pageLink, RuleChainType.CORE, config); + } break; case EntityType.DASHBOARD: pageLink.sortOrder.property = 'title'; @@ -498,6 +526,8 @@ export class EntityService { return entityTypes.indexOf(EntityType.ASSET) > -1 ? true : false; case AliasFilterType.deviceType: return entityTypes.indexOf(EntityType.DEVICE) > -1 ? true : false; + case AliasFilterType.edgeType: + return entityTypes.indexOf(EntityType.EDGE) > -1 ? true : false; case AliasFilterType.entityViewType: return entityTypes.indexOf(EntityType.ENTITY_VIEW) > -1 ? true : false; case AliasFilterType.relationsQuery: @@ -524,6 +554,8 @@ export class EntityService { return entityTypes.indexOf(EntityType.ASSET) > -1 ? true : false; case AliasFilterType.deviceSearchQuery: return entityTypes.indexOf(EntityType.DEVICE) > -1 ? true : false; + case AliasFilterType.edgeSearchQuery: + return entityTypes.indexOf(EntityType.EDGE) > -1 ? true : false; case AliasFilterType.entityViewSearchQuery: return entityTypes.indexOf(EntityType.ENTITY_VIEW) > -1 ? true : false; } @@ -559,14 +591,20 @@ export class EntityService { return entityType === EntityType.ASSET; case AliasFilterType.deviceType: return entityType === EntityType.DEVICE; + case AliasFilterType.edgeType: + return entityType === EntityType.EDGE; case AliasFilterType.entityViewType: return entityType === EntityType.ENTITY_VIEW; case AliasFilterType.relationsQuery: return true; + case AliasFilterType.apiUsageState: + return true; case AliasFilterType.assetSearchQuery: return entityType === EntityType.ASSET; case AliasFilterType.deviceSearchQuery: return entityType === EntityType.DEVICE; + case AliasFilterType.edgeSearchQuery: + return entityType === EntityType.EDGE; case AliasFilterType.entityViewSearchQuery: return entityType === EntityType.ENTITY_VIEW; } @@ -575,15 +613,18 @@ export class EntityService { public prepareAllowedEntityTypesList(allowedEntityTypes: Array, useAliasEntityTypes?: boolean): Array { - const authUser = getCurrentAuthUser(this.store); + const authState = getCurrentAuthState(this.store); const entityTypes: Array = []; - switch (authUser.authority) { + switch (authState.authUser.authority) { case Authority.SYS_ADMIN: entityTypes.push(EntityType.TENANT); break; case Authority.TENANT_ADMIN: entityTypes.push(EntityType.DEVICE); entityTypes.push(EntityType.ASSET); + if (authState.edgesSupportEnabled) { + entityTypes.push(EntityType.EDGE); + } entityTypes.push(EntityType.ENTITY_VIEW); entityTypes.push(EntityType.TENANT); entityTypes.push(EntityType.CUSTOMER); @@ -597,6 +638,9 @@ export class EntityService { case Authority.CUSTOMER_USER: entityTypes.push(EntityType.DEVICE); entityTypes.push(EntityType.ASSET); + if (authState.edgesSupportEnabled) { + entityTypes.push(EntityType.EDGE); + } entityTypes.push(EntityType.ENTITY_VIEW); entityTypes.push(EntityType.CUSTOMER); entityTypes.push(EntityType.USER); @@ -608,7 +652,7 @@ export class EntityService { } if (useAliasEntityTypes) { entityTypes.push(AliasEntityType.CURRENT_USER); - if (authUser.authority !== Authority.SYS_ADMIN) { + if (authState.authUser.authority !== Authority.SYS_ADMIN) { entityTypes.push(AliasEntityType.CURRENT_USER_OWNER); } } @@ -649,6 +693,7 @@ export class EntityService { entityFieldKeys.push(entityFields.type.keyName); break; case EntityType.DEVICE: + case EntityType.EDGE: case EntityType.ASSET: entityFieldKeys.push(entityFields.name.keyName); entityFieldKeys.push(entityFields.type.keyName); @@ -842,6 +887,9 @@ export class EntityService { case AliasFilterType.apiUsageState: result.entityFilter = deepClone(filter); return of(result); + case AliasFilterType.edgeType: + result.entityFilter = deepClone(filter); + return of(result); case AliasFilterType.relationsQuery: result.stateEntity = filter.rootStateEntity; let rootEntityType; @@ -863,6 +911,7 @@ export class EntityService { } case AliasFilterType.assetSearchQuery: case AliasFilterType.deviceSearchQuery: + case AliasFilterType.edgeSearchQuery: case AliasFilterType.entityViewSearchQuery: result.stateEntity = filter.rootStateEntity; if (result.stateEntity && stateEntityId) { @@ -898,6 +947,51 @@ export class EntityService { public saveEntityParameters(entityType: EntityType, entityData: ImportEntityData, update: boolean, config?: RequestConfig): Observable { + const saveEntityObservable: Observable> = this.getSaveEntityObservable(entityType, entityData, config); + return saveEntityObservable.pipe( + mergeMap((entity) => { + return this.saveEntityData(entity.id, entityData, config).pipe( + map(() => { + return { create: { entity: 1 } } as ImportEntitiesResultInfo; + }), + catchError(err => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) + ); + }), + catchError(err => { + if (update) { + let findEntityObservable: Observable>; + switch (entityType) { + case EntityType.DEVICE: + findEntityObservable = this.deviceService.findByName(entityData.name, config); + break; + case EntityType.ASSET: + findEntityObservable = this.assetService.findByName(entityData.name, config); + break; + case EntityType.EDGE: + findEntityObservable = this.edgeService.findByName(entityData.name, config); + break; + } + return findEntityObservable.pipe( + mergeMap((entity) => { + const updateEntityTasks: Observable[] = this.getUpdateEntityTasks(entityType, entityData, entity, config); + return forkJoin(updateEntityTasks).pipe( + map(() => { + return { update: { entity: 1 } } as ImportEntitiesResultInfo; + }), + catchError(updateError => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) + ); + }), + catchError(findErr => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) + ); + } else { + return of({ error: { entity: 1 } } as ImportEntitiesResultInfo); + } + }) + ); + } + + private getSaveEntityObservable(entityType: EntityType, entityData: ImportEntityData, + config?: RequestConfig): Observable> { let saveEntityObservable: Observable>; switch (entityType) { case EntityType.DEVICE: @@ -928,70 +1022,92 @@ export class EntityService { }; saveEntityObservable = this.assetService.saveAsset(asset, config); break; + case EntityType.EDGE: + const edgeEntityData: EdgeImportEntityData = entityData as EdgeImportEntityData; + const edge: Edge = { + name: edgeEntityData.name, + type: edgeEntityData.type, + label: edgeEntityData.label, + additionalInfo: { + description: edgeEntityData.description + }, + edgeLicenseKey: edgeEntityData.edgeLicenseKey, + cloudEndpoint: edgeEntityData.cloudEndpoint !== '' ? edgeEntityData.cloudEndpoint : window.location.origin, + routingKey: edgeEntityData.routingKey !== '' ? edgeEntityData.routingKey : guid(), + secret: edgeEntityData.secret !== '' ? edgeEntityData.secret : generateSecret(20) + }; + saveEntityObservable = this.edgeService.saveEdge(edge, config); + break; } - return saveEntityObservable.pipe( - mergeMap((entity) => { - return this.saveEntityData(entity.id, entityData, config).pipe( - map(() => { - return { create: { entity: 1 } } as ImportEntitiesResultInfo; - }), - catchError(err => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) - ); - }), - catchError(err => { - if (update) { - let findEntityObservable: Observable>; - switch (entityType) { + return saveEntityObservable; + + } + + private getUpdateEntityTasks(entityType: EntityType, entityData: ImportEntityData | EdgeImportEntityData, + entity: BaseData, config?: RequestConfig): Observable[] { + const tasks: Observable[] = []; + let result; + let additionalInfo; + switch (entityType) { + case EntityType.ASSET: + case EntityType.DEVICE: + result = entity as (Device | Asset); + additionalInfo = result.additionalInfo || {}; + if (result.label !== entityData.label || + result.type !== entityData.type || + additionalInfo.description !== entityData.description || + (result.id.entityType === EntityType.DEVICE && (additionalInfo.gateway !== entityData.gateway)) ) { + result.label = entityData.label; + result.type = entityData.type; + result.additionalInfo = additionalInfo; + result.additionalInfo.description = entityData.description; + if (result.id.entityType === EntityType.DEVICE) { + result.additionalInfo.gateway = entityData.gateway; + } + switch (result.id.entityType) { case EntityType.DEVICE: - findEntityObservable = this.deviceService.findByName(entityData.name, config); + tasks.push(this.deviceService.saveDevice(result, config)); break; case EntityType.ASSET: - findEntityObservable = this.assetService.findByName(entityData.name, config); + tasks.push(this.assetService.saveAsset(result, config)); break; } - return findEntityObservable.pipe( - mergeMap((entity) => { - const tasks: Observable[] = []; - const result: Device & Asset = entity as (Device | Asset); - const additionalInfo = result.additionalInfo || {}; - if (result.label !== entityData.label || - result.type !== entityData.type || - additionalInfo.description !== entityData.description || - (result.id.entityType === EntityType.DEVICE && (additionalInfo.gateway !== entityData.gateway)) ) { - result.label = entityData.label; - result.type = entityData.type; - result.additionalInfo = additionalInfo; - result.additionalInfo.description = entityData.description; - if (result.id.entityType === EntityType.DEVICE) { - result.additionalInfo.gateway = entityData.gateway; - } - if (result.id.entityType === EntityType.DEVICE && result.deviceProfileId) { - delete result.deviceProfileId; - } - switch (result.id.entityType) { - case EntityType.DEVICE: - tasks.push(this.deviceService.saveDevice(result, config)); - break; - case EntityType.ASSET: - tasks.push(this.assetService.saveAsset(result, config)); - break; - } - } - tasks.push(this.saveEntityData(entity.id, entityData, config)); - return forkJoin(tasks).pipe( - map(() => { - return { update: { entity: 1 } } as ImportEntitiesResultInfo; - }), - catchError(updateError => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) - ); - }), - catchError(findErr => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) - ); - } else { - return of({ error: { entity: 1 } } as ImportEntitiesResultInfo); } - }) - ); + tasks.push(this.saveEntityData(entity.id, entityData, config)); + break; + case EntityType.EDGE: + result = entity as Edge; + additionalInfo = result.additionalInfo || {}; + const edgeEntityData: EdgeImportEntityData = entityData as EdgeImportEntityData; + if (result.label !== edgeEntityData.label || + result.type !== edgeEntityData.type || + (edgeEntityData.cloudEndpoint !== '' && result.cloudEndpoint !== edgeEntityData.cloudEndpoint) || + (edgeEntityData.edgeLicenseKey !== '' && result.edgeLicenseKey !== edgeEntityData.edgeLicenseKey) || + (edgeEntityData.routingKey !== '' && result.routingKey !== edgeEntityData.routingKey) || + (edgeEntityData.secret !== '' && result.secret !== edgeEntityData.secret) || + additionalInfo.description !== edgeEntityData.description) { + result.label = edgeEntityData.label; + result.type = edgeEntityData.type; + result.additionalInfo = additionalInfo; + result.additionalInfo.description = edgeEntityData.description; + if (edgeEntityData.cloudEndpoint !== '') { + result.cloudEndpoint = edgeEntityData.cloudEndpoint; + } + if (edgeEntityData.edgeLicenseKey !== '') { + result.edgeLicenseKey = edgeEntityData.edgeLicenseKey; + } + if (edgeEntityData.routingKey !== '') { + result.routingKey = edgeEntityData.routingKey; + } + if (edgeEntityData.secret !== '') { + result.secret = edgeEntityData.secret; + } + tasks.push(this.edgeService.saveEdge(result, config)); + } + tasks.push(this.saveEntityData(entity.id, edgeEntityData, config)); + break; + } + return tasks; } public saveEntityData(entityId: EntityId, entityData: ImportEntityData, config?: RequestConfig): Observable { @@ -1203,4 +1319,62 @@ export class EntityService { datasource.dataKeys.push(dataKey); }); } + + public getAssignedToEdgeEntitiesByType(edgeId: string, entityType: EntityType, pageLink: PageLink): Observable> { + let entitiesObservable: Observable>; + switch (entityType) { + case (EntityType.ASSET): + entitiesObservable = this.assetService.getEdgeAssets(edgeId, pageLink); + break; + case (EntityType.DEVICE): + entitiesObservable = this.deviceService.getEdgeDevices(edgeId, pageLink); + break; + case (EntityType.ENTITY_VIEW): + entitiesObservable = this.entityViewService.getEdgeEntityViews(edgeId, pageLink); + break; + case (EntityType.DASHBOARD): + entitiesObservable = this.dashboardService.getEdgeDashboards(edgeId, pageLink); + break; + case (EntityType.RULE_CHAIN): + entitiesObservable = this.ruleChainService.getEdgeRuleChains(edgeId, pageLink); + break; + } + return entitiesObservable; + } + + public getEdgeEventContentByEntityType(entity: any): Observable { + let entityObservable: Observable; + const entityId: string = entity.entityId; + const entityType: any = entity.type; + switch (entityType) { + case EdgeEventType.DASHBOARD: + case EdgeEventType.ALARM: + case EdgeEventType.RULE_CHAIN: + case EdgeEventType.EDGE: + case EdgeEventType.USER: + case EdgeEventType.CUSTOMER: + case EdgeEventType.TENANT: + case EdgeEventType.ASSET: + case EdgeEventType.DEVICE: + case EdgeEventType.ENTITY_VIEW: + entityObservable = this.getEntity(entityType, entityId, { ignoreLoading: true, ignoreErrors: true }); + break; + case EdgeEventType.RULE_CHAIN_METADATA: + entityObservable = this.ruleChainService.getRuleChainMetadata(entityId); + break; + case EdgeEventType.WIDGET_TYPE: + entityObservable = this.widgetService.getWidgetTypeById(entityId); + break; + case EdgeEventType.WIDGETS_BUNDLE: + entityObservable = this.widgetService.getWidgetsBundle(entityId); + break; + case EdgeEventType.DEVICE_PROFILE: + entityObservable = this.deviceProfileService.getDeviceProfile(entityId); + break; + case EdgeEventType.RELATION: + entityObservable = of(entity.body); + break; + } + return entityObservable; + } } diff --git a/ui-ngx/src/app/core/http/event.service.ts b/ui-ngx/src/app/core/http/event.service.ts index 07d4c99aa7..fd740d73a7 100644 --- a/ui-ngx/src/app/core/http/event.service.ts +++ b/ui-ngx/src/app/core/http/event.service.ts @@ -21,7 +21,7 @@ import { HttpClient } from '@angular/common/http'; import { TimePageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; import { EntityId } from '@shared/models/id/entity-id'; -import { DebugEventType, Event, EventType } from '@shared/models/event.models'; +import { DebugEventType, Event, EventType, FilterEventBody } from '@shared/models/event.models'; @Injectable({ providedIn: 'root' @@ -38,4 +38,11 @@ export class EventService { `${pageLink.toQuery()}&tenantId=${tenantId}`, defaultHttpOptionsFromConfig(config)); } + + public getFilterEvents(entityId: EntityId, eventType: EventType | DebugEventType, tenantId: string, + filters: FilterEventBody, pageLink: TimePageLink, config?: RequestConfig): Observable> { + return this.http.post>(`/api/events/${entityId.entityType}/${entityId.id}` + + `${pageLink.toQuery()}&tenantId=${tenantId}`, {...filters, eventType}, defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/core/http/public-api.ts b/ui-ngx/src/app/core/http/public-api.ts index 69a5f2a5c5..74b69eb887 100644 --- a/ui-ngx/src/app/core/http/public-api.ts +++ b/ui-ngx/src/app/core/http/public-api.ts @@ -24,6 +24,7 @@ export * from './customer.service'; export * from './dashboard.service'; export * from './device.service'; export * from './entity.service'; +export * from './edge.service'; export * from './entity-relation.service'; export * from './entity-view.service'; export * from './event.service'; diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index 1b0796a389..2fef72fedb 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -43,14 +43,17 @@ export class ResourceService { } public downloadResource(resourceId: string): Observable { - return this.http.get(`/api/resource/${resourceId}/download`, { responseType: 'arraybuffer', observe: 'response' }).pipe( + return this.http.get(`/api/resource/${resourceId}/download`, { + responseType: 'arraybuffer', + observe: 'response' + }).pipe( map((response) => { const headers = response.headers; const filename = headers.get('x-filename'); const contentType = headers.get('content-type'); const linkElement = document.createElement('a'); try { - const blob = new Blob([response.body], { type: contentType }); + const blob = new Blob([response.body], {type: contentType}); const url = URL.createObjectURL(blob); linkElement.setAttribute('href', url); linkElement.setAttribute('download', filename); @@ -70,12 +73,12 @@ export class ResourceService { ); } - public saveResources(resources: Resource[], config?: RequestConfig) { + public saveResources(resources: Resource[], config?: RequestConfig): Observable { let partSize = 100; partSize = resources.length > partSize ? partSize : resources.length; - const resourceObservables = []; + const resourceObservables: Observable[] = []; for (let i = 0; i < partSize; i++) { - resourceObservables.push(this.saveResource(resources[i], config).pipe(catchError(error => of(error)))); + resourceObservables.push(this.saveResource(resources[i], config).pipe(catchError(() => of({} as Resource)))); } return forkJoin(resourceObservables).pipe( mergeMap((resource) => { diff --git a/ui-ngx/src/app/core/http/rule-chain.service.ts b/ui-ngx/src/app/core/http/rule-chain.service.ts index ef10f10b2c..e65e7017e0 100644 --- a/ui-ngx/src/app/core/http/rule-chain.service.ts +++ b/ui-ngx/src/app/core/http/rule-chain.service.ts @@ -26,6 +26,7 @@ import { RuleChainConnectionInfo, RuleChainMetaData, ruleChainNodeComponent, + RuleChainType, ruleNodeTypeComponentTypes, unknownNodeComponent } from '@shared/models/rule-chain.models'; @@ -43,13 +44,15 @@ import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; import { deepClone, snakeCase } from '@core/utils'; import { DebugRuleNodeEventBody } from '@app/shared/models/event.models'; +import { Edge } from '@shared/models/edge.models'; @Injectable({ providedIn: 'root' }) export class RuleChainService { - private ruleNodeComponents: Array; + private ruleNodeComponentsMap: Map> = + new Map>(); private ruleNodeConfigFactories: {[directive: string]: ComponentFactory} = {}; constructor( @@ -59,8 +62,8 @@ export class RuleChainService { private translate: TranslateService ) { } - public getRuleChains(pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/ruleChains${pageLink.toQuery()}`, + public getRuleChains(pageLink: PageLink, type: RuleChainType = RuleChainType.CORE, config?: RequestConfig): Observable> { + return this.http.get>(`/api/ruleChains${pageLink.toQuery()}&type=${type}`, defaultHttpOptionsFromConfig(config)); } @@ -116,18 +119,18 @@ export class RuleChainService { ); } - public getRuleNodeComponents(ruleNodeConfigResourcesModulesMap: {[key: string]: any}, config?: RequestConfig): + public getRuleNodeComponents(ruleNodeConfigResourcesModulesMap: {[key: string]: any}, ruleChainType: RuleChainType, config?: RequestConfig): Observable> { - if (this.ruleNodeComponents) { - return of(this.ruleNodeComponents); + if (this.ruleNodeComponentsMap.get(ruleChainType)) { + return of(this.ruleNodeComponentsMap.get(ruleChainType)); } else { - return this.loadRuleNodeComponents(config).pipe( + return this.loadRuleNodeComponents(ruleChainType, config).pipe( mergeMap((components) => { return this.resolveRuleNodeComponentsUiResources(components, ruleNodeConfigResourcesModulesMap).pipe( map((ruleNodeComponents) => { - this.ruleNodeComponents = ruleNodeComponents; - this.ruleNodeComponents.push(ruleChainNodeComponent); - this.ruleNodeComponents.sort( + this.ruleNodeComponentsMap.set(ruleChainType, ruleNodeComponents); + this.ruleNodeComponentsMap.get(ruleChainType).push(ruleChainNodeComponent); + this.ruleNodeComponentsMap.get(ruleChainType).sort( (comp1, comp2) => { let result = comp1.type.toString().localeCompare(comp2.type.toString()); if (result === 0) { @@ -136,7 +139,7 @@ export class RuleChainService { return result; } ); - return this.ruleNodeComponents; + return this.ruleNodeComponentsMap.get(ruleChainType); }) ); }) @@ -148,8 +151,8 @@ export class RuleChainService { return this.ruleNodeConfigFactories[directive]; } - public getRuleNodeComponentByClazz(clazz: string): RuleNodeComponentDescriptor { - const found = this.ruleNodeComponents.filter((component) => component.clazz === clazz); + public getRuleNodeComponentByClazz(ruleChainType: RuleChainType = RuleChainType.CORE, clazz: string): RuleNodeComponentDescriptor { + const found = this.ruleNodeComponentsMap.get(ruleChainType).filter((component) => component.clazz === clazz); if (found && found.length) { return found[0]; } else { @@ -204,8 +207,8 @@ export class RuleChainService { } } - private loadRuleNodeComponents(config?: RequestConfig): Observable> { - return this.componentDescriptorService.getComponentDescriptorsByTypes(ruleNodeTypeComponentTypes, config).pipe( + private loadRuleNodeComponents(ruleChainType: RuleChainType, config?: RequestConfig): Observable> { + return this.componentDescriptorService.getComponentDescriptorsByTypes(ruleNodeTypeComponentTypes, ruleChainType, config).pipe( map((components) => { const ruleNodeComponents: RuleNodeComponentDescriptor[] = []; components.forEach((component) => { @@ -292,4 +295,38 @@ export class RuleChainService { ); } + public getEdgeRuleChains(edgeId: string, pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/edge/${edgeId}/ruleChains${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public assignRuleChainToEdge(edgeId: string, ruleChainId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/edge/${edgeId}/ruleChain/${ruleChainId}`, null, + defaultHttpOptionsFromConfig(config)); + } + + public unassignRuleChainFromEdge(edgeId: string, ruleChainId: string, config?: RequestConfig) { + return this.http.delete(`/api/edge/${edgeId}/ruleChain/${ruleChainId}`, defaultHttpOptionsFromConfig(config)); + } + + public setEdgeTemplateRootRuleChain(ruleChainId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/ruleChain/${ruleChainId}/edgeTemplateRoot`, defaultHttpOptionsFromConfig(config)); + } + + public setAutoAssignToEdgeRuleChain(ruleChainId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/ruleChain/${ruleChainId}/autoAssignToEdge`, defaultHttpOptionsFromConfig(config)); + } + + public unsetAutoAssignToEdgeRuleChain(ruleChainId: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/ruleChain/${ruleChainId}/autoAssignToEdge`, defaultHttpOptionsFromConfig(config)); + } + + public getAutoAssignToEdgeRuleChains(config?: RequestConfig): Observable> { + return this.http.get>(`/api/ruleChain/autoAssignToEdgeRuleChains`, defaultHttpOptionsFromConfig(config)); + } + + public setEdgeRootRuleChain(edgeId: string, ruleChainId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/edge/${edgeId}/${ruleChainId}/root`, defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts index cabe78ff6e..6b0b9df4d0 100644 --- a/ui-ngx/src/app/core/services/item-buffer.service.ts +++ b/ui-ngx/src/app/core/services/item-buffer.service.ts @@ -292,6 +292,7 @@ export class ItemBufferService { y: origNode.y, name: origNode.name, componentClazz: origNode.component.clazz, + ruleChainType: origNode.ruleChainType }; if (origNode.targetRuleChainId) { node.targetRuleChainId = origNode.targetRuleChainId; @@ -330,7 +331,7 @@ export class ItemBufferService { const deltaX = x - ruleNodes.originX; const deltaY = y - ruleNodes.originY; for (const node of ruleNodes.nodes) { - const component = this.ruleChainService.getRuleNodeComponentByClazz(node.componentClazz); + const component = this.ruleChainService.getRuleNodeComponentByClazz(node.ruleChainType, node.componentClazz); if (component) { let icon = ruleNodeTypeDescriptors.get(component.type).icon; let iconUrl: string = null; diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index b14478f7e4..f1f286ca16 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -18,13 +18,13 @@ import { Injectable } from '@angular/core'; import { AuthService } from '../auth/auth.service'; import { select, Store } from '@ngrx/store'; import { AppState } from '../core.state'; -import { selectAuthUser, selectIsAuthenticated } from '../auth/auth.selectors'; +import { selectAuth, selectIsAuthenticated } from '../auth/auth.selectors'; import { take } from 'rxjs/operators'; import { HomeSection, MenuSection } from '@core/services/menu.models'; import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { Authority } from '@shared/models/authority.enum'; -import { AuthUser } from '@shared/models/user.model'; import { guid } from '@core/utils'; +import { AuthState } from '@core/auth/auth.models'; @Injectable({ providedIn: 'root' @@ -45,23 +45,23 @@ export class MenuService { } private buildMenu() { - this.store.pipe(select(selectAuthUser), take(1)).subscribe( - (authUser: AuthUser) => { - if (authUser) { + this.store.pipe(select(selectAuth), take(1)).subscribe( + (authState: AuthState) => { + if (authState.authUser) { let menuSections: Array; let homeSections: Array; - switch (authUser.authority) { + switch (authState.authUser.authority) { case Authority.SYS_ADMIN: - menuSections = this.buildSysAdminMenu(authUser); - homeSections = this.buildSysAdminHome(authUser); + menuSections = this.buildSysAdminMenu(authState); + homeSections = this.buildSysAdminHome(authState); break; case Authority.TENANT_ADMIN: - menuSections = this.buildTenantAdminMenu(authUser); - homeSections = this.buildTenantAdminHome(authUser); + menuSections = this.buildTenantAdminMenu(authState); + homeSections = this.buildTenantAdminHome(authState); break; case Authority.CUSTOMER_USER: - menuSections = this.buildCustomerUserMenu(authUser); - homeSections = this.buildCustomerUserHome(authUser); + menuSections = this.buildCustomerUserMenu(authState); + homeSections = this.buildCustomerUserHome(authState); break; } this.menuSections$.next(menuSections); @@ -71,7 +71,7 @@ export class MenuService { ); } - private buildSysAdminMenu(authUser: any): Array { + private buildSysAdminMenu(authState: AuthState): Array { const sections: Array = []; sections.push( { @@ -159,7 +159,7 @@ export class MenuService { return sections; } - private buildSysAdminHome(authUser: any): Array { + private buildSysAdminHome(authState: AuthState): Array { const homeSections: Array = []; homeSections.push( { @@ -232,7 +232,7 @@ export class MenuService { return homeSections; } - private buildTenantAdminMenu(authUser: any): Array { + private buildTenantAdminMenu(authState: AuthState): Array { const sections: Array = []; sections.push( { @@ -292,7 +292,37 @@ export class MenuService { type: 'link', path: '/entityViews', icon: 'view_quilt' - }, + } + ); + if (authState.edgesSupportEnabled) { + sections.push( + { + id: guid(), + name: 'edge.management', + type: 'toggle', + path: '/edges', + height: '80px', + icon: 'router', + pages: [ + { + id: guid(), + name: 'edge.edge-instances', + type: 'link', + path: '/edges', + icon: 'router' + }, + { + id: guid(), + name: 'edge.rulechain-templates', + type: 'link', + path: '/edges/ruleChains', + icon: 'settings_ethernet' + } + ] + } + ); + } + sections.push( { id: guid(), name: 'widget.widget-library', @@ -340,7 +370,7 @@ export class MenuService { return sections; } - private buildTenantAdminHome(authUser: any): Array { + private buildTenantAdminHome(authState: AuthState): Array { const homeSections: Array = []; homeSections.push( { @@ -403,7 +433,28 @@ export class MenuService { path: '/entityViews' } ] - }, + } + ); + if (authState.edgesSupportEnabled) { + homeSections.push( + { + name: 'edge.management', + places: [ + { + name: 'edge.edge-instances', + icon: 'router', + path: '/edges' + }, + { + name: 'edge.rulechain-templates', + icon: 'settings_ethernet', + path: '/edges/ruleChains' + } + ] + } + ); + } + homeSections.push( { name: 'resource.management', places: [ @@ -448,7 +499,7 @@ export class MenuService { return homeSections; } - private buildCustomerUserMenu(authUser: any): Array { + private buildCustomerUserMenu(authState: AuthState): Array { const sections: Array = []; sections.push( { @@ -479,7 +530,20 @@ export class MenuService { type: 'link', path: '/entityViews', icon: 'view_quilt' - }, + } + ); + if (authState.edgesSupportEnabled) { + sections.push( + { + id: guid(), + name: 'edge.edge-instances', + type: 'link', + path: '/edges', + icon: 'router' + } + ); + } + sections.push( { id: guid(), name: 'dashboard.dashboards', @@ -491,8 +555,9 @@ export class MenuService { return sections; } - private buildCustomerUserHome(authUser: any): Array { - const homeSections: Array = [ + private buildCustomerUserHome(authState: AuthState): Array { + const homeSections: Array = []; + homeSections.push( { name: 'asset.view-assets', places: [ @@ -522,7 +587,23 @@ export class MenuService { path: '/entityViews' } ] - }, + } + ); + if (authState.edgesSupportEnabled) { + homeSections.push( + { + name: 'edge.management', + places: [ + { + name: 'edge.edge-instances', + icon: 'router', + path: '/edges' + } + ] + } + ); + } + homeSections.push( { name: 'dashboard.view-dashboards', places: [ @@ -533,7 +614,7 @@ export class MenuService { } ] } - ]; + ); return homeSections; } diff --git a/ui-ngx/src/app/core/services/time.service.ts b/ui-ngx/src/app/core/services/time.service.ts index 47c8d8a456..23fc215573 100644 --- a/ui-ngx/src/app/core/services/time.service.ts +++ b/ui-ngx/src/app/core/services/time.service.ts @@ -31,7 +31,7 @@ import { isDefined } from '@core/utils'; export interface TimeInterval { name: string; - translateParams: {[key: string]: any}; + translateParams: { [key: string]: any }; value: number; } @@ -56,14 +56,14 @@ export class TimeService { public loadMaxDatapointsLimit(): Observable { return this.http.get('/api/dashboard/maxDatapointsLimit', defaultHttpOptions(true)).pipe( - map( (limit) => { - this.maxDatapointsLimit = limit; - if (!this.maxDatapointsLimit || this.maxDatapointsLimit <= MIN_LIMIT) { - this.maxDatapointsLimit = MIN_LIMIT + 1; - } - return this.maxDatapointsLimit; - }) - ); + map((limit) => { + this.maxDatapointsLimit = limit; + if (!this.maxDatapointsLimit || this.maxDatapointsLimit <= MIN_LIMIT) { + this.maxDatapointsLimit = MIN_LIMIT + 1; + } + return this.maxDatapointsLimit; + }) + ); } public matchesExistingInterval(min: number, max: number, intervalMs: number): boolean { @@ -79,7 +79,7 @@ export class TimeService { public boundMinInterval(min: number): number { if (isDefined(min)) { - min = Math.floor(min / 1000) * 1000; + min = Math.ceil(min / 1000) * 1000; } return this.toBound(min, MIN_INTERVAL, MAX_INTERVAL, MIN_INTERVAL); } diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 17c769e5ae..7be030bf54 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -127,6 +127,10 @@ export function isEmpty(obj: any): boolean { return true; } +export function isLiteralObject(value: any) { + return (!!value) && (value.constructor === Object); +} + export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined { if (isDefinedAndNotNull(value) && isNumeric(value) && (isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) { @@ -287,7 +291,7 @@ export function deepClone(target: T, ignoreFields?: string[]): T { return cp.map((n: any) => deepClone(n)) as any; } if (typeof target === 'object' && target !== {}) { - const cp = { ...(target as { [key: string]: any }) } as { [key: string]: any }; + const cp = {...(target as { [key: string]: any })} as { [key: string]: any }; Object.keys(cp).forEach(k => { if (!ignoreFields || ignoreFields.indexOf(k) === -1) { cp[k] = deepClone(cp[k]); diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts index 2aa43f5f2b..08119e6ce8 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts @@ -46,7 +46,7 @@ export interface EntityAliasesDialogData { widgets: Array; isSingleEntityAlias?: boolean; isSingleWidget?: boolean; - allowedEntityTypes?: Array; + allowedEntityTypes?: Array; disableAdd?: boolean; singleEntityAlias?: EntityAlias; customTitle?: string; diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html index 762b7149a2..3f03ea4fff 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html @@ -56,6 +56,8 @@ [syncStateWithQueryParam]="syncStateWithQueryParam" [states]="dashboardConfiguration.states"> +
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss index c7d2367975..6ace69d312 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss @@ -54,6 +54,11 @@ div.tb-dashboard-page { z-index: 13; pointer-events: none; + .dashboard_logo{ + height: 75%; + margin-right: 16px; + } + &.tb-dashboard-toolbar-opened { right: 0; // transition: right .3s cubic-bezier(.55, 0, .55, .2); diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 45ff291954..2b325e1aa0 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -90,7 +90,6 @@ import { } from '@home/components/alias/entity-aliases-dialog.component'; import { EntityAliases } from '@app/shared/models/alias.models'; import { EditWidgetComponent } from '@home/components/dashboard-page/edit-widget.component'; -import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { AddWidgetDialogComponent, AddWidgetDialogData @@ -118,10 +117,10 @@ import { ComponentPortal } from '@angular/cdk/portal'; import { DISPLAY_WIDGET_TYPES_PANEL_DATA, DisplayWidgetTypesPanelComponent, - DisplayWidgetTypesPanelData, - WidgetTypes + DisplayWidgetTypesPanelData } from '@home/components/dashboard-page/widget-types-panel.component'; import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/dashboard-widget-select.component'; +import {AliasEntityType, EntityType} from "@shared/models/entity-type.models"; // @dynamic @Component({ @@ -175,6 +174,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC isToolbarOpenedAnimate = false; isRightLayoutOpened = false; + allowedEntityTypes: Array = null; + editingWidget: Widget = null; editingWidgetLayout: WidgetLayout = null; editingWidgetOriginal: Widget = null; @@ -190,6 +191,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC addingLayoutCtx: DashboardPageLayoutContext; + logo = 'assets/logo_title_white.svg'; dashboardCtx: DashboardContext = { instanceId: this.utils.guid(), @@ -351,6 +353,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC }; this.window.parent.postMessage(JSON.stringify(message), '*'); } + + this.allowedEntityTypes = this.entityService.prepareAllowedEntityTypesList(null, true); } private reset() { @@ -481,6 +485,19 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } } + public showDashboardLogo(): boolean { + if (this.dashboard.configuration.settings && + isDefined(this.dashboard.configuration.settings.showDashboardLogo)) { + return this.dashboard.configuration.settings.showDashboardLogo && (this.forceFullscreen || this.singlePageMode || this.isFullscreen); + } else { + return false; + } + } + + public get dashboardLogo(): string { + return this.dashboard.configuration.settings.dashboardLogoUrl || this.logo; + } + public showRightLayoutSwitch(): boolean { return this.isMobile && this.layouts.right.show; } @@ -555,7 +572,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC data: { entityAliases: deepClone(this.dashboard.configuration.entityAliases), widgets: this.dashboardUtils.getWidgetsArray(this.dashboard), - isSingleEntityAlias: false + isSingleEntityAlias: false, + allowedEntityTypes: this.allowedEntityTypes } }).afterClosed().subscribe((entityAliases) => { if (entityAliases) { @@ -601,7 +619,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { settings: deepClone(this.dashboard.configuration.settings), - gridSettings + gridSettings, } }).afterClosed().subscribe((data) => { if (data) { @@ -1183,13 +1201,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC overlayRef.dispose(); }); + const filterWidgetTypes = this.dashboardWidgetSelectComponent.filterWidgetTypes; + const widgetTypesList = Array.from(this.dashboardWidgetSelectComponent.widgetTypes.values()).map(type => { + return {type, display: filterWidgetTypes === null ? true : filterWidgetTypes.includes(type)}; + }); + const providers: StaticProvider[] = [ { provide: DISPLAY_WIDGET_TYPES_PANEL_DATA, useValue: { - types: Array.from(this.dashboardWidgetSelectComponent.widgetTypes.values()).map(type => { - return {type, display: true}; - }), + types: widgetTypesList, typesUpdated: (newTypes) => { this.filterWidgetTypes = newTypes.filter(type => type.display).map(type => type.type); } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html index ede8052c68..244045314b 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html @@ -52,7 +52,8 @@ formControlName="titleColor">
-
+
{{ 'dashboard.display-dashboards-selection' | translate }} @@ -69,6 +70,13 @@ {{ 'dashboard.display-dashboard-export' | translate }}
+ + {{ 'dashboard.display-dashboard-logo' | translate }} + + +
{ + return this.filterWidgetTypes$.value; + } + @Output() widgetSelected: EventEmitter = new EventEmitter(); diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html index d893ec848f..c050b524cf 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html @@ -80,7 +80,7 @@ (mousedown)="widgetMouseDown($event, widget)" (click)="widgetClicked($event, widget)" (contextmenu)="openWidgetContextMenu($event, widget)"> -
+
{ + + queueStartTs: number; + + constructor(private edgeService: EdgeService, + private entityService: EntityService, + private dialogService: DialogService, + private translate: TranslateService, + private attributeService: AttributeService, + private datePipe: DatePipe, + private dialog: MatDialog, + public entityId: EntityId) { + super(); + + this.tableTitle = ''; + this.loadDataOnInit = false; + this.useTimePageLink = true; + this.detailsPanelEnabled = false; + this.selectionEnabled = false; + this.searchEnabled = false; + this.addEnabled = false; + this.entitiesDeleteEnabled = false; + + this.headerComponent = EdgeDownlinkTableHeaderComponent; + this.entityTranslations = { + noEntities: 'edge.no-downlinks-prompt' + }; + this.entityResources = {} as EntityTypeResource; + this.entitiesFetchFunction = pageLink => this.fetchEvents(pageLink); + this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + + this.updateColumns(); + } + + fetchEvents(pageLink: TimePageLink): Observable> { + this.loadEdgeInfo(); + return this.edgeService.getEdgeEvents(this.entityId, pageLink); + } + + loadEdgeInfo(): void { + this.attributeService.getEntityAttributes(this.entityId, AttributeScope.SERVER_SCOPE, ['queueStartTs']) + .subscribe( + attributes => this.onUpdate(attributes) + ); + } + + onUpdate(attributes) { + this.queueStartTs = 0; + let edge = attributes.reduce(function (map, attribute) { + map[attribute.key] = attribute; + return map; + }, {}); + if (edge.queueStartTs) { + this.queueStartTs = edge.queueStartTs.lastUpdateTs; + } + } + + updateColumns(updateTableColumns: boolean = false): void { + this.columns = []; + this.columns.push( + new DateEntityTableColumn('createdTime', 'event.event-time', this.datePipe, '120px'), + new EntityTableColumn('type', 'event.type', '25%', + entity => this.translate.instant(edgeEventTypeTranslations.get(entity.type)), entity => ({}), false), + new EntityTableColumn('action', 'edge.event-action', '25%', + entity => this.translate.instant(edgeEventActionTypeTranslations.get(entity.action)), entity => ({}), false), + new EntityTableColumn('entityId', 'edge.entity-id', '40%', + (entity) => entity.entityId, entity => ({}), false), + new EntityTableColumn('status', 'event.status', '10%', + (entity) => this.updateEdgeEventStatus(entity.createdTime), + entity => ({ + color: this.isPending(entity.createdTime) ? edgeEventStatusColor.get(EdgeEventStatus.PENDING) : edgeEventStatusColor.get(EdgeEventStatus.DEPLOYED) + }), false), + new EntityActionTableColumn('data', 'event.data', + { + name: this.translate.instant('action.view'), + icon: 'more_horiz', + isEnabled: (entity) => this.isEdgeEventHasData(entity.type), + onAction: ($event, entity) => + { + this.prepareEdgeEventContent(entity).subscribe((content) => { + this.showEdgeEventContent($event, content,'event.data'); + }); + } + }, + '40px'), + ); + if (updateTableColumns) { + this.table.columnsUpdated(true); + } + } + + updateEdgeEventStatus(createdTime): string { + if (this.queueStartTs && createdTime < this.queueStartTs) { + return this.translate.instant('edge.deployed'); + } else { + return this.translate.instant('edge.pending'); + } + } + + isPending(createdTime): boolean { + return createdTime > this.queueStartTs; + } + + isEdgeEventHasData(edgeEventType: EdgeEventType): boolean { + switch (edgeEventType) { + case EdgeEventType.ADMIN_SETTINGS: + return false; + default: + return true; + } + } + + prepareEdgeEventContent(entity: any): Observable { + return this.entityService.getEdgeEventContentByEntityType(entity).pipe( + map((result) => JSON.stringify(result)) + ); + } + + showEdgeEventContent($event: MouseEvent, content: string, title: string): void { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(EventContentDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + content, + title, + contentType: ContentType.JSON + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-header.component.html b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-header.component.html new file mode 100644 index 0000000000..8c23f6ac53 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-header.component.html @@ -0,0 +1,19 @@ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-header.component.scss b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-header.component.scss new file mode 100644 index 0000000000..aecdb0ae5a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-header.component.scss @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host { + padding-right: 8px; +} + +:host ::ng-deep { + mat-form-field { + + .mat-form-field-wrapper { + padding-bottom: 0; + } + + .mat-form-field-underline { + bottom: 0; + } + + @media #{$mat-xs} { + width: 100%; + + .mat-form-field-infix { + width: auto !important; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-header.component.ts b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-header.component.ts new file mode 100644 index 0000000000..33adc55b59 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-header.component.ts @@ -0,0 +1,42 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component'; +import { EdgeEvent } from "@shared/models/edge.models"; +import { EdgeDownlinkTableConfig } from "@home/components/edge/edge-downlink-table-config"; + +@Component({ + selector: 'tb-edge-downlink-table-header', + templateUrl: './edge-downlink-table-header.component.html', + styleUrls: ['./edge-downlink-table-header.component.scss'] +}) +export class EdgeDownlinkTableHeaderComponent extends EntityTableHeaderComponent { + + get eventTableConfig(): EdgeDownlinkTableConfig { + return this.entitiesTableConfig as EdgeDownlinkTableConfig; + } + + constructor(protected store: Store) { + super(store); + } + + eventTypeChanged() { + this.eventTableConfig.table.resetSortAndFilter(true, true); + } +} diff --git a/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table.component.html b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table.component.html new file mode 100644 index 0000000000..acefb832fd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table.component.html @@ -0,0 +1,18 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table.component.scss b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table.component.scss new file mode 100644 index 0000000000..9d50601a10 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + tb-entities-table { + .mat-drawer-container { + background-color: white; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table.component.ts b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table.component.ts new file mode 100644 index 0000000000..3b5fca1c7a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table.component.ts @@ -0,0 +1,90 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { MatDialog } from '@angular/material/dialog'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { EdgeDownlinkTableConfig } from './edge-downlink-table-config'; +import { DialogService } from '@core/services/dialog.service'; +import { AttributeService } from '@core/http/attribute.service'; +import { EdgeService } from '@core/http/edge.service'; +import { EntityService } from "@core/http/entity.service"; + +@Component({ + selector: 'tb-edge-downlink-table', + templateUrl: './edge-downlink-table.component.html', + styleUrls: ['./edge-downlink-table.component.scss'] +}) +export class EdgeDownlinkTableComponent implements OnInit { + + activeValue = false; + dirtyValue = false; + entityIdValue: EntityId; + + @Input() + set active(active: boolean) { + if (this.activeValue !== active) { + this.activeValue = active; + if (this.activeValue && this.dirtyValue) { + this.dirtyValue = false; + this.entitiesTable.updateData(); + } + } + } + + @Input() + set entityId(entityId: EntityId) { + this.entityIdValue = entityId; + if (this.edgeDownlinkTableConfig && this.edgeDownlinkTableConfig.entityId !== entityId) { + this.edgeDownlinkTableConfig.entityId = entityId; + this.entitiesTable.resetSortAndFilter(this.activeValue); + if (!this.activeValue) { + this.dirtyValue = true; + } + } + } + + @ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent; + + edgeDownlinkTableConfig: EdgeDownlinkTableConfig; + + constructor(private edgeService: EdgeService, + private entityService: EntityService, + private dialogService: DialogService, + private translate: TranslateService, + private attributeService: AttributeService, + private datePipe: DatePipe, + private dialog: MatDialog) { + } + + ngOnInit() { + this.dirtyValue = !this.activeValue; + this.edgeDownlinkTableConfig = new EdgeDownlinkTableConfig( + this.edgeService, + this.entityService, + this.dialogService, + this.translate, + this.attributeService, + this.datePipe, + this.dialog, + this.entityIdValue + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts index 07cee4ffb2..65af0f8e81 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts @@ -105,6 +105,17 @@ export class EntityFilterViewComponent implements ControlValueAccessor { {deviceType}); } break; + case AliasFilterType.edgeType: + const edgeType = this.filter.edgeType; + prefix = this.filter.edgeNameFilter; + if (prefix && prefix.length) { + this.filterDisplayValue = this.translate.instant('alias.filter-type-edge-type-and-name-description', + {edgeType, prefix}); + } else { + this.filterDisplayValue = this.translate.instant('alias.filter-type-edge-type-description', + {edgeType}); + } + break; case AliasFilterType.entityViewType: const entityView = this.filter.entityViewType; prefix = this.filter.entityViewNameFilter; @@ -170,6 +181,7 @@ export class EntityFilterViewComponent implements ControlValueAccessor { break; case AliasFilterType.assetSearchQuery: case AliasFilterType.deviceSearchQuery: + case AliasFilterType.edgeSearchQuery: case AliasFilterType.entityViewSearchQuery: allEntitiesText = this.translate.instant('alias.all-entities'); anyRelationText = this.translate.instant('alias.any-relation'); @@ -211,6 +223,16 @@ export class EntityFilterViewComponent implements ControlValueAccessor { this.filterDisplayValue = this.translate.instant('alias.filter-type-device-search-query-description', translationValues ); + } else if (this.filter.type === AliasFilterType.edgeSearchQuery) { + const edgeTypesQuoted = []; + this.filter.edgeTypes.forEach((filterEdgeType) => { + edgeTypesQuoted.push(`'${filterEdgeType}'`); + }); + const edgeTypesText = edgeTypesQuoted.join(', '); + translationValues.edgeTypes = edgeTypesText; + this.filterDisplayValue = this.translate.instant('alias.filter-type-edge-search-query-description', + translationValues + ); } else if (this.filter.type === AliasFilterType.entityViewSearchQuery) { const entityViewTypesQuoted = []; this.filter.entityViewTypes.forEach((filterEntityViewType) => { diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html index 5c5ca5ff59..e45c700b08 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html @@ -110,6 +110,16 @@ + + + + + edge.name-starts-with + + +
@@ -176,6 +186,7 @@ @@ -255,6 +266,14 @@ formControlName="deviceTypes"> + +
edge.edge-types
+ + +
entity-view.entity-view-types
+
+ + + + + {{ column.title | translate}} + + {{ 'event.all-events' | translate}} + + {{ value }} + + + + + + + {{ column.title | translate}} + + + {{ 'event.min-value' | translate }} + + + + + + {{ 'event.has-error' | translate }} + + + + + {{ column.title | translate}} + + + + + + {{ column.title | translate}} + + + + + +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.scss b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.scss new file mode 100644 index 0000000000..971b81689f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.scss @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: 100%; + min-width: 300px; + overflow: auto; + background: #fff; + border-radius: 4px; + box-shadow: + 0 7px 8px -4px rgba(0, 0, 0, .2), + 0 13px 19px 2px rgba(0, 0, 0, .14), + 0 5px 24px 4px rgba(0, 0, 0, .12); + + .mat-content { + overflow: hidden; + background-color: #fff; + } + + .mat-padding { + padding: 16px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.ts b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.ts new file mode 100644 index 0000000000..f60f7a09f7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.ts @@ -0,0 +1,109 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, InjectionToken } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { OverlayRef } from '@angular/cdk/overlay'; +import { EntityType } from '@shared/models/entity-type.models'; +import { FilterEventBody } from '@shared/models/event.models'; +import { deepTrim } from '@core/utils'; + +export const EVENT_FILTER_PANEL_DATA = new InjectionToken('AlarmFilterPanelData'); + +export interface EventFilterPanelData { + filterParams: FilterEventBody; + columns: Array; +} + +export interface FilterEntityColumn { + key: string; + title: string; +} + + +@Component({ + selector: 'tb-event-filter-panel', + templateUrl: './event-filter-panel.component.html', + styleUrls: ['./event-filter-panel.component.scss'] +}) +export class EventFilterPanelComponent { + + eventFilterFormGroup: FormGroup; + result: EventFilterPanelData; + + private conditionError = false; + + private msgDirectionTypes = ['IN', 'OUT']; + private statusTypes = ['Success', 'Failure']; + private entityTypes = Object.keys(EntityType); + + showColumns: FilterEntityColumn[] = []; + + constructor(@Inject(EVENT_FILTER_PANEL_DATA) + public data: EventFilterPanelData, + public overlayRef: OverlayRef, + private fb: FormBuilder) { + this.eventFilterFormGroup = this.fb.group({}); + this.data.columns.forEach((column) => { + this.showColumns.push(column); + const validators = []; + if (this.isNumberFields(column.key)) { + validators.push(Validators.min(0)); + } + this.eventFilterFormGroup.addControl(column.key, this.fb.control(this.data.filterParams[column.key] || '', validators)); + if (column.key === 'isError') { + this.conditionError = true; + } + }); + } + + isSelector(key: string): string { + return ['msgDirectionType', 'status', 'entityName'].includes(key) ? key : ''; + } + + isNumberFields(key: string): string { + return ['messagesProcessed', 'errorsOccurred'].includes(key) ? key : ''; + } + + selectorValues(key: string): string[] { + switch (key) { + case 'msgDirectionType': + return this.msgDirectionTypes; + case 'status': + return this.statusTypes; + case 'entityName': + return this.entityTypes; + } + } + + update() { + const filter = deepTrim(Object.fromEntries(Object.entries(this.eventFilterFormGroup.value).filter(([_, v]) => v !== ''))); + this.result = { + filterParams: filter, + columns: this.data.columns + }; + this.overlayRef.dispose(); + } + + showErrorMsgFields() { + return !this.conditionError || this.eventFilterFormGroup.get('isError').value !== ''; + } + + cancel() { + this.overlayRef.dispose(); + } +} + diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts index bb62d8d150..1dc663684f 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts @@ -20,7 +20,7 @@ import { EntityTableColumn, EntityTableConfig } from '@home/models/entity/entities-table-config.models'; -import { DebugEventType, Event, EventType } from '@shared/models/event.models'; +import { DebugEventType, Event, EventType, FilterEventBody } from '@shared/models/event.models'; import { TimePageLink } from '@shared/models/page/page-link'; import { TranslateService } from '@ngx-translate/core'; import { DatePipe } from '@angular/common'; @@ -38,16 +38,29 @@ import { EventContentDialogComponent, EventContentDialogData } from '@home/components/event/event-content-dialog.component'; -import { sortObjectKeys } from '@core/utils'; +import { isEqual, sortObjectKeys } from '@core/utils'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ChangeDetectorRef, Injector, StaticProvider, ViewContainerRef } from '@angular/core'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { + EVENT_FILTER_PANEL_DATA, + EventFilterPanelComponent, + EventFilterPanelData, + FilterEntityColumn +} from '@home/components/event/event-filter-panel.component'; export class EventTableConfig extends EntityTableConfig { eventTypeValue: EventType | DebugEventType; + private filterParams: FilterEventBody = {}; + private filterColumns: FilterEntityColumn[] = []; + set eventType(eventType: EventType | DebugEventType) { if (this.eventTypeValue !== eventType) { this.eventTypeValue = eventType; this.updateColumns(true); + this.updateFilterColumns(); } } @@ -66,7 +79,10 @@ export class EventTableConfig extends EntityTableConfig { public tenantId: string, private defaultEventType: EventType | DebugEventType, private disabledEventTypes: Array = null, - private debugEventTypes: Array = null) { + private debugEventTypes: Array = null, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef) { super(); this.loadDataOnInit = false; this.tableTitle = ''; @@ -101,10 +117,20 @@ export class EventTableConfig extends EntityTableConfig { this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; this.updateColumns(); + this.updateFilterColumns(); + + this.headerActionDescriptors.push({ + name: this.translate.instant('event.events-filter'), + icon: 'filter_list', + isEnabled: () => true, + onAction: ($event) => { + this.editEventFilter($event); + } + }); } fetchEvents(pageLink: TimePageLink): Observable> { - return this.eventService.getEvents(this.entityId, this.eventType, this.tenantId, pageLink); + return this.eventService.getFilterEvents(this.entityId, this.eventType, this.tenantId, this.filterParams, pageLink); } updateColumns(updateTableColumns: boolean = false): void { @@ -169,7 +195,7 @@ export class EventTableConfig extends EntityTableConfig { }), false, key => ({ padding: '0 12px 0 0' })), - new EntityTableColumn('entity', 'event.entity', '100px', + new EntityTableColumn('entityName', 'event.entity-type', '100px', (entity) => entity.body.entityName, entity => ({ padding: '0 12px 0 0', }), false, key => ({ @@ -249,4 +275,91 @@ export class EventTableConfig extends EntityTableConfig { } }); } + + private updateFilterColumns() { + this.filterParams = {}; + this.filterColumns = [{key: 'server', title: 'event.server'}]; + switch (this.eventType) { + case EventType.ERROR: + this.filterColumns.push( + {key: 'method', title: 'event.method'}, + {key: 'error', title: 'event.error'} + ); + break; + case EventType.LC_EVENT: + this.filterColumns.push( + {key: 'method', title: 'event.event'}, + {key: 'status', title: 'event.status'}, + {key: 'error', title: 'event.error'} + ); + break; + case EventType.STATS: + this.filterColumns.push( + {key: 'messagesProcessed', title: 'event.min-messages-processed'}, + {key: 'errorsOccurred', title: 'event.min-errors-occurred'} + ); + break; + case DebugEventType.DEBUG_RULE_NODE: + case DebugEventType.DEBUG_RULE_CHAIN: + this.filterColumns.push( + {key: 'msgDirectionType', title: 'event.type'}, + {key: 'entityId', title: 'event.entity-id'}, + {key: 'entityName', title: 'event.entity-type'}, + {key: 'msgType', title: 'event.message-type'}, + {key: 'relationType', title: 'event.relation-type'}, + {key: 'dataSearch', title: 'event.data'}, + {key: 'metadataSearch', title: 'event.metadata'}, + {key: 'isError', title: 'event.error'}, + {key: 'error', title: 'event.error'} + ); + break; + } + } + + private editEventFilter($event: MouseEvent) { + if ($event) { + $event.stopPropagation(); + } + const target = $event.target || $event.srcElement || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const providers: StaticProvider[] = [ + { + provide: EVENT_FILTER_PANEL_DATA, + useValue: { + columns: this.filterColumns, + filterParams: this.filterParams + } as EventFilterPanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + const componentRef = overlayRef.attach(new ComponentPortal(EventFilterPanelComponent, + this.viewContainerRef, injector)); + componentRef.onDestroy(() => { + if (componentRef.instance.result && !isEqual(this.filterParams, componentRef.instance.result.filterParams)) { + this.filterParams = componentRef.instance.result.filterParams; + this.table.updateData(); + } + }); + this.cd.detectChanges(); + } } + diff --git a/ui-ngx/src/app/modules/home/components/event/event-table.component.ts b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts index 6aaa256ab8..00aeb4f50c 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { DatePipe } from '@angular/common'; import { MatDialog } from '@angular/material/dialog'; @@ -24,6 +24,7 @@ import { EventTableConfig } from './event-table-config'; import { EventService } from '@core/http/event.service'; import { DialogService } from '@core/services/dialog.service'; import { DebugEventType, EventType } from '@shared/models/event.models'; +import { Overlay } from '@angular/cdk/overlay'; @Component({ selector: 'tb-event-table', @@ -80,7 +81,10 @@ export class EventTableComponent implements OnInit { private dialogService: DialogService, private translate: TranslateService, private datePipe: DatePipe, - private dialog: MatDialog) { + private dialog: MatDialog, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef) { } ngOnInit() { @@ -95,7 +99,10 @@ export class EventTableComponent implements OnInit { this.tenantId, this.defaultEventType, this.disabledEventTypes, - this.debugEventTypes + this.debugEventTypes, + this.overlay, + this.viewContainerRef, + this.cd ); } diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts index c6c8a5ed51..d5299a52ea 100644 --- a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts @@ -15,11 +15,21 @@ /// import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; import { BooleanFilterPredicate, BooleanOperation, - booleanOperationTranslationMap, EntityKeyValueType, + booleanOperationTranslationMap, + EntityKeyValueType, FilterPredicateType } from '@shared/models/query/query.models'; @@ -32,10 +42,15 @@ import { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => BooleanFilterPredicateComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => BooleanFilterPredicateComponent), + multi: true } ] }) -export class BooleanFilterPredicateComponent implements ControlValueAccessor, OnInit { +export class BooleanFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit { @Input() disabled: boolean; @@ -73,7 +88,7 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On registerOnTouched(fn: any): void { } - setDisabledState?(isDisabled: boolean): void { + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { this.booleanFilterPredicateFormGroup.disable({emitEvent: false}); @@ -82,17 +97,20 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On } } + validate(): ValidationErrors | null { + return this.booleanFilterPredicateFormGroup ? null : { + booleanFilterPredicate: {valid: false} + }; + } + writeValue(predicate: BooleanFilterPredicate): void { this.booleanFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false}); this.booleanFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false}); } private updateModel() { - let predicate: BooleanFilterPredicate = null; - if (this.booleanFilterPredicateFormGroup.valid) { - predicate = this.booleanFilterPredicateFormGroup.getRawValue(); - predicate.type = FilterPredicateType.BOOLEAN; - } + const predicate: BooleanFilterPredicate = this.booleanFilterPredicateFormGroup.getRawValue(); + predicate.type = FilterPredicateType.BOOLEAN; this.propagateChange(predicate); } diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts index 9f4acfc3c6..b177f118c0 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts @@ -16,11 +16,7 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { - ComplexFilterPredicate, - ComplexFilterPredicateInfo, - EntityKeyValueType -} from '@shared/models/query/query.models'; +import { ComplexFilterPredicateInfo, EntityKeyValueType } from '@shared/models/query/query.models'; import { MatDialog } from '@angular/material/dialog'; import { ComplexFilterPredicateDialogComponent, @@ -71,7 +67,7 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On registerOnTouched(fn: any): void { } - setDisabledState?(isDisabled: boolean): void { + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts index 3c16b01319..75c9bd3ce8 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts @@ -21,7 +21,10 @@ import { FormArray, FormBuilder, FormGroup, + NG_VALIDATORS, NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, Validators } from '@angular/forms'; import { Observable, of, Subscription } from 'rxjs'; @@ -49,10 +52,15 @@ import { map } from 'rxjs/operators'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FilterPredicateListComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => FilterPredicateListComponent), + multi: true } ] }) -export class FilterPredicateListComponent implements ControlValueAccessor, OnInit { +export class FilterPredicateListComponent implements ControlValueAccessor, Validator, OnInit { @Input() disabled: boolean; @@ -108,6 +116,12 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni } } + validate(control: AbstractControl): ValidationErrors | null { + return this.filterListFormGroup.valid ? null : { + filterList: {valid: false} + }; + } + writeValue(predicates: Array): void { if (this.valueChangeSubscription) { this.valueChangeSubscription.unsubscribe(); @@ -178,7 +192,7 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni private updateModel() { const predicates: Array = this.filterListFormGroup.getRawValue().predicates; - if (this.filterListFormGroup.valid && predicates.length) { + if (predicates.length) { this.propagateChange(predicates); } else { this.propagateChange(null); diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts index 7417317dae..934a4774fb 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts @@ -19,7 +19,10 @@ import { ControlValueAccessor, FormBuilder, FormGroup, + NG_VALIDATORS, NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, ValidatorFn, Validators } from '@angular/forms'; @@ -39,10 +42,15 @@ import { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FilterPredicateValueComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => FilterPredicateValueComponent), + multi: true } ] }) -export class FilterPredicateValueComponent implements ControlValueAccessor, OnInit { +export class FilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit { private readonly inheritModeForSources: DynamicValueSourceType[] = [ DynamicValueSourceType.CURRENT_CUSTOMER, @@ -62,7 +70,22 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn } } - @Input() onlyUserDynamicSource = false; + private onlyUserDynamicSourceValue = false; + + @Input() + set onlyUserDynamicSource(dynamicMode: boolean) { + this.onlyUserDynamicSourceValue = dynamicMode; + if (this.filterPredicateValueFormGroup) { + this.updateValidationDynamicMode(); + setTimeout(() => { + this.updateModel(); + }, 0); + } + } + + get onlyUserDynamicSource(): boolean { + return this.onlyUserDynamicSourceValue; + } @Input() valueType: EntityKeyValueType; @@ -83,6 +106,7 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn allow = true; private propagateChange = null; + private propagateChangePending = false; constructor(private fb: FormBuilder) { } @@ -126,6 +150,7 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn this.updateShowInheritMode(sourceType); } ); + this.updateValidationDynamicMode(); this.filterPredicateValueFormGroup.valueChanges.subscribe(() => { this.updateModel(); }); @@ -133,12 +158,18 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn registerOnChange(fn: any): void { this.propagateChange = fn; + if (this.propagateChangePending) { + this.propagateChangePending = false; + setTimeout(() => { + this.updateModel(); + }, 0); + } } registerOnTouched(fn: any): void { } - setDisabledState?(isDisabled: boolean): void { + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { this.filterPredicateValueFormGroup.disable({emitEvent: false}); @@ -147,28 +178,35 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn } } + validate(): ValidationErrors | null { + return this.filterPredicateValueFormGroup.valid ? null : { + filterPredicateValue: {valid: false} + }; + } + writeValue(predicateValue: FilterPredicateValue): void { + this.propagateChangePending = false; this.filterPredicateValueFormGroup.get('defaultValue').patchValue(predicateValue.defaultValue, {emitEvent: false}); - this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').patchValue(predicateValue.dynamicValue ? - predicateValue.dynamicValue.sourceType : null, {emitEvent: false}); - this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').patchValue(predicateValue.dynamicValue ? - predicateValue.dynamicValue.sourceAttribute : null, {emitEvent: false}); - this.filterPredicateValueFormGroup.get('dynamicValue.inherit').patchValue(predicateValue.dynamicValue ? - predicateValue.dynamicValue.inherit : false, {emitEvent: false}); + this.filterPredicateValueFormGroup.get('dynamicValue').patchValue({ + sourceType: predicateValue.dynamicValue ? predicateValue.dynamicValue.sourceType : null, + sourceAttribute: predicateValue.dynamicValue ? predicateValue.dynamicValue.sourceAttribute : null, + inherit: predicateValue.dynamicValue ? predicateValue.dynamicValue.inherit : false + }, {emitEvent: this.onlyUserDynamicSource}); this.updateShowInheritMode(predicateValue?.dynamicValue?.sourceType); } private updateModel() { - let predicateValue: FilterPredicateValue = null; - if (this.filterPredicateValueFormGroup.valid) { - predicateValue = this.filterPredicateValueFormGroup.getRawValue(); - if (predicateValue.dynamicValue) { - if (!predicateValue.dynamicValue.sourceType || !predicateValue.dynamicValue.sourceAttribute) { - predicateValue.dynamicValue = null; - } + const predicateValue: FilterPredicateValue = this.filterPredicateValueFormGroup.getRawValue(); + if (predicateValue.dynamicValue) { + if (!predicateValue.dynamicValue.sourceType || !predicateValue.dynamicValue.sourceAttribute) { + predicateValue.dynamicValue = null; } } - this.propagateChange(predicateValue); + if (this.propagateChange) { + this.propagateChange(predicateValue); + } else { + this.propagateChangePending = true; + } } private updateShowInheritMode(sourceType: DynamicValueSourceType) { @@ -179,4 +217,16 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn this.inheritMode = false; } } + + private updateValidationDynamicMode() { + if (this.onlyUserDynamicSource) { + this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').setValidators(Validators.required); + this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').setValidators(Validators.required); + } else { + this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').clearValidators(); + this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').clearValidators(); + } + this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').updateValueAndValidity({emitEvent: false}); + this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').updateValueAndValidity({emitEvent: false}); + } } diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts index 69972b6a90..17361f55bd 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts @@ -15,11 +15,17 @@ /// import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { - EntityKeyValueType, - FilterPredicateType, KeyFilterPredicate, KeyFilterPredicateInfo -} from '@shared/models/query/query.models'; + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { EntityKeyValueType, FilterPredicateType, KeyFilterPredicateInfo } from '@shared/models/query/query.models'; @Component({ selector: 'tb-filter-predicate', @@ -30,10 +36,15 @@ import { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FilterPredicateComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => FilterPredicateComponent), + multi: true } ] }) -export class FilterPredicateComponent implements ControlValueAccessor, OnInit { +export class FilterPredicateComponent implements ControlValueAccessor, Validator, OnInit { @Input() disabled: boolean; @@ -75,7 +86,7 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit { registerOnTouched(fn: any): void { } - setDisabledState?(isDisabled: boolean): void { + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { this.filterPredicateFormGroup.disable({emitEvent: false}); @@ -84,6 +95,12 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit { } } + validate(): ValidationErrors | null { + return this.filterPredicateFormGroup.valid ? null : { + filterPredicate: {valid: false} + }; + } + writeValue(predicate: KeyFilterPredicateInfo): void { this.type = predicate.keyFilterPredicate.type; this.filterPredicateFormGroup.get('predicate').patchValue(predicate.keyFilterPredicate, {emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts index 1229f18ec2..c1d1a1db82 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts @@ -22,7 +22,10 @@ import { FormBuilder, FormControl, FormGroup, + NG_VALIDATORS, NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, Validators } from '@angular/forms'; import { Observable, Subscription } from 'rxjs'; @@ -46,10 +49,15 @@ import { EntityId } from '@shared/models/id/entity-id'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => KeyFilterListComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => KeyFilterListComponent), + multi: true } ] }) -export class KeyFilterListComponent implements ControlValueAccessor, OnInit { +export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit { @Input() disabled: boolean; @@ -104,6 +112,12 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { } } + validate(): ValidationErrors | null { + return this.keyFilterListFormGroup.valid && this.keyFiltersControl.valid ? null : { + keyFilterList: {valid: false} + }; + } + writeValue(keyFilters: Array): void { if (this.valueChangeSubscription) { this.valueChangeSubscription.unsubscribe(); diff --git a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts index 434c90fa80..2b24a5a6b1 100644 --- a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts @@ -15,7 +15,16 @@ /// import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; import { EntityKeyValueType, FilterPredicateType, @@ -33,10 +42,15 @@ import { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NumericFilterPredicateComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => NumericFilterPredicateComponent), + multi: true } ] }) -export class NumericFilterPredicateComponent implements ControlValueAccessor, OnInit { +export class NumericFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit { @Input() disabled: boolean; @@ -76,7 +90,7 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On registerOnTouched(fn: any): void { } - setDisabledState?(isDisabled: boolean): void { + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { this.numericFilterPredicateFormGroup.disable({emitEvent: false}); @@ -85,17 +99,20 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On } } + validate(): ValidationErrors | null { + return this.numericFilterPredicateFormGroup.valid ? null : { + numericFilterPredicate: {valid: false} + }; + } + writeValue(predicate: NumericFilterPredicate): void { this.numericFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false}); this.numericFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false}); } private updateModel() { - let predicate: NumericFilterPredicate = null; - if (this.numericFilterPredicateFormGroup.valid) { - predicate = this.numericFilterPredicateFormGroup.getRawValue(); - predicate.type = FilterPredicateType.NUMERIC; - } + const predicate: NumericFilterPredicate = this.numericFilterPredicateFormGroup.getRawValue(); + predicate.type = FilterPredicateType.NUMERIC; this.propagateChange(predicate); } diff --git a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts index 11b4dad275..3caa282576 100644 --- a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts @@ -15,7 +15,16 @@ /// import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; import { EntityKeyValueType, FilterPredicateType, @@ -33,10 +42,15 @@ import { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StringFilterPredicateComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => StringFilterPredicateComponent), + multi: true } ] }) -export class StringFilterPredicateComponent implements ControlValueAccessor, OnInit { +export class StringFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit { @Input() disabled: boolean; @@ -90,12 +104,15 @@ export class StringFilterPredicateComponent implements ControlValueAccessor, OnI this.stringFilterPredicateFormGroup.get('ignoreCase').patchValue(predicate.ignoreCase, {emitEvent: false}); } + validate(c): ValidationErrors { + return this.stringFilterPredicateFormGroup.valid ? null : { + stringFilterPredicate: {valid: false} + }; + } + private updateModel() { - let predicate: StringFilterPredicate = null; - if (this.stringFilterPredicateFormGroup.valid) { - predicate = this.stringFilterPredicateFormGroup.getRawValue(); - predicate.type = FilterPredicateType.STRING; - } + const predicate: StringFilterPredicate = this.stringFilterPredicateFormGroup.getRawValue(); + predicate.type = FilterPredicateType.STRING; this.propagateChange(predicate); } diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index ab707b6b07..e9d8fb7aa0 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -25,6 +25,7 @@ import { AuditLogDetailsDialogComponent } from '@home/components/audit-log/audit import { AuditLogTableComponent } from '@home/components/audit-log/audit-log-table.component'; import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component'; import { EventTableComponent } from '@home/components/event/event-table.component'; +import { EventFilterPanelComponent } from '@home/components/event/event-filter-panel.component'; import { RelationTableComponent } from '@home/components/relation/relation-table.component'; import { RelationDialogComponent } from '@home/components/relation/relation-dialog.component'; import { AlarmTableHeaderComponent } from '@home/components/alarm/alarm-table-header.component'; @@ -133,6 +134,8 @@ import { ManageDashboardStatesDialogComponent } from '@home/components/dashboard import { DashboardStateDialogComponent } from '@home/components/dashboard-page/states/dashboard-state-dialog.component'; import { EmbedDashboardDialogComponent } from '@home/components/widget/dialog/embed-dashboard-dialog.component'; import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token'; +import { EdgeDownlinkTableComponent } from '@home/components/edge/edge-downlink-table.component'; +import { EdgeDownlinkTableHeaderComponent } from '@home/components/edge/edge-downlink-table-header.component'; import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-page/widget-types-panel.component'; import { FirmwareAutocompleteComponent } from '@home/components/firmware/firmware-autocomplete.component'; @@ -148,6 +151,9 @@ import { FirmwareAutocompleteComponent } from '@home/components/firmware/firmwar EventContentDialogComponent, EventTableHeaderComponent, EventTableComponent, + EventFilterPanelComponent, + EdgeDownlinkTableHeaderComponent, + EdgeDownlinkTableComponent, RelationTableComponent, RelationDialogComponent, RelationFiltersComponent, @@ -265,6 +271,8 @@ import { FirmwareAutocompleteComponent } from '@home/components/firmware/firmwar EntityDetailsPanelComponent, AuditLogTableComponent, EventTableComponent, + EdgeDownlinkTableHeaderComponent, + EdgeDownlinkTableComponent, RelationTableComponent, RelationFiltersComponent, AlarmTableComponent, diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.ts b/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.ts index 237ec28695..f878d67dee 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.ts +++ b/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.ts @@ -32,7 +32,7 @@ import { CsvToJsonResult, ImportEntityColumnType } from '@home/components/import-export/import-export.models'; -import { ImportEntitiesResultInfo, ImportEntityData } from '@app/shared/models/entity.models'; +import { EdgeImportEntityData, ImportEntitiesResultInfo, ImportEntityData } from '@app/shared/models/entity.models'; import { ImportExportService } from '@home/components/import-export/import-export.service'; export interface ImportDialogCsvData { @@ -186,25 +186,14 @@ export class ImportDialogCsvComponent extends DialogComponent[] = []; for (let i = 0; i < partSize; i++) { - const importEntityPromise = - this.entityService.saveEntityParameters(entityType, entitiesData[i], updateData, config).pipe( + let saveEntityPromise: Observable; + saveEntityPromise = this.entityService.saveEntityParameters(entityType, entitiesData[i], updateData, config); + const importEntityPromise = saveEntityPromise.pipe( tap((res) => { if (importEntityCompleted) { importEntityCompleted(); @@ -400,7 +401,7 @@ export class ImportExportService { ); } - public importRuleChain(): Observable { + public importRuleChain(expectedRuleChainType: RuleChainType): Observable { return this.openImportDialog('rulechain.import', 'rulechain.rulechain-file').pipe( mergeMap((ruleChainImport: RuleChainImport) => { if (!this.validateImportedRuleChain(ruleChainImport)) { @@ -408,6 +409,11 @@ export class ImportExportService { {message: this.translate.instant('rulechain.invalid-rulechain-file-error'), type: 'error'})); throw new Error('Invalid rule chain file'); + } else if (ruleChainImport.ruleChain.type !== expectedRuleChainType) { + this.store.dispatch(new ActionNotificationShow( + {message: this.translate.instant('rulechain.invalid-rulechain-type-error', {expectedRuleChainType}), + type: 'error'})); + throw new Error('Invalid rule chain type'); } else { return this.ruleChainService.resolveRuleChainMetadata(ruleChainImport.metadata).pipe( map((resolvedMetadata) => { @@ -494,6 +500,9 @@ export class ImportExportService { || isUndefined(ruleChainImport.ruleChain.name)) { return false; } + if (isUndefined(ruleChainImport.ruleChain.type)) { + ruleChainImport.ruleChain.type = RuleChainType.CORE; + } return true; } @@ -636,6 +645,9 @@ export class ImportExportService { private editMissingAliases(widgets: Array, isSingleWidget: boolean, customTitle: string, missingEntityAliases: EntityAliases): Observable { + const allowedEntityTypes: Array = + this.entityService.prepareAllowedEntityTypesList(null, true); + return this.dialog.open(EntityAliasesDialogComponent, { disableClose: true, @@ -645,7 +657,8 @@ export class ImportExportService { widgets, customTitle, isSingleWidget, - disableAdd: true + disableAdd: true, + allowedEntityTypes } }).afterClosed().pipe( map((updatedEntityAliases) => { diff --git a/ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts b/ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts index 157259fbf3..6f0bd04fcc 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts +++ b/ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts @@ -93,6 +93,14 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce { value: ImportEntityColumnType.timeseries } ); break; + case EntityType.EDGE: + this.columnTypes.push( + { value: ImportEntityColumnType.edgeLicenseKey }, + { value: ImportEntityColumnType.cloudEndpoint }, + { value: ImportEntityColumnType.routingKey }, + { value: ImportEntityColumnType.secret } + ); + break; } } @@ -118,6 +126,10 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce const isSelectCredentials = this.columns.findIndex((column) => column.type === ImportEntityColumnType.accessToken) > -1; const isSelectGateway = this.columns.findIndex((column) => column.type === ImportEntityColumnType.isGateway) > -1; const isSelectDescription = this.columns.findIndex((column) => column.type === ImportEntityColumnType.description) > -1; + const isSelectEdgeLicenseKey = this.columns.findIndex((column) => column.type === ImportEntityColumnType.edgeLicenseKey) > -1; + const isSelectCloudEndpoint = this.columns.findIndex((column) => column.type === ImportEntityColumnType.cloudEndpoint) > -1; + const isSelectRoutingKey = this.columns.findIndex((column) => column.type === ImportEntityColumnType.routingKey) > -1; + const isSelectSecret = this.columns.findIndex((column) => column.type === ImportEntityColumnType.secret) > -1; const hasInvalidColumn = this.columns.findIndex((column) => !this.columnValid(column)) > -1; this.valid = isSelectName && isSelectType && !hasInvalidColumn; @@ -135,6 +147,22 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce if (accessTokenColumnType) { accessTokenColumnType.disabled = isSelectCredentials; } + const edgeLicenseKeyColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.edgeLicenseKey); + if (edgeLicenseKeyColumnType) { + edgeLicenseKeyColumnType.disabled = isSelectEdgeLicenseKey; + } + const cloudEndpointColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.cloudEndpoint); + if (cloudEndpointColumnType) { + cloudEndpointColumnType.disabled = isSelectCloudEndpoint; + } + const routingKeyColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.routingKey); + if (routingKeyColumnType) { + routingKeyColumnType.disabled = isSelectRoutingKey; + } + const secretColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.secret); + if (secretColumnType) { + secretColumnType.disabled = isSelectSecret; + } if (this.propagateChange) { this.propagateChange(this.columns); } else { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html index e04ecfb258..320373e52c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html @@ -61,6 +61,21 @@ {{ 'device-profile.attributes-proto-schema-required' | translate}} + + device-profile.rpc-request-proto-schema + + + {{ 'device-profile.rpc-request-proto-schema-required' | translate}} + + device-profile.rpc-request-proto-schema-hint + + + device-profile.rpc-response-proto-schema + + + {{ 'device-profile.rpc-response-proto-schema-required' | translate}} + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts index 4a686d39bd..dc1da3d973 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts @@ -24,6 +24,8 @@ import { coapDeviceTypeTranslationMap, CoapTransportDeviceType, defaultAttributesSchema, + defaultRpcRequestSchema, + defaultRpcResponseSchema, defaultTelemetrySchema, DeviceProfileTransportConfiguration, DeviceTransportType, @@ -59,7 +61,9 @@ export class CoapDeviceProfileTransportConfigurationComponent implements Control private transportPayloadTypeConfiguration = this.fb.group({ transportPayloadType: [TransportPayloadType.JSON, Validators.required], deviceTelemetryProtoSchema: [defaultTelemetrySchema, Validators.required], - deviceAttributesProtoSchema: [defaultAttributesSchema, Validators.required] + deviceAttributesProtoSchema: [defaultAttributesSchema, Validators.required], + deviceRpcRequestProtoSchema: [defaultRpcRequestSchema, Validators.required], + deviceRpcResponseProtoSchema: [defaultRpcResponseSchema, Validators.required] }); get required(): boolean { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.html new file mode 100644 index 0000000000..5599e8d116 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.html @@ -0,0 +1,53 @@ + +
+ +
+

{{ (readonly ? 'device-profile.lwm2m.attribute-lwm2m-toolbar-view' : + 'device-profile.lwm2m.attribute-lwm2m-toolbar-edit') | translate }}

+
+ + +
+ + +
+ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.ts new file mode 100644 index 0000000000..834be7535c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.ts @@ -0,0 +1,90 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import {Component, Inject, OnInit, SkipSelf} from "@angular/core"; +import {ErrorStateMatcher} from "@angular/material/core"; +import {DialogComponent} from "@shared/components/dialog.component"; +import {Store} from "@ngrx/store"; +import {AppState} from "@core/core.state"; +import {Router} from "@angular/router"; +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; +import {FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm} from "@angular/forms"; +import {TranslateService} from "@ngx-translate/core"; +import {JsonObject} from "@angular/compiler-cli/ngcc/src/packages/entry_point"; + +export interface Lwm2mAttributesDialogData { + readonly: boolean; + attributeLwm2m: JsonObject; + destName: string +} + +@Component({ + selector: 'tb-lwm2m-attributes-dialog', + templateUrl: './lwm2m-attributes-dialog.component.html', + styleUrls: ['./lwm2m-attributes.component.scss'], + providers: [{provide: ErrorStateMatcher, useExisting: Lwm2mAttributesDialogComponent}], +}) +export class Lwm2mAttributesDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { + + readonly = this.data.readonly; + + attributeLwm2m = this.data.attributeLwm2m; + + submitted = false; + + dirtyValue = false; + + attributeLwm2mDialogFormGroup: FormGroup; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: Lwm2mAttributesDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + public translate: TranslateService) { + super(store, router, dialogRef); + + this.attributeLwm2mDialogFormGroup = this.fb.group({ + keyFilters: [{}, []] + }); + this.attributeLwm2mDialogFormGroup.patchValue({keyFilters: this.attributeLwm2m}); + this.attributeLwm2mDialogFormGroup.get('keyFilters').valueChanges.subscribe((attributes) => { + this.attributeLwm2m = attributes; + }); + if (this.readonly) { + this.attributeLwm2mDialogFormGroup.disable({emitEvent: false}); + } + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + save(): void { + this.submitted = true; + this.dialogRef.close(this.attributeLwm2m); + } + + cancel(): void { + this.dialogRef.close(null); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html new file mode 100644 index 0000000000..e0a53c49c4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.html @@ -0,0 +1,83 @@ + +
+
+ device-profile.lwm2m.attribute-lwm2m-destination + {{ titleText }} +
+
+ device-profile.lwm2m.attribute-lwm2m-name + device-profile.lwm2m.attribute-lwm2m-value +
+
+
+
+ + + + + {{ attributeLwm2mMap.get(attrKey[attributeLwm2m]) }} + + + + + + + + +
+ + {{ 'device-profile.lwm2m.key-name' | translate }} + {{ 'device-profile.lwm2m.required' | translate }} + + + {{ 'device-profile.lwm2m.valid-attribute-lwm2m-key' | translate: {attrEnums: attrKeys} }} + + + {{ 'device-profile.lwm2m.valid-attribute-lwm2m-value' | translate: {attrEnums: attrKeys} }} + +
+ {{noDataText ? noDataText : 'device-profile.lwm2m.no-data'}} +
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.ts new file mode 100644 index 0000000000..7d7af196f0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component.ts @@ -0,0 +1,227 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import {Component, forwardRef, Input, OnInit} from "@angular/core"; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from "@angular/forms"; +import {Subscription} from "rxjs"; +import {PageComponent} from "@shared/components/page.component"; +import {Store} from "@ngrx/store"; +import {AppState} from "@core/core.state"; +import { + ATTRIBUTE_KEYS, + ATTRIBUTE_LWM2M_ENUM, + ATTRIBUTE_LWM2M_MAP +} from "@home/components/profile/device/lwm2m/lwm2m-profile-config.models"; +import {isDefinedAndNotNull, isEmpty, isEmptyStr, isUndefinedOrNull} from "@core/utils"; + + +@Component({ + selector: 'tb-lwm2m-attributes-key-list', + templateUrl: './lwm2m-attributes-key-list.component.html', + styleUrls: ['./lwm2m-attributes.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mAttributesKeyListComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => Lwm2mAttributesKeyListComponent), + multi: true, + } + ] +}) +export class Lwm2mAttributesKeyListComponent extends PageComponent implements ControlValueAccessor, OnInit, Validator { + + attrKeys = ATTRIBUTE_KEYS; + + attrKey = ATTRIBUTE_LWM2M_ENUM; + + attributeLwm2mMap = ATTRIBUTE_LWM2M_MAP; + + @Input() disabled: boolean; + + @Input() titleText: string; + + @Input() noDataText: string; + + kvListFormGroup: FormGroup; + + private propagateChange = null; + + private valueChangeSubscription: Subscription = null; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.kvListFormGroup = this.fb.group({}); + this.kvListFormGroup.addControl('keyVals', + this.fb.array([])); + } + + keyValsFormArray(): FormArray { + return this.kvListFormGroup.get('keyVals') as FormArray; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.kvListFormGroup.disable({emitEvent: false}); + } else { + this.kvListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(keyValMap: { [key: string]: string }): void { + if (this.valueChangeSubscription) { + this.valueChangeSubscription.unsubscribe(); + } + const keyValsControls: Array = []; + if (keyValMap) { + for (const property of Object.keys(keyValMap)) { + if (Object.prototype.hasOwnProperty.call(keyValMap, property)) { + keyValsControls.push(this.fb.group({ + key: [property, [Validators.required, this.attributeLwm2mKeyValidator]], + value: [keyValMap[property], this.attributeLwm2mValueValidator(property)] + })); + } + } + } + this.kvListFormGroup.setControl('keyVals', this.fb.array(keyValsControls)); + this.valueChangeSubscription = this.kvListFormGroup.valueChanges.subscribe(() => { + // this.updateValidate(); + this.updateModel(); + }); + if (this.disabled) { + this.kvListFormGroup.disable({emitEvent: false}); + } else { + this.kvListFormGroup.enable({emitEvent: false}); + } + } + + public removeKeyVal(index: number) { + (this.kvListFormGroup.get('keyVals') as FormArray).removeAt(index); + } + + public addKeyVal() { + const keyValsFormArray = this.kvListFormGroup.get('keyVals') as FormArray; + keyValsFormArray.push(this.fb.group({ + key: ['', [Validators.required, this.attributeLwm2mKeyValidator]], + value: ['', []] + })); + } + + public validate(c?: FormControl) { + const kvList: { key: string; value: string }[] = this.kvListFormGroup.get('keyVals').value; + let valid = true; + for (const entry of kvList) { + if (isUndefinedOrNull(entry.key) || isEmptyStr(entry.key) || !ATTRIBUTE_KEYS.includes(entry.key)) { + valid = false; + break; + } + if (entry.key !== 'ver' && isNaN(Number(entry.value))) { + valid = false; + break; + } + } + return (valid) ? null : { + keyVals: { + valid: false, + }, + }; + } + + private updateValidate (c?: FormControl) { + const kvList = this.kvListFormGroup.get('keyVals') as FormArray; + kvList.controls.forEach(fg => { + if (fg.get('key').value==='ver') { + fg.get('value').setValidators(null); + fg.get('value').setErrors(null); + } + else { + fg.get('value').setValidators(this.attributeLwm2mValueNumberValidator); + fg.get('value').setErrors(this.attributeLwm2mValueNumberValidator(fg.get('value'))); + } + }); + } + + private updateModel() { + this.updateValidate(); + if (this.validate() === null) { + const kvList: { key: string; value: string }[] = this.kvListFormGroup.get('keyVals').value; + const keyValMap: { [key: string]: string | number } = {}; + kvList.forEach((entry) => { + if (isUndefinedOrNull(entry.value) || entry.key === 'ver' || isEmptyStr(entry.value.toString())) { + keyValMap[entry.key] = entry.value.toString(); + } else { + keyValMap[entry.key] = Number(entry.value) + } + }); + this.propagateChange(keyValMap); + } + else { + this.propagateChange(null); + } + } + + + private attributeLwm2mKeyValidator = (control: AbstractControl) => { + const key = control.value as string; + if (isDefinedAndNotNull(key) && !isEmpty(key)) { + if (!ATTRIBUTE_KEYS.includes(key)) { + return { + validAttributeKey: true + }; + } + } + return null; + } + + private attributeLwm2mValueNumberValidator = (control: AbstractControl) => { + if (isNaN(Number(control.value)) || Number(control.value) < 0) { + return { + 'validAttributeValue': true + }; + } + return null; + } + + private attributeLwm2mValueValidator = (property: string): Object [] => { + return property === 'ver'? [] : [this.attributeLwm2mValueNumberValidator]; + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.html new file mode 100644 index 0000000000..2011cd65f5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.html @@ -0,0 +1,32 @@ + +
+
+ {{attributeLwm2mToString()}} +
+ +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.scss b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.scss new file mode 100644 index 0000000000..63dd0bdb3e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.scss @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-kv-map { + span.no-data-found { + position: relative; + display: flex; + height: 40px; + + &.disabled { + color: rgba(0, 0, 0, .38); + } + } + } +} + +:host ::ng-deep { + .mat-form-field-wrapper { + padding-bottom: 0; + } + .mat-form-field-infix { + border-top: 0; + } + .mat-form-field-underline { + bottom: 0; + } +} + +.vertical-padding { + padding: 0 0 10px 20px; +} + +.resource-name-lw-end{ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align:end; + //width: 80px; + cursor: pointer; +} + +.resource-name-lw{ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; +} + diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.ts new file mode 100644 index 0000000000..ca4b66ae36 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes.component.ts @@ -0,0 +1,159 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import {Component, EventEmitter, forwardRef, Inject, Input, Output} from "@angular/core"; +import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR} from "@angular/forms"; +import {coerceBooleanProperty} from "@angular/cdk/coercion"; +import {Store} from "@ngrx/store"; +import {AppState} from "@core/core.state"; +import {DeviceProfileService} from "@core/http/device-profile.service"; +import {WINDOW} from "@core/services/window.service"; +import {deepClone, isDefinedAndNotNull, isEmpty} from "@core/utils"; +import { + Lwm2mAttributesDialogComponent, + Lwm2mAttributesDialogData +} from "@home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component"; +import {MatDialog} from "@angular/material/dialog"; +import {TranslateService} from "@ngx-translate/core"; +import {ATTRIBUTE_LWM2M_LABEL} from "@home/components/profile/device/lwm2m/lwm2m-profile-config.models"; + + +@Component({ + selector: 'tb-profile-lwm2m-attributes', + templateUrl: './lwm2m-attributes.component.html', + styleUrls: ['./lwm2m-attributes.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mAttributesComponent), + multi: true + }] +}) +export class Lwm2mAttributesComponent implements ControlValueAccessor { + attributeLwm2mFormGroup: FormGroup; + attributeLwm2mLabel = ATTRIBUTE_LWM2M_LABEL; + + private requiredValue: boolean; + private dirty = false; + + @Input() + attributeLwm2m: {}; + + @Input() + isAttributeTelemetry: boolean; + + @Input() + destName: string; + + @Input() + disabled: boolean; + + @Output() + updateAttributeLwm2m = new EventEmitter(); + + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + private propagateChange = (v: any) => { + } + + constructor(private store: Store, + private dialog: MatDialog, + private fb: FormBuilder, + private deviceProfileService: DeviceProfileService, + private translate: TranslateService, + @Inject(WINDOW) private window: Window) {} + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.attributeLwm2mFormGroup.disable({emitEvent: false}); + } else { + this.attributeLwm2mFormGroup.enable({emitEvent: false}); + } + } + + ngOnInit() { + this.attributeLwm2mFormGroup = this.fb.group({ + attributeLwm2m: [this.attributeLwm2m] + }); + } + + writeValue(value: {} | null): void {} + + attributeLwm2mToString = (): string => { + return this.isIconEditAdd () ? this.attributeLwm2mLabelToString() : this.translate.instant('device-profile.lwm2m.no-data'); + } + + private attributeLwm2mLabelToString = (): string => { + let label = JSON.stringify(this.attributeLwm2m); + label = deepClone(label.replace('{', '')); + label = deepClone(label.replace('}', '')); + this.attributeLwm2mLabel.forEach((value: string, key: string) => { + const dest = '\"' + key + '\"\:'; + label = deepClone(label.replace(dest, value)); + }); + return label; + } + + isDisableBtn (): boolean { + return this.disabled || this.isAttributeTelemetry ? !(isDefinedAndNotNull(this.attributeLwm2m) && + !isEmpty(this.attributeLwm2m) && this.disabled) : this.disabled; + } + + isIconView (): boolean { + return this.isAttributeTelemetry || this.disabled; + } + + isIconEditAdd (): boolean { + return isDefinedAndNotNull(this.attributeLwm2m) && !isEmpty(this.attributeLwm2m); + } + + isToolTipLabel (): string { + return this.disabled ? this.translate.instant('device-profile.lwm2m.attribute-lwm2m-tip') : + this.isAttributeTelemetry ? this.translate.instant('device-profile.lwm2m.attribute-lwm2m-disable-tip') : + this.translate.instant('device-profile.lwm2m.attribute-lwm2m-tip'); + } + + public editAttributesLwm2m = ($event: Event): void => { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(Lwm2mAttributesDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + readonly: this.disabled, + attributeLwm2m: this.disabled ? this.attributeLwm2m : deepClone(this.attributeLwm2m), + destName: this.destName + } + }).afterClosed().subscribe((result) => { + if (result) { + this.attributeLwm2m = result; + this.attributeLwm2mFormGroup.patchValue({attributeLwm2m: this.attributeLwm2m}); + this.updateAttributeLwm2m.next(this.attributeLwm2m); + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts index 0adc882f0f..b82da4bc06 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts @@ -14,8 +14,8 @@ /// limitations under the License. /// -import { Component, forwardRef, Inject, Input } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import {Component, forwardRef, Inject, Input} from '@angular/core'; +import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators} from '@angular/forms'; import { DEFAULT_CLIENT_HOLD_OFF_TIME, DEFAULT_ID_SERVER, @@ -27,13 +27,13 @@ import { SECURITY_CONFIG_MODE, SECURITY_CONFIG_MODE_NAMES, ServerSecurityConfig -} from './profile-config.models'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { WINDOW } from '@core/services/window.service'; -import { pairwise, startWith } from 'rxjs/operators'; -import { DeviceProfileService } from '@core/http/device-profile.service'; +} from './lwm2m-profile-config.models'; +import {Store} from '@ngrx/store'; +import {AppState} from '@core/core.state'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import {WINDOW} from '@core/services/window.service'; +import {pairwise, startWith} from 'rxjs/operators'; +import {DeviceProfileService} from '@core/http/device-profile.service'; @Component({ selector: 'tb-profile-lwm2m-device-config-server', diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html index 84797bcb98..5363d05d74 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html @@ -82,8 +82,6 @@ {{ 'device-profile.lwm2m.required' | translate }} -
-
{{ 'device-profile.lwm2m.default-min-period' | translate }} @@ -93,13 +91,16 @@ {{ 'device-profile.lwm2m.required' | translate }} - +
+
+ {{ 'device-profile.lwm2m.binding' | translate }} - - - {{ 'device-profile.lwm2m.binding' | translate }} - {{ 'device-profile.lwm2m.required' | translate }} - + + + {{ bindingModeTypeNamesMap.get(bindingModeType[bindingMode]) }} + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts index 82beff220f..d6f6823db6 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts @@ -14,30 +14,32 @@ /// limitations under the License. /// -import {DeviceProfileTransportConfiguration, DeviceTransportType} from '@shared/models/device.models'; -import {Component, forwardRef, Inject, Input} from '@angular/core'; -import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators} from '@angular/forms'; -import {Store} from '@ngrx/store'; -import {AppState} from '@app/core/core.state'; -import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import { DeviceProfileTransportConfiguration } from '@shared/models/device.models'; +import { Component, forwardRef, Inject, Input } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ATTRIBUTE, + BINDING_MODE, + BINDING_MODE_NAMES, getDefaultProfileConfig, INSTANCES, KEY_NAME, + Lwm2mProfileConfigModels, ModelValue, ObjectLwM2M, OBSERVE, OBSERVE_ATTR_TELEMETRY, - ProfileConfigModels, RESOURCES, TELEMETRY -} from './profile-config.models'; -import {DeviceProfileService} from '@core/http/device-profile.service'; -import {deepClone, isDefinedAndNotNull, isUndefined} from '@core/utils'; -import {WINDOW} from '@core/services/window.service'; -import {JsonArray, JsonObject} from '@angular/compiler-cli/ngcc/src/packages/entry_point'; -import {Direction} from '@shared/models/page/sort-order'; +} from './lwm2m-profile-config.models'; +import { DeviceProfileService } from '@core/http/device-profile.service'; +import { deepClone, isDefinedAndNotNull, isEmpty, isUndefined } from '@core/utils'; +import { WINDOW } from '@core/services/window.service'; +import { JsonArray, JsonObject } from '@angular/compiler-cli/ngcc/src/packages/entry_point'; +import { Direction } from '@shared/models/page/sort-order'; @Component({ selector: 'tb-profile-lwm2m-device-transport-configuration', @@ -50,10 +52,13 @@ import {Direction} from '@shared/models/page/sort-order'; }) export class Lwm2mDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, Validators { - private configurationValue: ProfileConfigModels; + private configurationValue: Lwm2mProfileConfigModels; private requiredValue: boolean; private disabled = false; + bindingModeType = BINDING_MODE; + bindingModeTypes = Object.keys(BINDING_MODE); + bindingModeTypeNamesMap = BINDING_MODE_NAMES; lwm2mDeviceProfileFormGroup: FormGroup; lwm2mDeviceConfigFormGroup: FormGroup; bootstrapServers: string; @@ -70,7 +75,8 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro this.requiredValue = coerceBooleanProperty(value); } - private propagateChange = (v: any) => { } + private propagateChange = (v: any) => { + } constructor(private store: Store, private fb: FormBuilder, @@ -84,7 +90,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro lifetime: [null, Validators.required], defaultMinPeriod: [null, Validators.required], notifIfDisabled: [true, []], - binding: ['U', Validators.required], + binding:[], bootstrapServer: [null, Validators.required], lwm2mServer: [null, Validators.required], }); @@ -118,7 +124,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro } } - writeValue(value: ProfileConfigModels | null): void { + writeValue(value: Lwm2mProfileConfigModels | null): void { this.configurationValue = (Object.keys(value).length === 0) ? getDefaultProfileConfig() : value; this.lwm2mDeviceConfigFormGroup.patchValue({ configurationJson: this.configurationValue @@ -165,7 +171,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro let configuration: DeviceProfileTransportConfiguration = null; if (this.lwm2mDeviceConfigFormGroup.valid && this.lwm2mDeviceProfileFormGroup.valid) { configuration = this.lwm2mDeviceConfigFormGroup.value.configurationJson; - configuration.type = DeviceTransportType.LWM2M; + // configuration.type = DeviceTransportType.LWM2M; } this.propagateChange(configuration); } @@ -198,15 +204,15 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro private getObserveAttrTelemetryObjects = (objectList: ObjectLwM2M[]): object => { const objectLwM2MS = deepClone(objectList); if (this.configurationValue.observeAttr && objectLwM2MS.length > 0) { - const observeArray = this.configurationValue.observeAttr.observe; const attributeArray = this.configurationValue.observeAttr.attribute; const telemetryArray = this.configurationValue.observeAttr.telemetry; let keyNameJson = this.configurationValue.observeAttr.keyName; if (this.includesNotZeroInstance(attributeArray, telemetryArray)) { this.addInstances(attributeArray, telemetryArray, objectLwM2MS); } - if (isDefinedAndNotNull(observeArray) && observeArray.length > 0) { - this.updateObserveAttrTelemetryObjects(observeArray, objectLwM2MS, OBSERVE); + if (isDefinedAndNotNull(this.configurationValue.observeAttr.observe) && + this.configurationValue.observeAttr.observe.length > 0) { + this.updateObserveAttrTelemetryObjects(this.configurationValue.observeAttr.observe, objectLwM2MS, OBSERVE); } if (isDefinedAndNotNull(attributeArray) && attributeArray.length > 0) { this.updateObserveAttrTelemetryObjects(attributeArray, objectLwM2MS, ATTRIBUTE); @@ -214,6 +220,9 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro if (isDefinedAndNotNull(telemetryArray) && telemetryArray.length > 0) { this.updateObserveAttrTelemetryObjects(telemetryArray, objectLwM2MS, TELEMETRY); } + if (isDefinedAndNotNull(this.configurationValue.observeAttr.attributeLwm2m)) { + this.updateAttributeLwm2m(objectLwM2MS); + } if (isDefinedAndNotNull(keyNameJson)) { this.configurationValue.observeAttr.keyName = this.validateKeyNameObjects(keyNameJson, attributeArray, telemetryArray); this.upDateJsonAllConfig(); @@ -256,45 +265,72 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro [nameParameter] = true; } }); + } + private updateAttributeLwm2m = (objectLwM2MS: ObjectLwM2M[]): void => { + Object.keys(this.configurationValue.observeAttr.attributeLwm2m).forEach(key => { + const [objectKeyId, instanceId, resourceId] = Array.from(key.substring(1).split('/'), String); + let objectLwM2M = objectLwM2MS.find(objectLwm2m => objectLwm2m.keyId === objectKeyId); + if (objectLwM2M && instanceId) { + let instance = objectLwM2M.instances.find(instance => instance.id === +instanceId) + if (instance && resourceId) { + instance.resources.find(resource => resource.id === +resourceId) + .attributeLwm2m = this.configurationValue.observeAttr.attributeLwm2m[key]; + } else if (instance) { + instance.attributeLwm2m = this.configurationValue.observeAttr.attributeLwm2m[key]; + } + } else if (objectLwM2M) { + objectLwM2M.attributeLwm2m = this.configurationValue.observeAttr.attributeLwm2m[key]; + } + }); } - private updateKeyNameObjects = (clientObserveAttrTelemetry: ObjectLwM2M[]): void => { + private updateKeyNameObjects = (objectLwM2MS: ObjectLwM2M[]): void => { Object.keys(this.configurationValue.observeAttr.keyName).forEach(key => { const [objectKeyId, instanceId, resourceId] = Array.from(key.substring(1).split('/'), String); - const objectLwM2M = clientObserveAttrTelemetry.find(objectLwm2m => objectLwm2m.keyId === objectKeyId) + const objectLwM2M = objectLwM2MS.find(objectLwm2m => objectLwm2m.keyId === objectKeyId) if (objectLwM2M) { objectLwM2M.instances.find(instance => instance.id === +instanceId) - .resources.find(resource => resource.id === +resourceId) - .keyName = this.configurationValue.observeAttr.keyName[key]; - }}); + .resources.find(resource => resource.id === +resourceId) + .keyName = this.configurationValue.observeAttr.keyName[key]; + } + }); } private validateKeyNameObjects = (nameJson: JsonObject, attributeArray: JsonArray, telemetryArray: JsonArray): {} => { const keyName = JSON.parse(JSON.stringify(nameJson)); let keyNameValidate = {}; - const keyAttrTelemetry = attributeArray.concat(telemetryArray) ; + const keyAttrTelemetry = attributeArray.concat(telemetryArray); Object.keys(keyName).forEach(key => { if (keyAttrTelemetry.includes(key)) { keyNameValidate[key] = keyName[key]; } }); - return keyNameValidate; + return keyNameValidate; } private updateObserveAttrTelemetryFromGroupToJson = (val: ObjectLwM2M[]): void => { const observeArray: Array = []; const attributeArray: Array = []; const telemetryArray: Array = []; + const attributeLwm2m: any = {}; const keyNameNew = {}; const observeJson: ObjectLwM2M[] = JSON.parse(JSON.stringify(val)); observeJson.forEach(obj => { + if (isDefinedAndNotNull(obj.attributeLwm2m) && !isEmpty(obj.attributeLwm2m)) { + const pathObject = `/${obj.keyId}`; + attributeLwm2m[pathObject] = obj.attributeLwm2m; + } if (obj.hasOwnProperty(INSTANCES) && Array.isArray(obj.instances)) { obj.instances.forEach(instance => { + if (isDefinedAndNotNull(instance.attributeLwm2m) && !isEmpty(instance.attributeLwm2m)) { + const pathInstance = `/${obj.keyId}/${instance.id}`; + attributeLwm2m[pathInstance] = instance.attributeLwm2m; + } if (instance.hasOwnProperty(RESOURCES) && Array.isArray(instance.resources)) { instance.resources.forEach(resource => { if (resource.attribute || resource.telemetry) { - let pathRes = `/${obj.keyId}/${instance.id}/${resource.id}`; + const pathRes = `/${obj.keyId}/${instance.id}/${resource.id}`; if (resource.observe) { observeArray.push(pathRes); } @@ -305,6 +341,9 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro telemetryArray.push(pathRes); } keyNameNew[pathRes] = resource.keyName; + if (isDefinedAndNotNull(resource.attributeLwm2m) && !isEmpty(resource.attributeLwm2m)) { + attributeLwm2m[pathRes] = resource.attributeLwm2m; + } } }) } @@ -316,13 +355,15 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro observe: observeArray, attribute: attributeArray, telemetry: telemetryArray, - keyName: this.sortObjectKeyPathJson(KEY_NAME, keyNameNew) + keyName: this.sortObjectKeyPathJson(KEY_NAME, keyNameNew), + attributeLwm2m: attributeLwm2m }; } else { this.configurationValue.observeAttr.observe = observeArray; this.configurationValue.observeAttr.attribute = attributeArray; this.configurationValue.observeAttr.telemetry = telemetryArray; this.configurationValue.observeAttr.keyName = this.sortObjectKeyPathJson(KEY_NAME, keyNameNew); + this.configurationValue.observeAttr.attributeLwm2m = attributeLwm2m; } } @@ -392,6 +433,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro this.removeObserveAttrTelemetryFromJson(TELEMETRY, value.keyId); this.removeObserveAttrTelemetryFromJson(ATTRIBUTE, value.keyId); this.removeKeyNameFromJson(value.keyId); + this.removeAttributeLwm2mFromJson(value.keyId); this.updateObserveAttrTelemetryObjectFormGroup(objectsOld); this.upDateJsonAllConfig(); } @@ -413,4 +455,14 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro } }); } + + private removeAttributeLwm2mFromJson = (keyId: string): void => { + debugger + const keyNameJson = this.configurationValue.observeAttr.attributeLwm2m; + Object.keys(keyNameJson).forEach(key => { + if (key.startsWith(`/${keyId}`)) { + delete keyNameJson[key]; + } + }); + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html similarity index 95% rename from ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.html rename to ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html index b37abd2c79..50f49a8ceb 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component.html @@ -17,7 +17,7 @@ -->
- {{data.objectName}}    (object [{{data.objectKeyId}}]) + {{data.objectName}} (object <{{data.objectKeyId}}>)
-
- Instance [{{instances.get('id').value}}] +
+ {{'device-profile.lwm2m.instance-label' | translate}} <{{instances.get('id').value}}>
-
- - -
-
+
-
+
+ + +
+
+
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.css b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.scss similarity index 66% rename from ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.css rename to ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.scss index 7dc73ee405..2de6fe17d6 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.css +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.scss @@ -13,14 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -.vertical-padding { - padding: 0 0 10px 20px; -} - -.left-padding { - padding-left: 5px; -} - .tb-panel-title-height { user-select: none; min-height: 32px; @@ -28,21 +20,13 @@ .checkbox-padding { padding-left: 22px; -} - -.label-resource { - /*padding: 4px;*/ - color: #002699; - /*text-transform: uppercase;*/ - background: rgba(220, 220, 220, .35); - border-radius: 20px; - width: inherit; - /*margin: 10px 0;*/ - overflow: hidden; - font-size: 15px; - text-overflow: ellipsis; - white-space: nowrap; - opacity: .8; text-align:center; } +:host{ + .instance-list { + mat-expansion-panel-header { + color: inherit; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts index 814876dab8..c38c413e98 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts @@ -14,7 +14,6 @@ /// limitations under the License. /// - import {Component, forwardRef, Input} from '@angular/core'; import { AbstractControl, @@ -28,19 +27,29 @@ import { import {Store} from '@ngrx/store'; import {AppState} from '@core/core.state'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; -import {CLIENT_LWM2M, Instance, INSTANCES, ObjectLwM2M, ResourceLwM2M, RESOURCES} from './profile-config.models'; +import { + ATTRIBUTE, + ATTRIBUTE_LWM2M, + CLIENT_LWM2M, + Instance, + INSTANCES, + ObjectLwM2M, + ResourceLwM2M, + RESOURCES, + TELEMETRY +} from './lwm2m-profile-config.models'; import {deepClone, isDefinedAndNotNull, isEqual, isUndefined} from '@core/utils'; import {MatDialog} from '@angular/material/dialog'; import {TranslateService} from '@ngx-translate/core'; import { - Lwm2mObjectAddInstancesComponent, - Lwm2mObjectAddInstancesData -} from '@home/components/profile/device/lwm2m/lwm2m-object-add-instances.component'; + Lwm2mObjectAddInstancesData, + Lwm2mObjectAddInstancesDialogComponent +} from '@home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component'; @Component({ selector: 'tb-profile-lwm2m-observe-attr-telemetry', templateUrl: './lwm2m-observe-attr-telemetry.component.html', - styleUrls: ['./lwm2m-observe-attr-telemetry.component.css'], + styleUrls: [ './lwm2m-observe-attr-telemetry.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, @@ -56,6 +65,7 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor valuePrev = null as any; observeAttrTelemetryFormGroup: FormGroup; + resources = RESOURCES; get required(): boolean { return this.requiredValue; @@ -143,6 +153,7 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor name: objectLwM2M.name, multiple: objectLwM2M.multiple, mandatory: objectLwM2M.mandatory, + attributeLwm2m: objectLwM2M.attributeLwm2m, instances: this.createInstanceLwM2M(objectLwM2M.instances) }); })); @@ -152,6 +163,7 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor return this.fb.array(instancesLwM2M.map((instanceLwM2M) => { return this.fb.group({ id: instanceLwM2M.id, + attributeLwm2m: {value: instanceLwM2M.attributeLwm2m, disabled: this.disabled}, resources: {value: instanceLwM2M.resources, disabled: this.disabled} }); })); @@ -222,7 +234,7 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor $event.stopPropagation(); $event.preventDefault(); } - this.dialog.open(Lwm2mObjectAddInstancesComponent, { + this.dialog.open(Lwm2mObjectAddInstancesDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { @@ -240,6 +252,7 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor } private updateInstancesIds = (data: Lwm2mObjectAddInstancesData): void => { + debugger const objectLwM2MFormGroup = (this.observeAttrTelemetryFormGroup.get(CLIENT_LWM2M) as FormArray).controls .find(e => e.value.keyId === data.objectKeyId) as FormGroup; const instancesArray = objectLwM2MFormGroup.value.instances as Instance []; @@ -249,6 +262,8 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor r.attribute = false; r.telemetry = false; r.observe = false; + r.keyName = {}; + r.attributeLwm2m = {}; }); const valueOld = this.instancesToSetId(instancesArray); if (!isEqual(valueOld, data.instancesIds)) { @@ -286,6 +301,7 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor private pushInstance = (instancesFormArray: FormArray, x: number, instanceNew: Instance): void => { instancesFormArray.push(this.fb.group({ id: x, + attributeLwm2m: instanceNew.attributeLwm2m, resources: {value: instanceNew.resources, disabled: this.disabled} })); } @@ -297,4 +313,54 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor private instancesToSetId = (instances: Instance[]): Set => { return new Set(instances.map(x => x.id)); } + + getNameObjectLwm2m = (objectName: string, idVerObj: string): string => { + return objectName + ' <' + idVerObj + '>'; + } + getNameInstanceLwm2m = (instance: Instance, idVerObj: string): string => { + return ' instance <' + idVerObj + '/' + instance.id +'>'; + } + + updateAttributeLwm2mObject = (event: Event, objectKeyId: number): void => { + const objectLwM2MFormGroup = (this.observeAttrTelemetryFormGroup.get(CLIENT_LWM2M) as FormArray).controls + .find(e => e.value.keyId === objectKeyId) as FormGroup; + objectLwM2MFormGroup.patchValue({attributeLwm2m: event}); + } + + updateAttributeLwm2mInstance = (event: Event, indexInstance: number, objectKeyId: number): void => { + + const objectLwM2MFormGroup = (this.observeAttrTelemetryFormGroup.get(CLIENT_LWM2M) as FormArray).controls + .find(e => e.value.keyId === objectKeyId) as FormGroup; + const instancesFormArray = objectLwM2MFormGroup.get(INSTANCES) as FormArray; + instancesFormArray.at(indexInstance).patchValue({attributeLwm2m: event}); + } + + disableObserveInstance = (instance: AbstractControl): boolean => { + const checkedAttrTelemetry = this.observeInstance(instance); + if (checkedAttrTelemetry) { + instance.get(ATTRIBUTE_LWM2M).patchValue(null); + } + return checkedAttrTelemetry; + } + + disableObserveObject = (index: number): boolean => { + const object = (this.observeAttrTelemetryFormGroup.get(CLIENT_LWM2M) as FormArray).at(index) as FormGroup; + const instances = object.controls.instances as FormArray; + const checkedAttrTelemetry = instances.controls.filter(instance => !this.disableObserveInstance(instance)); + if (checkedAttrTelemetry.length === 0) { + object.controls.attributeLwm2m.patchValue(null); + } + return checkedAttrTelemetry.length === 0; + } + + + observeInstance = (instance: AbstractControl): boolean => { + const resources = instance.get(RESOURCES).value as ResourceLwM2M[]; + if (isDefinedAndNotNull(resources)) { + const checkedAttribute = resources.filter(resource => resource[ATTRIBUTE]); + const checkedTelemetry = resources.filter(resource => resource[TELEMETRY]); + return checkedAttribute.length === 0 && checkedTelemetry.length === 0; + } + return false; + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-components.module.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-components.module.ts index 70334c9908..43c297df56 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-components.module.ts @@ -14,16 +14,19 @@ /// limitations under the License. /// -import { NgModule } from '@angular/core'; -import { Lwm2mDeviceProfileTransportConfigurationComponent } from './lwm2m-device-profile-transport-configuration.component'; -import { Lwm2mObjectListComponent } from './lwm2m-object-list.component'; -import { Lwm2mObserveAttrTelemetryComponent } from './lwm2m-observe-attr-telemetry.component'; -import { Lwm2mObserveAttrTelemetryResourceComponent } from './lwm2m-observe-attr-telemetry-resource.component'; -import { Lwm2mDeviceConfigServerComponent } from './lwm2m-device-config-server.component'; -import { Lwm2mObjectAddInstancesComponent } from './lwm2m-object-add-instances.component'; -import { Lwm2mObjectAddInstancesListComponent } from './lwm2m-object-add-instances-list.component'; -import { CommonModule } from '@angular/common'; -import { SharedModule } from '@app/shared/shared.module'; +import {NgModule} from '@angular/core'; +import {Lwm2mDeviceProfileTransportConfigurationComponent} from './lwm2m-device-profile-transport-configuration.component'; +import {Lwm2mObjectListComponent} from './lwm2m-object-list.component'; +import {Lwm2mObserveAttrTelemetryComponent} from './lwm2m-observe-attr-telemetry.component'; +import {Lwm2mObserveAttrTelemetryResourceComponent} from './lwm2m-observe-attr-telemetry-resource.component'; +import {Lwm2mAttributesDialogComponent} from './lwm2m-attributes-dialog.component'; +import {Lwm2mAttributesComponent} from './lwm2m-attributes.component'; +import {Lwm2mAttributesKeyListComponent} from './lwm2m-attributes-key-list.component'; +import {Lwm2mDeviceConfigServerComponent} from './lwm2m-device-config-server.component'; +import {Lwm2mObjectAddInstancesDialogComponent} from './lwm2m-object-add-instances-dialog.component'; +import {Lwm2mObjectAddInstancesListComponent} from './lwm2m-object-add-instances-list.component'; +import {CommonModule} from '@angular/common'; +import {SharedModule} from '@app/shared/shared.module'; @NgModule({ declarations: @@ -32,8 +35,11 @@ import { SharedModule } from '@app/shared/shared.module'; Lwm2mObjectListComponent, Lwm2mObserveAttrTelemetryComponent, Lwm2mObserveAttrTelemetryResourceComponent, + Lwm2mAttributesDialogComponent, + Lwm2mAttributesComponent, + Lwm2mAttributesKeyListComponent, Lwm2mDeviceConfigServerComponent, - Lwm2mObjectAddInstancesComponent, + Lwm2mObjectAddInstancesDialogComponent, Lwm2mObjectAddInstancesListComponent ], imports: [ @@ -45,8 +51,11 @@ import { SharedModule } from '@app/shared/shared.module'; Lwm2mObjectListComponent, Lwm2mObserveAttrTelemetryComponent, Lwm2mObserveAttrTelemetryResourceComponent, + Lwm2mAttributesDialogComponent, + Lwm2mAttributesComponent, + Lwm2mAttributesKeyListComponent, Lwm2mDeviceConfigServerComponent, - Lwm2mObjectAddInstancesComponent, + Lwm2mObjectAddInstancesDialogComponent, Lwm2mObjectAddInstancesListComponent ], providers: [ diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts similarity index 70% rename from ui-ngx/src/app/modules/home/components/profile/device/lwm2m/profile-config.models.ts rename to ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts index 7241abbf76..ff927bc09f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/profile-config.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts @@ -16,7 +16,9 @@ export const PAGE_SIZE_LIMIT = 50; export const INSTANCES = 'instances'; +export const INSTANCE = 'instance'; export const RESOURCES = 'resources'; +export const ATTRIBUTE_LWM2M = 'attributeLwm2m'; export const CLIENT_LWM2M = 'clientLwM2M'; export const CLIENT_LWM2M_SETTINGS = 'clientLwM2mSettings'; export const OBSERVE_ATTR_TELEMETRY = 'observeAttrTelemetry'; @@ -33,7 +35,7 @@ export const DEFAULT_CLIENT_HOLD_OFF_TIME = 1; export const DEFAULT_LIFE_TIME = 300; export const DEFAULT_MIN_PERIOD = 1; export const DEFAULT_NOTIF_IF_DESIBLED = true; -export const DEFAULT_BINDING = 'U'; +export const DEFAULT_BINDING = 'UQ'; export const DEFAULT_BOOTSTRAP_SERVER_ACCOUNT_TIME_OUT = 0; export const LEN_MAX_PUBLIC_KEY_RPK = 182; export const LEN_MAX_PUBLIC_KEY_X509 = 3000; @@ -42,6 +44,72 @@ export const KEY_REGEXP_NUMBER = /^(\-?|\+?)\d*$/; export const INSTANCES_ID_VALUE_MIN = 0; export const INSTANCES_ID_VALUE_MAX = 65535; + +export enum BINDING_MODE { + U = 'U', + UQ = 'UQ', + T = 'T', + TQ = 'TQ', + S = 'S', + SQ = 'SQ', + US = 'US', + TS = 'TS', + UQS = 'UQS', + TQS = 'TQS' +} + +export const BINDING_MODE_NAMES = new Map( + [ + [BINDING_MODE.U, 'U: UDP connection in standard mode'], + [BINDING_MODE.UQ, 'UQ: UDP connection in queue mode'], + [BINDING_MODE.US, 'US: both UDP and SMS connections active, both in standard mode'], + [BINDING_MODE.UQS, 'UQS: both UDP and SMS connections active; UDP in queue mode, SMS in standard mode'], + [BINDING_MODE.T,'T: TCP connection in standard mode'], + [BINDING_MODE.TQ, 'TQ: TCP connection in queue mode'], + [BINDING_MODE.TS, 'TS: both TCP and SMS connections active, both in standard mode'], + [BINDING_MODE.TQS, 'TQS: both TCP and SMS connections active; TCP in queue mode, SMS in standard mode'], + [BINDING_MODE.S, 'S: SMS connection in standard mode'], + [BINDING_MODE.SQ, 'SQ: SMS connection in queue mode'] + ] +); + +export enum ATTRIBUTE_LWM2M_ENUM { + dim = 'dim', + ver = 'ver', + pmin = 'pmin', + pmax = 'pmax', + gt = 'gt', + lt = 'lt', + st = 'st' +} + +export const ATTRIBUTE_LWM2M_LABEL = new Map( + [ + [ATTRIBUTE_LWM2M_ENUM.dim, 'dim='], + [ATTRIBUTE_LWM2M_ENUM.ver, 'ver='], + [ATTRIBUTE_LWM2M_ENUM.pmin, 'pmin='], + [ATTRIBUTE_LWM2M_ENUM.pmax, 'pmax='], + [ATTRIBUTE_LWM2M_ENUM.gt, '>'], + [ATTRIBUTE_LWM2M_ENUM.lt, '<'], + [ATTRIBUTE_LWM2M_ENUM.st, 'st='] + ] +); + +export const ATTRIBUTE_LWM2M_MAP = new Map( + [ + [ATTRIBUTE_LWM2M_ENUM.dim, 'Dimension'], + [ATTRIBUTE_LWM2M_ENUM.ver, 'Object version'], + [ATTRIBUTE_LWM2M_ENUM.pmin, 'Minimum period'], + [ATTRIBUTE_LWM2M_ENUM.pmax, 'Maximum period'], + [ATTRIBUTE_LWM2M_ENUM.gt, 'Greater than'], + [ATTRIBUTE_LWM2M_ENUM.lt, 'Lesser than'], + [ATTRIBUTE_LWM2M_ENUM.st, 'Step'], + + ] +); + +export const ATTRIBUTE_KEYS = Object.keys(ATTRIBUTE_LWM2M_ENUM) as string[]; + export enum SECURITY_CONFIG_MODE { PSK = 'PSK', RPK = 'RPK', @@ -54,7 +122,7 @@ export const SECURITY_CONFIG_MODE_NAMES = new Map( [SECURITY_CONFIG_MODE.PSK, 'Pre-Shared Key'], [SECURITY_CONFIG_MODE.RPK, 'Raw Public Key'], [SECURITY_CONFIG_MODE.X509, 'X.509 Certificate'], - [SECURITY_CONFIG_MODE.NO_SEC, 'No Security'], + [SECURITY_CONFIG_MODE.NO_SEC, 'No Security'] ] ); @@ -90,7 +158,7 @@ interface BootstrapSecurityConfig { lwm2mServer: ServerSecurityConfig; } -export interface ProfileConfigModels { +export interface Lwm2mProfileConfigModels { clientLwM2mSettings: ClientLwM2mSettings; observeAttr: ObservableAttributes; bootstrap: BootstrapSecurityConfig; @@ -100,11 +168,13 @@ export interface ProfileConfigModels { export interface ClientLwM2mSettings { clientOnlyObserveAfterConnect: number; } + export interface ObservableAttributes { observe: string[]; attribute: string[]; telemetry: string[]; keyName: {}; + attributeLwm2m: {}; } export function getDefaultBootstrapServersSecurityConfig(): BootstrapServersSecurityConfig { @@ -151,21 +221,22 @@ function getDefaultProfileObserveAttrConfig(): ObservableAttributes { observe: [], attribute: [], telemetry: [], - keyName: {} + keyName: {}, + attributeLwm2m: {} }; } -function getDefaultProfileClientLwM2mSettingsConfig(): ClientLwM2mSettings { +export function getDefaultProfileConfig(hostname?: any): Lwm2mProfileConfigModels { return { - clientOnlyObserveAfterConnect: 1 + clientLwM2mSettings: getDefaultProfileClientLwM2mSettingsConfig(), + observeAttr: getDefaultProfileObserveAttrConfig(), + bootstrap: getDefaultProfileBootstrapSecurityConfig((hostname) ? hostname : DEFAULT_HOST_NAME) }; } -export function getDefaultProfileConfig(hostname?: any): ProfileConfigModels { +function getDefaultProfileClientLwM2mSettingsConfig(): ClientLwM2mSettings { return { - clientLwM2mSettings: getDefaultProfileClientLwM2mSettingsConfig(), - observeAttr: getDefaultProfileObserveAttrConfig(), - bootstrap: getDefaultProfileBootstrapSecurityConfig((hostname) ? hostname : DEFAULT_HOST_NAME) + clientOnlyObserveAfterConnect: 1 }; } @@ -175,11 +246,13 @@ export interface ResourceLwM2M { observe: boolean; attribute: boolean; telemetry: boolean; - keyName: string; + keyName: {}; + attributeLwm2m?: {}; } export interface Instance { id: number; + attributeLwm2m?: {}; resources: ResourceLwM2M[]; } @@ -190,11 +263,12 @@ export interface Instance { * mandatory == false => Optional */ export interface ObjectLwM2M { + id: number; keyId: string; name: string; multiple?: boolean; mandatory?: boolean; + attributeLwm2m?: {}; instances?: Instance []; } - diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html index 670e6fbb72..55f0869dbc 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html @@ -88,6 +88,21 @@ {{ 'device-profile.attributes-proto-schema-required' | translate}} + + device-profile.rpc-request-proto-schema + + + {{ 'device-profile.rpc-request-proto-schema-required' | translate}} + + device-profile.rpc-request-proto-schema-hint + + + device-profile.rpc-response-proto-schema + + + {{ 'device-profile.rpc-response-proto-schema-required' | translate}} + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts index a61ad841aa..381e2f9e8d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts @@ -29,6 +29,8 @@ import { AppState } from '@app/core/core.state'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { defaultAttributesSchema, + defaultRpcRequestSchema, + defaultRpcResponseSchema, defaultTelemetrySchema, DeviceProfileTransportConfiguration, DeviceTransportType, @@ -90,7 +92,9 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control transportPayloadTypeConfiguration: this.fb.group({ transportPayloadType: [TransportPayloadType.JSON, Validators.required], deviceTelemetryProtoSchema: [defaultTelemetrySchema, Validators.required], - deviceAttributesProtoSchema: [defaultAttributesSchema, Validators.required] + deviceAttributesProtoSchema: [defaultAttributesSchema, Validators.required], + deviceRpcRequestProtoSchema: [defaultRpcRequestSchema, Validators.required], + deviceRpcResponseProtoSchema: [defaultRpcResponseSchema, Validators.required] }) }, {validator: this.uniqueDeviceTopicValidator} ); @@ -139,15 +143,21 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control if (forceUpdated) { transportPayloadTypeForm.patchValue({ deviceTelemetryProtoSchema: defaultTelemetrySchema, - deviceAttributesProtoSchema: defaultAttributesSchema + deviceAttributesProtoSchema: defaultAttributesSchema, + deviceRpcRequestProtoSchema: defaultRpcRequestSchema, + deviceRpcResponseProtoSchema: defaultRpcResponseSchema }, {emitEvent: false}); } if (type === TransportPayloadType.PROTOBUF && !this.disabled) { transportPayloadTypeForm.get('deviceTelemetryProtoSchema').enable({emitEvent: false}); transportPayloadTypeForm.get('deviceAttributesProtoSchema').enable({emitEvent: false}); + transportPayloadTypeForm.get('deviceRpcRequestProtoSchema').enable({emitEvent: false}); + transportPayloadTypeForm.get('deviceRpcResponseProtoSchema').enable({emitEvent: false}); } else { transportPayloadTypeForm.get('deviceTelemetryProtoSchema').disable({emitEvent: false}); transportPayloadTypeForm.get('deviceAttributesProtoSchema').disable({emitEvent: false}); + transportPayloadTypeForm.get('deviceRpcRequestProtoSchema').enable({emitEvent: false}); + transportPayloadTypeForm.get('deviceRpcResponseProtoSchema').disable({emitEvent: false}); } } diff --git a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts index 46e162f5fe..b190b0d293 100644 --- a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.ts @@ -29,6 +29,7 @@ import { EntityService } from '@core/http/entity.service'; import { TruncatePipe } from '@shared/pipe/truncate.pipe'; import { RuleChainService } from '@core/http/rule-chain.service'; import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { RuleChainType } from '@app/shared/models/rule-chain.models'; @Component({ selector: 'tb-rule-chain-autocomplete', @@ -187,8 +188,9 @@ export class RuleChainAutocompleteComponent implements ControlValueAccessor, OnI fetchRuleChain(searchText?: string): Observable>> { this.searchText = searchText; + // voba: at the moment device profiles are not supported by edge, so 'core' hardcoded return this.entityService.getEntitiesByNameFilter(EntityType.RULE_CHAIN, searchText, - 50, null, {ignoreLoading: true}); + 50, RuleChainType.CORE, {ignoreLoading: true}); } clear() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html new file mode 100644 index 0000000000..13deeacc17 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.html @@ -0,0 +1,28 @@ + +
+
+ {{ customerTitle }} + edge.widget-datasource-error +
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.scss new file mode 100644 index 0000000000..ec930cceb1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.scss @@ -0,0 +1,128 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host-context(.tb-has-timewindow) { + .tb-edges-overview { + mat-toolbar { + height: 60px; + max-height: 60px; + .mat-toolbar-tools { + height: 60px; + max-height: 60px; + } + } + } +} + +:host { + .tb-edges-overview { + mat-toolbar.mat-table-toolbar:not([color="primary"]) { + background: transparent; + } + mat-toolbar { + min-height: 39px; + max-height: 39px; + .mat-toolbar-tools { + min-height: 39px; + max-height: 39px; + } + } + + .mat-subheader { + padding: 15px; + color: rgba(0,0,0,0.54); + font-size: 14px; + font-weight: 400; + } + + .tb-entities-nav-tree-panel { + overflow-x: auto; + overflow-y: auto; + } + } +} + +:host ::ng-deep { + .tb-nav-tree-container { + &.jstree-proton { + .jstree-anchor { + div.node-icon { + display: inline-block; + width: 22px; + height: 22px; + margin-right: 2px; + margin-bottom: 2px; + background-color: transparent; + background-repeat: no-repeat; + background-attachment: scroll; + background-position: center center; + background-size: 18px 18px; + } + + mat-icon.node-icon { + width: 22px; + min-width: 22px; + height: 22px; + min-height: 22px; + margin-right: 2px; + margin-bottom: 2px; + color: inherit; + vertical-align: middle; + + &.material-icons { + font-size: 18px; + line-height: 22px; + text-align: center; + } + } + + &.jstree-hovered:not(.jstree-clicked), + &.jstree-disabled { + div.node-icon { + opacity: .5; + } + } + } + } + } + + @media (max-width: 768px) { + .tb-nav-tree-container { + &.jstree-proton-responsive { + .jstree-anchor { + div.node-icon { + width: 40px; + height: 40px; + margin: 0; + background-size: 24px 24px; + } + + mat-icon.node-icon { + width: 40px; + min-width: 40px; + height: 40px; + min-height: 40px; + margin: 0; + + &.material-icons { + font-size: 24px; + line-height: 40px; + } + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts new file mode 100644 index 0000000000..0b15def29f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.component.ts @@ -0,0 +1,197 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { Datasource, DatasourceType, WidgetConfig } from '@shared/models/widget.models'; +import { IWidgetSubscription } from '@core/api/widget-api.models'; +import { UtilsService } from '@core/services/utils.service'; +import { LoadNodesCallback } from '@shared/components/nav-tree.component'; +import { EntityType } from '@shared/models/entity-type.models'; +import { + EdgeGroupNodeData, + edgeGroupsNodeText, + edgeGroupsTypes, + entityNodeText, + EdgeOverviewNode, + EntityNodeData, + EntityNodeDatasource +} from '@home/components/widget/lib/edges-overview-widget.models'; +import { EdgeService } from '@core/http/edge.service'; +import { EntityService } from '@core/http/entity.service'; +import { TranslateService } from '@ngx-translate/core'; +import { PageLink } from '@shared/models/page/page-link'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { isDefined } from '@core/utils'; + +interface EdgesOverviewWidgetSettings { + enableDefaultTitle: boolean; +} + +@Component({ + selector: 'tb-edges-overview-widget', + templateUrl: './edges-overview-widget.component.html', + styleUrls: ['./edges-overview-widget.component.scss'] +}) +export class EdgesOverviewWidgetComponent extends PageComponent implements OnInit { + + @Input() + ctx: WidgetContext; + + public toastTargetId = 'edges-overview-' + this.utils.guid(); + public customerTitle: string = null; + public edgeIsDatasource: boolean = true; + + private widgetConfig: WidgetConfig; + private subscription: IWidgetSubscription; + private datasources: Array; + private settings: EdgesOverviewWidgetSettings; + + private nodeIdCounter = 0; + + constructor(protected store: Store, + private edgeService: EdgeService, + private entityService: EntityService, + private translateService: TranslateService, + private utils: UtilsService, + private cd: ChangeDetectorRef) { + super(store); + } + + ngOnInit(): void { + this.widgetConfig = this.ctx.widgetConfig; + this.subscription = this.ctx.defaultSubscription; + this.datasources = this.subscription.datasources as Array; + this.settings = this.ctx.settings; + this.initializeConfig(); + this.ctx.updateWidgetParams(); + } + + public loadNodes: LoadNodesCallback = (node, cb) => { + const datasource: Datasource = this.datasources[0]; + if (node.id === '#' && datasource) { + if (datasource.type === DatasourceType.entity && datasource.entity.id.entityType === EntityType.EDGE) { + var selectedEdge: BaseData = datasource.entity; + this.updateTitle(selectedEdge); + this.getCustomerTitle(selectedEdge.id.id); + cb(this.loadNodesForEdge(selectedEdge)); + } else if (datasource.type === DatasourceType.function) { + cb(this.loadNodesForEdge(datasource.entity)); + } else { + this.edgeIsDatasource = false; + cb([]); + } + } + else if (node.data && node.data.entity.id.entityType === EntityType.EDGE) { + const edgeId = node.data.entity.id.id; + const entityType = node.data.entityType; + const pageLink = new PageLink(datasource.pageLink.pageSize); + this.entityService.getAssignedToEdgeEntitiesByType(edgeId, entityType, pageLink).subscribe( + (entities) => { + if (entities.data.length > 0) { + cb(this.entitiesToNodes(entities.data)); + } else { + cb([]); + } + } + ); + } else { + cb([]); + } + } + + private loadNodesForEdge(entity: BaseData): EdgeOverviewNode[] { + const nodes: EdgeOverviewNode[] = []; + const authUser = getCurrentAuthUser(this.store); + var allowedGroupTypes: EntityType[] = edgeGroupsTypes; + if (authUser.authority === Authority.CUSTOMER_USER) { + allowedGroupTypes = edgeGroupsTypes.filter(type => type !== EntityType.RULE_CHAIN); + } + allowedGroupTypes.forEach((entityType) => { + const node: EdgeOverviewNode = { + id: (++this.nodeIdCounter)+'', + icon: false, + text: edgeGroupsNodeText(this.translateService, entityType), + children: true, + data: { + entityType, + entity, + internalId: entity.id.id + '_' + entityType + } as EdgeGroupNodeData + }; + nodes.push(node); + }); + return nodes; + } + + private createEntityNode(entity: BaseData): EdgeOverviewNode { + return { + id: (++this.nodeIdCounter)+'', + icon: false, + text: entityNodeText(entity), + children: false, + state: { + disabled: false + }, + data: { + entity: entity, + internalId: entity.id.id + } as EntityNodeData + } as EdgeOverviewNode; + } + + private entitiesToNodes(entities: BaseData[]): EdgeOverviewNode[] { + const nodes: EdgeOverviewNode[] = []; + if (entities) { + entities.forEach((entity) => { + const node = this.createEntityNode(entity); + nodes.push(node); + }); + } + return nodes; + } + + private getCustomerTitle(edgeId: string) { + this.edgeService.getEdgeInfo(edgeId).subscribe( + (edge) => { + if (edge.customerTitle) { + this.customerTitle = this.translateService.instant('edge.assigned-to-customer', {customerTitle: edge.customerTitle}); + } else { + this.customerTitle = null; + } + this.cd.detectChanges(); + }); + } + + private initializeConfig(): void { + const edgeIsDatasource: boolean = this.datasources[0] && this.datasources[0].type === DatasourceType.entity && this.datasources[0].entity.id.entityType === EntityType.EDGE; + if (edgeIsDatasource) { + const edge = this.datasources[0].entity; + this.updateTitle(edge); + } + } + + private updateTitle(edge: BaseData): void { + const displayDefaultTitle: boolean = isDefined(this.settings.enableDefaultTitle) ? this.settings.enableDefaultTitle : false; + this.ctx.widgetTitle = displayDefaultTitle ? `${edge.name} Quick Overview` : this.widgetConfig.title; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts new file mode 100644 index 0000000000..e13ef2797f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/edges-overview-widget.models.ts @@ -0,0 +1,108 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NavTreeNode } from '@shared/components/nav-tree.component'; +import { Datasource } from '@shared/models/widget.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { BaseData, HasId } from '@shared/models/base-data'; + +export interface EntityNodeDatasource extends Datasource { + nodeId: string; +} + +export function edgeGroupsNodeText(translate: TranslateService, entityType: EntityType): string { + const nodeIcon = materialIconByEntityType(entityType); + const nodeText = textForEdgeGroupsType(translate, entityType); + return nodeIcon + nodeText; +} + +export function entityNodeText(entity: any): string { + const nodeIcon = materialIconByEntityType(entity.id.entityType); + const nodeText = entity.name; + return nodeIcon + nodeText; +} + +export function materialIconByEntityType(entityType: EntityType): string { + let materialIcon = 'insert_drive_file'; + switch (entityType) { + case EntityType.DEVICE: + materialIcon = 'devices_other'; + break; + case EntityType.ASSET: + materialIcon = 'domain'; + break; + case EntityType.DASHBOARD: + materialIcon = 'dashboards'; + break; + case EntityType.ENTITY_VIEW: + materialIcon = 'view_quilt'; + break; + case EntityType.RULE_CHAIN: + materialIcon = 'settings_ethernet'; + break; + } + return '' + materialIcon + ''; +} + +export function textForEdgeGroupsType(translate: TranslateService, entityType: EntityType): string { + let textForEdgeGroupsType: string = ''; + switch (entityType) { + case EntityType.DEVICE: + textForEdgeGroupsType = 'device.devices'; + break; + case EntityType.ASSET: + textForEdgeGroupsType = 'asset.assets'; + break; + case EntityType.DASHBOARD: + textForEdgeGroupsType = 'dashboard.dashboards'; + break; + case EntityType.ENTITY_VIEW: + textForEdgeGroupsType = 'entity-view.entity-views'; + break; + case EntityType.RULE_CHAIN: + textForEdgeGroupsType = 'rulechain.rulechains'; + break; + } + return translate.instant(textForEdgeGroupsType); +} + +export const edgeGroupsTypes: EntityType[] = [ + EntityType.ASSET, + EntityType.DEVICE, + EntityType.ENTITY_VIEW, + EntityType.DASHBOARD, + EntityType.RULE_CHAIN +] + +export interface EdgeOverviewNode extends NavTreeNode { + data?: EdgeOverviewNodeData; +} + +export type EdgeOverviewNodeData = EdgeGroupNodeData | EntityNodeData; + +export interface EdgeGroupNodeData extends BaseEdgeOverviewNodeData { + entityType: EntityType; + entity: BaseData; +} + +export interface EntityNodeData extends BaseEdgeOverviewNodeData { + entity: BaseData; +} + +export interface BaseEdgeOverviewNodeData { + internalId: string; +} 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 new file mode 100644 index 0000000000..81c06fe0f7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html @@ -0,0 +1,69 @@ + + +
+ +
+
+ +
+
+ + +
+
+ +
+
+ {{ 'widgets.input-widgets.no-entity-selected' | translate }} +
+
+ {{ 'widgets.input-widgets.no-datakey-selected' | translate }} +
+
+ {{ errorMessage | translate }} +
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss new file mode 100644 index 0000000000..f097de5afb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tb-json-input { + width: 100%; + height: 100%; + padding: 5px; + + &__form { + overflow: auto; + height: 100%; + } + + &__error { + text-align: center; + font-size: 18px; + color: #a0a0a0; + } +} + +.tb-toast { + font-size: 14px!important; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts new file mode 100644 index 0000000000..ce174fcde0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts @@ -0,0 +1,227 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models'; +import { IWidgetSubscription } from '@core/api/widget-api.models'; +import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { AttributeService } from '@core/http/attribute.service'; +import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; +import { createLabelFromDatasource } from '@core/utils'; +import { Observable } from 'rxjs'; + +enum JsonInputWidgetMode { + ATTRIBUTE = 'ATTRIBUTE', + TIME_SERIES = 'TIME_SERIES', +} + +interface JsonInputWidgetSettings { + widgetTitle: string; + widgetMode: JsonInputWidgetMode; + attributeScope?: AttributeScope; + showLabel: boolean; + labelValue?: string; + attributeRequired: boolean; + showResultMessage: boolean; +} + +@Component({ + selector: 'tb-json-input-widget ', + templateUrl: './json-input-widget.component.html', + styleUrls: ['./json-input-widget.component.scss'] +}) +export class JsonInputWidgetComponent extends PageComponent implements OnInit { + + @Input() + ctx: WidgetContext; + + public settings: JsonInputWidgetSettings; + private widgetConfig: WidgetConfig; + private subscription: IWidgetSubscription; + private datasource: Datasource; + + labelValue: string; + + entityDetected = false; + dataKeyDetected = false; + isValidParameter = false; + errorMessage: string; + + isFocused: boolean; + originalValue: any; + attributeUpdateFormGroup: FormGroup; + + toastTargetId = 'json-input-widget' + this.utils.guid(); + + constructor(protected store: Store, + private utils: UtilsService, + private fb: FormBuilder, + private attributeService: AttributeService, + private translate: TranslateService) { + super(store); + } + + ngOnInit(): void { + this.ctx.$scope.jsonInputWidget = this; + this.settings = this.ctx.settings; + this.widgetConfig = this.ctx.widgetConfig; + this.subscription = this.ctx.defaultSubscription; + this.datasource = this.subscription.datasources[0]; + this.initializeConfig(); + this.validateDatasources(); + this.buildForm(); + this.ctx.updateWidgetParams(); + } + + private initializeConfig() { + if (this.settings.widgetTitle && this.settings.widgetTitle.length) { + const title = createLabelFromDatasource(this.datasource, this.settings.widgetTitle); + this.ctx.widgetTitle = this.utils.customTranslation(title, title); + } else { + this.ctx.widgetTitle = this.ctx.widgetConfig.title; + } + + if (this.settings.labelValue && this.settings.labelValue.length) { + const label = createLabelFromDatasource(this.datasource, this.settings.labelValue); + this.labelValue = this.utils.customTranslation(label, label); + } else { + this.labelValue = this.translate.instant('widgets.input-widgets.value'); + } + } + + private validateDatasources() { + if (this.datasource?.type === DatasourceType.entity) { + this.entityDetected = true; + if (this.datasource.dataKeys.length) { + this.dataKeyDetected = true; + + if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) { + if (this.datasource.dataKeys[0].type === DataKeyType.attribute) { + if (this.settings.attributeScope === AttributeScope.SERVER_SCOPE || this.datasource.entityType === EntityType.DEVICE) { + this.isValidParameter = true; + } else { + this.errorMessage = 'widgets.input-widgets.not-allowed-entity'; + } + } else { + this.errorMessage = 'widgets.input-widgets.no-attribute-selected'; + } + } else { + if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) { + this.isValidParameter = true; + } else { + this.errorMessage = 'widgets.input-widgets.no-timeseries-selected'; + } + } + + } + } + } + + private buildForm() { + const validators: ValidatorFn[] = []; + if (this.settings.attributeRequired) { + validators.push(Validators.required); + } + this.attributeUpdateFormGroup = this.fb.group({ + currentValue: [{}, validators] + }); + this.attributeUpdateFormGroup.valueChanges.subscribe(() => { + this.ctx.detectChanges(); + }); + } + + private updateWidgetData(data: Array) { + if (this.isValidParameter) { + let value = {}; + if (data[0].data[0][1] !== '') { + try { + value = JSON.parse(data[0].data[0][1]); + } catch (e) { + value = data[0].data[0][1]; + } + } + this.originalValue = value; + if (!this.isFocused) { + this.attributeUpdateFormGroup.get('currentValue').patchValue(this.originalValue); + this.ctx.detectChanges(); + } + } + } + + public onDataUpdated() { + this.updateWidgetData(this.subscription.data); + } + + public save() { + this.isFocused = false; + + const attributeToSave: AttributeData = { + key: this.datasource.dataKeys[0].name, + value: this.attributeUpdateFormGroup.get('currentValue').value + }; + + const entityId: EntityId = { + entityType: this.datasource.entityType, + id: this.datasource.entityId + }; + + let saveAttributeObservable: Observable; + if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) { + saveAttributeObservable = this.attributeService.saveEntityAttributes( + entityId, + this.settings.attributeScope, + [attributeToSave], + {} + ); + } else { + saveAttributeObservable = this.attributeService.saveEntityTimeseries( + entityId, + LatestTelemetry.LATEST_TELEMETRY, + [attributeToSave], + {} + ); + } + saveAttributeObservable.subscribe( + () => { + this.attributeUpdateFormGroup.markAsPristine(); + this.ctx.detectChanges(); + if (this.settings.showResultMessage) { + this.ctx.showSuccessToast(this.translate.instant('widgets.input-widgets.update-successful'), + 1000, 'bottom', 'left', this.toastTargetId); + } + }, + () => { + if (this.settings.showResultMessage) { + this.ctx.showErrorToast(this.translate.instant('widgets.input-widgets.update-failed'), + 'bottom', 'left', this.toastTargetId); + } + }); + } + + public discard() { + this.attributeUpdateFormGroup.reset({currentValue: this.originalValue}, {emitEvent: false}); + this.attributeUpdateFormGroup.markAsPristine(); + this.isFocused = false; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts index 077d8dfb6d..32a9074ca0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts @@ -19,7 +19,8 @@ import { createLabelFromDatasource, hashCode, isDefined, - isDefinedAndNotNull, isFunction, + isDefinedAndNotNull, + isFunction, isNumber, isUndefined, padValue @@ -30,7 +31,7 @@ import { Datasource, DatasourceData } from '@shared/models/widget.models'; import _ from 'lodash'; import { mapProviderSchema, providerSets } from '@home/components/widget/lib/maps/schemes'; import { addCondition, mergeSchemes } from '@core/schema-utils'; -import L, {Projection} from "leaflet"; +import L from 'leaflet'; export function getProviderSchema(mapProvider: MapProviders, ignoreImageMap = false) { const providerSchema = _.cloneDeep(mapProviderSchema); @@ -318,22 +319,24 @@ export const parseWithTranslation = { }; export function parseData(input: DatasourceData[]): FormattedData[] { - return _(input).groupBy(el => el?.datasource?.entityName) + return _(input).groupBy(el => el?.datasource.entityId + el?.datasource.entityType) .values().value().map((entityArray, i) => { const obj: FormattedData = { entityName: entityArray[0]?.datasource?.entityName, - entityId: entityArray[0]?.datasource?.entityId, - entityType: entityArray[0]?.datasource?.entityType, - $datasource: entityArray[0]?.datasource, + entityId: entityArray[0].datasource.entityId, + entityType: entityArray[0].datasource.entityType, + $datasource: entityArray[0].datasource, dsIndex: i, deviceType: null }; entityArray.filter(el => el.data.length).forEach(el => { - const indexDate = el?.data?.length ? el.data.length - 1 : 0; - obj[el?.dataKey?.label] = el?.data[indexDate][1]; - obj[el?.dataKey?.label + '|ts'] = el?.data[indexDate][0]; - if (el?.dataKey?.label === 'type') { - obj.deviceType = el?.data[indexDate][1]; + const indexDate = el.data.length ? el.data.length - 1 : 0; + if (!obj.hasOwnProperty(el.dataKey.label) || el.data[indexDate][1] !== '') { + obj[el.dataKey.label] = el.data[indexDate][1]; + obj[el.dataKey.label + '|ts'] = el.data[indexDate][0]; + if (el.dataKey.label === 'type') { + obj.deviceType = el.data[indexDate][1]; + } } }); return obj; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts index b1b5a3b336..d2cf53265f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts @@ -121,7 +121,7 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy this.switchResize$ = new ResizeObserver(() => { this.resize(); - }) + }); this.switchResize$.observe(this.switchContainerRef.nativeElement); this.init(); } @@ -202,13 +202,13 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy let width = this.switchContainer.width(); let height = this.switchContainer.height(); if (this.showOnOffLabels) { - height = height*2/3; + height = height * 2 / 3; } - const ratio = width/height; + const ratio = width / height; if (ratio > switchAspectRation) { - width = height*switchAspectRation; + width = height * switchAspectRation; } else { - height = width/switchAspectRation; + height = width / switchAspectRation; } this.switchElement.css({width, height}); this.matSlideToggle.css({width, height, minWidth: width}); @@ -232,11 +232,11 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy fontSize--; textWidth = this.measureTextWidth(text, fontSize); } - element.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'}); + element.css({fontSize: fontSize + 'px', lineHeight: fontSize + 'px'}); } private measureTextWidth(text: string, fontSize: number): number { - this.textMeasure.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'}); + this.textMeasure.css({fontSize: fontSize + 'px', lineHeight: fontSize + 'px'}); this.textMeasure.text(text); return this.textMeasure.width(); } @@ -260,6 +260,7 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy this.ctx.controlApi.sendTwoWayCommand(this.getValueMethod, null, this.requestTimeout).subscribe( (responseBody) => { this.setValue(this.parseValueFunction(responseBody)); + this.ctx.detectChanges(); }, () => { const errorText = this.ctx.defaultSubscription.rpcErrorText; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 2d080bc8e9..b55c21b179 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -37,6 +37,8 @@ import { GatewayFormComponent } from './lib/gateway/gateway-form.component'; import { ImportExportService } from '@home/components/import-export/import-export.service'; import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component'; import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component'; +import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-overview-widget.component'; +import { JsonInputWidgetComponent } from '@home/components/widget/lib/json-input-widget.component'; @NgModule({ declarations: @@ -47,8 +49,10 @@ import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navig AlarmsTableWidgetComponent, TimeseriesTableWidgetComponent, EntitiesHierarchyWidgetComponent, + EdgesOverviewWidgetComponent, DateRangeNavigatorWidgetComponent, DateRangeNavigatorPanelComponent, + JsonInputWidgetComponent, MultipleInputWidgetComponent, TripAnimationComponent, PhotoCameraInputWidgetComponent, @@ -67,8 +71,10 @@ import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navig AlarmsTableWidgetComponent, TimeseriesTableWidgetComponent, EntitiesHierarchyWidgetComponent, + EdgesOverviewWidgetComponent, RpcWidgetsModule, DateRangeNavigatorWidgetComponent, + JsonInputWidgetComponent, MultipleInputWidgetComponent, TripAnimationComponent, PhotoCameraInputWidgetComponent, @@ -81,4 +87,5 @@ import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navig ImportExportService ] }) -export class WidgetComponentsModule { } +export class WidgetComponentsModule { +} diff --git a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts index f5b26df29c..bcaaa71bf9 100644 --- a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts @@ -28,6 +28,7 @@ import { EntityViewService } from '@core/http/entity-view.service'; import { DashboardService } from '@core/http/dashboard.service'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; +import { EdgeService } from '@core/http/edge.service'; export interface AddEntitiesToCustomerDialogData { customerId: string; @@ -57,6 +58,7 @@ export class AddEntitiesToCustomerDialogComponent extends @Inject(MAT_DIALOG_DATA) public data: AddEntitiesToCustomerDialogData, private deviceService: DeviceService, private assetService: AssetService, + private edgeService: EdgeService, private entityViewService: EntityViewService, private dashboardService: DashboardService, @SkipSelf() private errorStateMatcher: ErrorStateMatcher, @@ -79,6 +81,10 @@ export class AddEntitiesToCustomerDialogComponent extends this.assignToCustomerTitle = 'asset.assign-asset-to-customer'; this.assignToCustomerText = 'asset.assign-asset-to-customer-text'; break; + case EntityType.EDGE: + this.assignToCustomerTitle = 'edge.assign-edge-to-customer'; + this.assignToCustomerText = 'edge.assign-edge-to-customer-text'; + break; case EntityType.ENTITY_VIEW: this.assignToCustomerTitle = 'entity-view.assign-entity-view-to-customer'; this.assignToCustomerText = 'entity-view.assign-entity-view-to-customer-text'; @@ -122,6 +128,8 @@ export class AddEntitiesToCustomerDialogComponent extends return this.deviceService.assignDeviceToCustomer(customerId, entityId); case EntityType.ASSET: return this.assetService.assignAssetToCustomer(customerId, entityId); + case EntityType.EDGE: + return this.edgeService.assignEdgeToCustomer(customerId, entityId); case EntityType.ENTITY_VIEW: return this.entityViewService.assignEntityViewToCustomer(customerId, entityId); case EntityType.DASHBOARD: diff --git a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html new file mode 100644 index 0000000000..82f166d71a --- /dev/null +++ b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.html @@ -0,0 +1,57 @@ + +
+ +

{{ assignToEdgeTitle | translate }}

+ + +
+ + +
+
+
+ {{ assignToEdgeText | translate }} + + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.ts b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.ts new file mode 100644 index 0000000000..1cd9ba87ba --- /dev/null +++ b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-edge-dialog.component.ts @@ -0,0 +1,146 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { DeviceService } from '@core/http/device.service'; +import { EdgeService } from '@core/http/edge.service'; +import { EntityType } from '@shared/models/entity-type.models'; +import { forkJoin, Observable } from 'rxjs'; +import { AssetService } from '@core/http/asset.service'; +import { EntityViewService } from '@core/http/entity-view.service'; +import { DashboardService } from '@core/http/dashboard.service'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { RuleChainType } from '@shared/models/rule-chain.models'; + +export interface AddEntitiesToEdgeDialogData { + edgeId: string; + entityType: EntityType; +} + +@Component({ + selector: 'tb-add-entities-to-edge-dialog', + templateUrl: './add-entities-to-edge-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: AddEntitiesToEdgeDialogComponent}], + styleUrls: [] +}) +export class AddEntitiesToEdgeDialogComponent extends + DialogComponent implements OnInit, ErrorStateMatcher { + + addEntitiesToEdgeFormGroup: FormGroup; + + submitted = false; + + entityType: EntityType; + subType: string; + + assignToEdgeTitle: string; + assignToEdgeText: string; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddEntitiesToEdgeDialogData, + private deviceService: DeviceService, + private edgeService: EdgeService, + private assetService: AssetService, + private entityViewService: EntityViewService, + private dashboardService: DashboardService, + private ruleChainService: RuleChainService, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + this.entityType = data.entityType; + } + + ngOnInit(): void { + this.addEntitiesToEdgeFormGroup = this.fb.group({ + entityIds: [null, [Validators.required]] + }); + this.subType = ''; + switch (this.data.entityType) { + case EntityType.DEVICE: + this.assignToEdgeTitle = 'device.assign-device-to-edge-title'; + this.assignToEdgeText = 'device.assign-device-to-edge-text'; + break; + case EntityType.RULE_CHAIN: + this.assignToEdgeTitle = 'rulechain.assign-rulechain-to-edge-title'; + this.assignToEdgeText = 'rulechain.assign-rulechain-to-edge-text'; + this.subType = RuleChainType.EDGE; + break; + case EntityType.ASSET: + this.assignToEdgeTitle = 'asset.assign-asset-to-edge-title'; + this.assignToEdgeText = 'asset.assign-asset-to-edge-text'; + break; + case EntityType.ENTITY_VIEW: + this.assignToEdgeTitle = 'entity-view.assign-entity-view-to-edge-title'; + this.assignToEdgeText = 'entity-view.assign-entity-view-to-edge-text'; + break; + case EntityType.DASHBOARD: + this.assignToEdgeTitle = 'dashboard.assign-dashboard-to-edge-title'; + this.assignToEdgeText = 'dashboard.assign-dashboard-to-edge-text'; + break; + } + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(false); + } + + assign(): void { + this.submitted = true; + const entityIds: Array = this.addEntitiesToEdgeFormGroup.get('entityIds').value; + const tasks: Observable[] = []; + entityIds.forEach( + (entityId) => { + tasks.push(this.getAssignToEdgeTask(this.data.edgeId, entityId)); + } + ); + forkJoin(tasks).subscribe( + () => { + this.dialogRef.close(true); + } + ); + } + + private getAssignToEdgeTask(edgeId: string, entityId: string): Observable { + switch (this.data.entityType) { + case EntityType.DEVICE: + return this.deviceService.assignDeviceToEdge(edgeId, entityId); + case EntityType.ASSET: + return this.assetService.assignAssetToEdge(edgeId, entityId); + case EntityType.ENTITY_VIEW: + return this.entityViewService.assignEntityViewToEdge(edgeId, entityId); + case EntityType.DASHBOARD: + return this.dashboardService.assignDashboardToEdge(edgeId, entityId); + case EntityType.RULE_CHAIN: + return this.ruleChainService.assignRuleChainToEdge(edgeId, entityId); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts index 8d8230c5ee..2574bf8ae1 100644 --- a/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts @@ -28,6 +28,7 @@ import { AssetService } from '@core/http/asset.service'; import { EntityViewService } from '@core/http/entity-view.service'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; +import { EdgeService } from '@core/http/edge.service'; export interface AssignToCustomerDialogData { entityIds: Array; @@ -57,6 +58,7 @@ export class AssignToCustomerDialogComponent extends @Inject(MAT_DIALOG_DATA) public data: AssignToCustomerDialogData, private deviceService: DeviceService, private assetService: AssetService, + private edgeService: EdgeService, private entityViewService: EntityViewService, @SkipSelf() private errorStateMatcher: ErrorStateMatcher, public dialogRef: MatDialogRef, @@ -77,6 +79,10 @@ export class AssignToCustomerDialogComponent extends this.assignToCustomerTitle = 'asset.assign-asset-to-customer'; this.assignToCustomerText = 'asset.assign-to-customer-text'; break; + case EntityType.EDGE: + this.assignToCustomerTitle = 'edge.assign-edge-to-customer'; + this.assignToCustomerText = 'edge.assign-to-customer-text'; + break; case EntityType.ENTITY_VIEW: this.assignToCustomerTitle = 'entity-view.assign-entity-view-to-customer'; this.assignToCustomerText = 'entity-view.assign-to-customer-text'; @@ -116,6 +122,8 @@ export class AssignToCustomerDialogComponent extends return this.deviceService.assignDeviceToCustomer(customerId, entityId); case EntityType.ASSET: return this.assetService.assignAssetToCustomer(customerId, entityId); + case EntityType.EDGE: + return this.edgeService.assignEdgeToCustomer(customerId, entityId); case EntityType.ENTITY_VIEW: return this.entityViewService.assignEntityViewToCustomer(customerId, entityId); } diff --git a/ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts index 1a5cc337df..91079ad040 100644 --- a/ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts +++ b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts @@ -20,12 +20,14 @@ import { SharedModule } from '@app/shared/shared.module'; import { AssignToCustomerDialogComponent } from '@modules/home/dialogs/assign-to-customer-dialog.component'; import { AddEntitiesToCustomerDialogComponent } from '@modules/home/dialogs/add-entities-to-customer-dialog.component'; import { HomeDialogsService } from './home-dialogs.service'; +import { AddEntitiesToEdgeDialogComponent } from '@home/dialogs/add-entities-to-edge-dialog.component'; @NgModule({ declarations: [ AssignToCustomerDialogComponent, - AddEntitiesToCustomerDialogComponent + AddEntitiesToCustomerDialogComponent, + AddEntitiesToEdgeDialogComponent ], imports: [ CommonModule, @@ -33,7 +35,8 @@ import { HomeDialogsService } from './home-dialogs.service'; ], exports: [ AssignToCustomerDialogComponent, - AddEntitiesToCustomerDialogComponent + AddEntitiesToCustomerDialogComponent, + AddEntitiesToEdgeDialogComponent ], providers: [ HomeDialogsService diff --git a/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts index 69b183bb3f..fc7fd73384 100644 --- a/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts +++ b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts @@ -36,6 +36,8 @@ export class HomeDialogsService { return this.openImportDialogCSV(entityType, 'device.import', 'device.device-file'); case EntityType.ASSET: return this.openImportDialogCSV(entityType, 'asset.import', 'asset.asset-file'); + case EntityType.EDGE: + return this.openImportDialogCSV(entityType, 'edge.import', 'edge.edge-file'); } } diff --git a/ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw b/ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw index b54ce8ea99..0a037e80bb 100644 --- a/ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw +++ b/ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw @@ -4413,7 +4413,8 @@ "showDashboardExport": false, "toolbarAlwaysOpen": true, "titleColor": "rgba(0,0,0,0.870588)", - "showFilters": false + "showFilters": false, + "showDashboardLogo": false } }, "name": "Api Usage" diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset.component.html b/ui-ngx/src/app/modules/home/pages/asset/asset.component.html index 3bcc4594c9..66b9a152c7 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset.component.html +++ b/ui-ngx/src/app/modules/home/pages/asset/asset.component.html @@ -34,6 +34,12 @@ [fxShow]="!isEdit && (assetScope === 'customer' || assetScope === 'tenant') && isAssignedToCustomer(entity)"> {{ (entity?.customerIsPublic ? 'asset.make-private' : 'asset.unassign-from-customer') | translate }} + + + + + + + +
+ + + + + +
+
+ + + + +
+
+
+ + edge.assignedToCustomer + + +
+ {{ 'edge.edge-public' | translate }} +
+
+
+ + edge.name + + + {{ 'edge.name-required' | translate }} + + + + +
+
+ + edge.edge-license-key + + + {{ 'edge.edge-license-key-required' | translate }} + + +
+
+
edge.cloud-endpoint-hint
+ + edge.cloud-endpoint + + + {{ 'edge.cloud-endpoint-required' | translate }} + + +
+
+
+ + edge.edge-key + + + +
+
+ + edge.edge-secret + + + +
+
+ + edge.label + + +
+ + edge.description + + +
+
+
+
diff --git a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss b/ui-ngx/src/app/modules/home/pages/edge/edge.component.scss similarity index 96% rename from ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss rename to ui-ngx/src/app/modules/home/pages/edge/edge.component.scss index 97c400525e..3abcc4c738 100644 --- a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.component.scss @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - :host { - min-width: 364px; + } diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts b/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts new file mode 100644 index 0000000000..007176ce24 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts @@ -0,0 +1,142 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '@home/components/entity/entity.component'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { EdgeInfo } from '@shared/models/edge.models'; +import { TranslateService } from '@ngx-translate/core'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { generateSecret, guid } from '@core/utils'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; + +@Component({ + selector: 'tb-edge', + templateUrl: './edge.component.html', + styleUrls: ['./edge.component.scss'] +}) +export class EdgeComponent extends EntityComponent { + + entityType = EntityType; + + edgeScope: 'tenant' | 'customer' | 'customer_user'; + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: EdgeInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + public fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + ngOnInit() { + this.edgeScope = this.entitiesTableConfig.componentsData.edgeScope; + this.entityForm.patchValue({ + cloudEndpoint: window.location.origin + }); + super.ngOnInit(); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + isAssignedToCustomer(entity: EdgeInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; + } + + buildForm(entity: EdgeInfo): FormGroup { + const form = this.fb.group( + { + name: [entity ? entity.name : '', [Validators.required]], + type: [entity?.type ? entity.type : 'default', [Validators.required]], + label: [entity ? entity.label : ''], + cloudEndpoint: [null, [Validators.required]], + edgeLicenseKey: ['', [Validators.required]], + routingKey: this.fb.control({value: entity ? entity.routingKey : null, disabled: true}), + secret: this.fb.control({value: entity ? entity.secret : null, disabled: true}), + additionalInfo: this.fb.group( + { + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''] + } + ) + } + ); + this.generateRoutingKeyAndSecret(entity, form); + return form; + } + + updateForm(entity: EdgeInfo) { + this.entityForm.patchValue({ + name: entity.name, + type: entity.type, + label: entity.label, + cloudEndpoint: entity.cloudEndpoint ? entity.cloudEndpoint : window.location.origin, + edgeLicenseKey: entity.edgeLicenseKey, + routingKey: entity.routingKey, + secret: entity.secret, + additionalInfo: { + description: entity.additionalInfo ? entity.additionalInfo.description : '' + } + }); + this.generateRoutingKeyAndSecret(entity, this.entityForm); + } + + updateFormState() { + super.updateFormState(); + this.entityForm.get('routingKey').disable({emitEvent: false}); + this.entityForm.get('secret').disable({emitEvent: false}); + } + + onEdgeIdCopied($event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('edge.id-copied-message'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + + onEdgeInfoCopied(type: string) { + const message = type === 'key' ? 'edge.edge-key-copied-message' + : 'edge.edge-secret-copied-message'; + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant(message), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + + private generateRoutingKeyAndSecret(entity: EdgeInfo, form: FormGroup) { + if (entity && !entity.id) { + form.get('routingKey').patchValue(guid(), {emitEvent: false}); + form.get('secret').patchValue(generateSecret(20), {emitEvent: false}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge.module.ts b/ui-ngx/src/app/modules/home/pages/edge/edge.module.ts new file mode 100644 index 0000000000..1fc00a023b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.module.ts @@ -0,0 +1,42 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeDialogsModule } from '@home/dialogs/home-dialogs.module'; +import { HomeComponentsModule } from '@home/components/home-components.module'; +import { EdgeRoutingModule } from '@home/pages/edge/edge-routing.module'; +import { EdgeComponent } from '@modules/home/pages/edge/edge.component'; +import { EdgeTableHeaderComponent } from '@home/pages/edge/edge-table-header.component'; +import { EdgeTabsComponent } from '@home/pages/edge/edge-tabs.component'; + +@NgModule({ + declarations: [ + EdgeComponent, + EdgeTableHeaderComponent, + EdgeTabsComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeDialogsModule, + HomeComponentsModule, + EdgeRoutingModule + ] +}) + +export class EdgeModule { } diff --git a/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts new file mode 100644 index 0000000000..6cbeff0efe --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts @@ -0,0 +1,553 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; + +import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; +import { + CellActionDescriptor, + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig, + GroupActionDescriptor, + HeaderActionDescriptor +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { forkJoin, Observable, of } from 'rxjs'; +import { select, Store } from '@ngrx/store'; +import { selectAuthUser } from '@core/auth/auth.selectors'; +import { map, mergeMap, take, tap } from 'rxjs/operators'; +import { AppState } from '@core/core.state'; +import { Authority } from '@app/shared/models/authority.enum'; +import { CustomerService } from '@core/http/customer.service'; +import { Customer } from '@app/shared/models/customer.model'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { BroadcastService } from '@core/services/broadcast.service'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { + AssignToCustomerDialogComponent, + AssignToCustomerDialogData +} from '@modules/home/dialogs/assign-to-customer-dialog.component'; +import { + AddEntitiesToCustomerDialogComponent, + AddEntitiesToCustomerDialogData +} from '../../dialogs/add-entities-to-customer-dialog.component'; +import { HomeDialogsService } from '@home/dialogs/home-dialogs.service'; +import { Edge, EdgeInfo } from '@shared/models/edge.models'; +import { EdgeService } from '@core/http/edge.service'; +import { EdgeComponent } from '@home/pages/edge/edge.component'; +import { EdgeTableHeaderComponent } from '@home/pages/edge/edge-table-header.component'; +import { EdgeId } from '@shared/models/id/edge-id'; +import { EdgeTabsComponent } from '@home/pages/edge/edge-tabs.component'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; + +@Injectable() +export class EdgesTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + private customerId: string; + + constructor(private store: Store, + private broadcast: BroadcastService, + private edgeService: EdgeService, + private customerService: CustomerService, + private dialogService: DialogService, + private homeDialogs: HomeDialogsService, + private translate: TranslateService, + private datePipe: DatePipe, + private router: Router, + private dialog: MatDialog) { + + this.config.entityType = EntityType.EDGE; + this.config.entityComponent = EdgeComponent; + this.config.entityTabsComponent = EdgeTabsComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.EDGE); + this.config.entityResources = entityTypeResources.get(EntityType.EDGE); + + this.config.deleteEntityTitle = edge => this.translate.instant('edge.delete-edge-title', {edgeName: edge.name}); + this.config.deleteEntityContent = () => this.translate.instant('edge.delete-edge-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('edge.delete-edges-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('edge.delete-edges-text'); + + this.config.loadEntity = id => this.edgeService.getEdgeInfo(id.id); + this.config.saveEntity = edge => { + return this.edgeService.saveEdge(edge).pipe( + tap(() => { + this.broadcast.broadcast('edgeSaved'); + }), + mergeMap((savedEdge) => this.edgeService.getEdgeInfo(savedEdge.id.id) + )); + }; + this.config.onEntityAction = action => this.onEdgeAction(action); + this.config.detailsReadonly = () => this.config.componentsData.edgeScope === 'customer_user'; + this.config.headerComponent = EdgeTableHeaderComponent; + } + + resolve(route: ActivatedRouteSnapshot): Observable> { + const routeParams = route.params; + this.config.componentsData = { + edgeScope: route.data.edgesType, + edgeType: '' + }; + this.customerId = routeParams.customerId; + return this.store.pipe(select(selectAuthUser), take(1)).pipe( + tap((authUser) => { + if (authUser.authority === Authority.CUSTOMER_USER) { + this.config.componentsData.edgeScope = 'customer_user'; + this.customerId = authUser.customerId; + } + }), + mergeMap(() => + this.customerId ? this.customerService.getCustomer(this.customerId) : of(null as Customer) + ), + map((parentCustomer) => { + if (parentCustomer) { + if (parentCustomer.additionalInfo && parentCustomer.additionalInfo.isPublic) { + this.config.tableTitle = this.translate.instant('customer.public-edges'); + } else { + this.config.tableTitle = parentCustomer.title + ': ' + this.translate.instant('edge.edge-instances'); + } + } else { + this.config.tableTitle = this.translate.instant('edge.edge-instances'); + } + this.config.columns = this.configureColumns(this.config.componentsData.edgeScope); + this.configureEntityFunctions(this.config.componentsData.edgeScope); + this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.edgeScope); + this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.edgeScope); + this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.edgeScope); + this.config.addEnabled = this.config.componentsData.edgeScope !== 'customer_user'; + this.config.entitiesDeleteEnabled = this.config.componentsData.edgeScope === 'tenant'; + this.config.deleteEnabled = () => this.config.componentsData.edgeScope === 'tenant'; + return this.config; + }) + ); + } + + configureColumns(edgeScope: string): Array> { + const columns: Array> = [ + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('name', 'edge.name', '25%'), + new EntityTableColumn('type', 'edge.edge-type', '25%'), + new EntityTableColumn('label', 'edge.label', '25%') + ]; + if (edgeScope === 'tenant') { + columns.push( + new EntityTableColumn('customerTitle', 'customer.customer', '25%'), + new EntityTableColumn('customerIsPublic', 'edge.public', '60px', + entity => { + return checkBoxCell(entity.customerIsPublic); + }, () => ({}), false) + ); + } + return columns; + } + + configureEntityFunctions(edgeScope: string): void { + if (edgeScope === 'tenant') { + this.config.entitiesFetchFunction = pageLink => + this.edgeService.getTenantEdgeInfos(pageLink, this.config.componentsData.edgeType); + this.config.deleteEntity = id => this.edgeService.deleteEdge(id.id); + } + if (edgeScope === 'customer') { + this.config.entitiesFetchFunction = pageLink => + this.edgeService.getCustomerEdgeInfos(this.customerId, pageLink, this.config.componentsData.edgeType); + this.config.deleteEntity = id => this.edgeService.unassignEdgeFromCustomer(id.id); + } + if (edgeScope === 'customer_user') { + this.config.entitiesFetchFunction = pageLink => + this.edgeService.getCustomerEdgeInfos(this.customerId, pageLink, this.config.componentsData.edgeType); + this.config.deleteEntity = id => this.edgeService.unassignEdgeFromCustomer(id.id); + } + } + + configureCellActions(edgeScope: string): Array> { + const actions: Array> = []; + if (edgeScope === 'tenant') { + actions.push( + { + name: this.translate.instant('edge.make-public'), + icon: 'share', + isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID), + onAction: ($event, entity) => this.makePublic($event, entity) + }, + { + name: this.translate.instant('edge.assign-to-customer'), + icon: 'assignment_ind', + isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID), + onAction: ($event, entity) => this.assignToCustomer($event, [entity.id]) + }, + { + name: this.translate.instant('edge.unassign-from-customer'), + icon: 'assignment_return', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('edge.make-private'), + icon: 'reply', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('edge.manage-edge-assets'), + icon: 'domain', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.ASSET) + }, + { + name: this.translate.instant('edge.manage-edge-devices'), + icon: 'devices_other', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.DEVICE) + }, + { + name: this.translate.instant('edge.manage-edge-entity-views'), + icon: 'view_quilt', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.ENTITY_VIEW) + }, + { + name: this.translate.instant('edge.manage-edge-dashboards'), + icon: 'dashboard', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.DASHBOARD) + }, + { + name: this.translate.instant('edge.manage-edge-rulechains'), + icon: 'settings_ethernet', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.RULE_CHAIN) + } + ); + } + if (edgeScope === 'customer') { + actions.push( + { + name: this.translate.instant('edge.unassign-from-customer'), + icon: 'assignment_return', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + { + name: this.translate.instant('edge.make-private'), + icon: 'reply', + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic), + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) + }, + ); + } + if (edgeScope === 'customer_user') { + actions.push( + { + name: this.translate.instant('edge.manage-edge-assets'), + icon: 'domain', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.ASSET) + }, + { + name: this.translate.instant('edge.manage-edge-devices'), + icon: 'devices_other', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.DEVICE) + }, + { + name: this.translate.instant('edge.manage-edge-entity-views'), + icon: 'view_quilt', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.ENTITY_VIEW) + }, + { + name: this.translate.instant('edge.manage-edge-dashboards'), + icon: 'dashboard', + isEnabled: (entity) => true, + onAction: ($event, entity) => this.openEdgeEntitiesByType($event, entity, EntityType.DASHBOARD) + } + ); + } + return actions; + } + + configureGroupActions(edgeScope: string): Array> { + const actions: Array> = []; + if (edgeScope === 'tenant') { + actions.push( + { + name: this.translate.instant('edge.assign-edge-to-customer-text'), + icon: 'assignment_ind', + isEnabled: true, + onAction: ($event, entities) => this.assignToCustomer($event, entities.map((entity) => entity.id)) + } + ); + } + if (edgeScope === 'customer') { + actions.push( + { + name: this.translate.instant('edge.unassign-from-customer'), + icon: 'assignment_return', + isEnabled: true, + onAction: ($event, entities) => this.unassignEdgesFromCustomer($event, entities) + } + ); + } + return actions; + } + + configureAddActions(edgeScope: string): Array { + const actions: Array = []; + if (edgeScope === 'tenant') { + actions.push( + { + name: this.translate.instant('edge.add-edge-text'), + icon: 'insert_drive_file', + isEnabled: () => true, + onAction: ($event) => this.config.table.addEntity($event) + }, + { + name: this.translate.instant('edge.import'), + icon: 'file_upload', + isEnabled: () => true, + onAction: ($event) => this.importEdges($event) + } + ); + } + if (edgeScope === 'customer') { + actions.push( + { + name: this.translate.instant('edge.assign-new-edge'), + icon: 'add', + isEnabled: () => true, + onAction: ($event) => this.addEdgesToCustomer($event) + } + ); + } + return actions; + } + + importEdges($event: Event) { + this.homeDialogs.importEntities(EntityType.EDGE).subscribe((res) => { + if (res) { + this.broadcast.broadcast('edgeSaved'); + this.config.table.updateData(); + } + }); + } + + addEdgesToCustomer($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AddEntitiesToCustomerDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + customerId: this.customerId, + entityType: EntityType.EDGE + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.table.updateData(); + } + }); + } + + makePublic($event: Event, edge: Edge) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('edge.make-public-edge-title', {edgeName: edge.name}), + this.translate.instant('edge.make-public-edge-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.edgeService.makeEdgePublic(edge.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + openEdgeEntitiesByType($event: Event, edge: Edge, entityType: EntityType) { + if ($event) { + $event.stopPropagation(); + } + let suffix: string; + switch (entityType) { + case EntityType.DEVICE: + suffix = 'devices'; + break; + case EntityType.ASSET: + suffix = 'assets'; + break; + case EntityType.EDGE: + suffix = 'assets'; + break; + case EntityType.ENTITY_VIEW: + suffix = 'entityViews'; + break; + case EntityType.DASHBOARD: + suffix = 'dashboards'; + break; + case EntityType.RULE_CHAIN: + suffix = 'ruleChains'; + break; + } + this.router.navigateByUrl(`edges/${edge.id.id}/${suffix}`); + } + + assignToCustomer($event: Event, edgesIds: Array) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AssignToCustomerDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entityIds: edgesIds, + entityType: EntityType.EDGE + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.config.table.updateData(); + } + }); + } + + unassignFromCustomer($event: Event, edge: EdgeInfo) { + if ($event) { + $event.stopPropagation(); + } + const isPublic = edge.customerIsPublic; + let title; + let content; + if (isPublic) { + title = this.translate.instant('edge.make-private-edge-title', {edgeName: edge.name}); + content = this.translate.instant('edge.make-private-edge-text'); + } else { + title = this.translate.instant('edge.unassign-edge-title', {edgeName: edge.name}); + content = this.translate.instant('edge.unassign-edge-text'); + } + this.dialogService.confirm( + title, + content, + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.edgeService.unassignEdgeFromCustomer(edge.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + unassignEdgesFromCustomer($event: Event, edges: Array) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('edge.unassign-edges-title', {count: edges.length}), + this.translate.instant('edge.unassign-edges-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + const tasks: Observable[] = []; + edges.forEach( + (edge) => { + tasks.push(this.edgeService.unassignEdgeFromCustomer(edge.id.id)); + } + ); + forkJoin(tasks).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + syncEdge($event, edge) { + if ($event) { + $event.stopPropagation(); + } + this.edgeService.syncEdge(edge.id.id).subscribe( + () => { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('edge.sync-process-started-successfully'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + ); + } + + onEdgeAction(action: EntityAction): boolean { + switch (action.action) { + case 'makePublic': + this.makePublic(action.event, action.entity); + return true; + case 'assignToCustomer': + this.assignToCustomer(action.event, [action.entity.id]); + return true; + case 'unassignFromCustomer': + this.unassignFromCustomer(action.event, action.entity); + return true; + case 'openEdgeAssets': + this.openEdgeEntitiesByType(action.event, action.entity, EntityType.ASSET); + return true; + case 'openEdgeDevices': + this.openEdgeEntitiesByType(action.event, action.entity, EntityType.DEVICE); + return true; + case 'openEdgeEntityViews': + this.openEdgeEntitiesByType(action.event, action.entity, EntityType.ENTITY_VIEW); + return true; + case 'openEdgeDashboards': + this.openEdgeEntitiesByType(action.event, action.entity, EntityType.DASHBOARD); + return true; + case 'openEdgeRuleChains': + this.openEdgeEntitiesByType(action.event, action.entity, EntityType.RULE_CHAIN); + return true; + case 'syncEdge': + this.syncEdge(action.event, action.entity); + return true; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html index cf1a9bc936..5ce3b05569 100644 --- a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html @@ -34,6 +34,12 @@ [fxShow]="!isEdit && (entityViewScope === 'customer' || entityViewScope === 'tenant') && isAssignedToCustomer(entity)"> {{ (entity?.customerIsPublic ? 'entity-view.make-private' : 'entity-view.unassign-from-customer') | translate }} + + + + +
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts index dd7f62e493..3d8eae1e7a 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts @@ -31,6 +31,8 @@ import { EntityTableConfig } from '@home/models/entity/entities-table-config.mod }) export class RuleChainComponent extends EntityComponent { + ruleChainScope: 'tenant' | 'edges' | 'edge'; + constructor(protected store: Store, protected translate: TranslateService, @Inject('entity') protected entityValue: RuleChain, @@ -39,6 +41,11 @@ export class RuleChainComponent extends EntityComponent { super(store, fb, entityValue, entitiesTableConfigValue); } + ngOnInit() { + this.ruleChainScope = this.entitiesTableConfig.componentsData.ruleChainScope; + super.ngOnInit(); + } + hideDelete() { if (this.entitiesTableConfig) { return !this.entitiesTableConfig.deleteEnabled(this.entity); @@ -78,4 +85,30 @@ export class RuleChainComponent extends EntityComponent { horizontalPosition: 'right' })); } + + isEdgeRootRuleChain() { + if (this.entitiesTableConfig && this.entityValue) { + return this.entitiesTableConfig.componentsData.edge?.rootRuleChainId?.id == this.entityValue.id.id; + } else { + return false; + } + } + + isAutoAssignToEdgeRuleChain() { + if (this.entitiesTableConfig && this.entityValue) { + return !this.entityValue.root && + this.entitiesTableConfig.componentsData?.autoAssignToEdgeRuleChainIds?.includes(this.entityValue.id.id); + } else { + return false; + } + } + + isNotAutoAssignToEdgeRuleChain() { + if (this.entitiesTableConfig && this.entityValue) { + return !this.entityValue.root && + !this.entitiesTableConfig.componentsData?.autoAssignToEdgeRuleChainIds?.includes(this.entityValue.id.id); + } else { + return false; + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts index f2cf4b9da8..37a21cab2e 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts @@ -16,69 +16,202 @@ import { Injectable } from '@angular/core'; -import { Resolve, Router } from '@angular/router'; +import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; import { + CellActionDescriptor, checkBoxCell, DateEntityTableColumn, + EntityColumn, EntityTableColumn, - EntityTableConfig + EntityTableConfig, + GroupActionDescriptor, + HeaderActionDescriptor } from '@home/models/entity/entities-table-config.models'; import { TranslateService } from '@ngx-translate/core'; import { DatePipe } from '@angular/common'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityAction } from '@home/models/entity/entity-component.models'; -import { RuleChain } from '@shared/models/rule-chain.models'; +import { RuleChain, RuleChainType } from '@shared/models/rule-chain.models'; import { RuleChainService } from '@core/http/rule-chain.service'; import { RuleChainComponent } from '@modules/home/pages/rulechain/rulechain.component'; import { DialogService } from '@core/services/dialog.service'; import { RuleChainTabsComponent } from '@home/pages/rulechain/rulechain-tabs.component'; import { ImportExportService } from '@home/components/import-export/import-export.service'; import { ItemBufferService } from '@core/services/item-buffer.service'; +import { EdgeService } from '@core/http/edge.service'; +import { forkJoin, Observable } from 'rxjs'; +import { + AddEntitiesToEdgeDialogComponent, + AddEntitiesToEdgeDialogData +} from '@home/dialogs/add-entities-to-edge-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { isUndefined } from '@core/utils'; +import { PageLink } from '@shared/models/page/page-link'; +import { Edge } from '@shared/models/edge.models'; +import { mergeMap } from 'rxjs/operators'; +import { PageData } from '@shared/models/page/page-data'; @Injectable() export class RuleChainsTableConfigResolver implements Resolve> { private readonly config: EntityTableConfig = new EntityTableConfig(); + private edge: Edge; constructor(private ruleChainService: RuleChainService, private dialogService: DialogService, + private dialog: MatDialog, private importExport: ImportExportService, private itembuffer: ItemBufferService, + private edgeService: EdgeService, private translate: TranslateService, private datePipe: DatePipe, private router: Router) { - this.config.entityType = EntityType.RULE_CHAIN; this.config.entityComponent = RuleChainComponent; this.config.entityTabsComponent = RuleChainTabsComponent; this.config.entityTranslations = entityTypeTranslations.get(EntityType.RULE_CHAIN); this.config.entityResources = entityTypeResources.get(EntityType.RULE_CHAIN); - this.config.columns.push( + this.config.deleteEntityTitle = ruleChain => this.translate.instant('rulechain.delete-rulechain-title', + {ruleChainName: ruleChain.name}); + this.config.deleteEntityContent = () => this.translate.instant('rulechain.delete-rulechain-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('rulechain.delete-rulechains-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('rulechain.delete-rulechains-text'); + this.config.loadEntity = id => this.ruleChainService.getRuleChain(id.id); + this.config.saveEntity = ruleChain => this.saveRuleChain(ruleChain); + this.config.deleteEntity = id => this.ruleChainService.deleteRuleChain(id.id); + this.config.onEntityAction = action => this.onRuleChainAction(action); + } + + resolve(route: ActivatedRouteSnapshot): EntityTableConfig { + const edgeId = route.params?.edgeId; + const ruleChainScope = route.data?.ruleChainsType ? route.data?.ruleChainsType : 'tenant'; + this.config.componentsData = { + ruleChainScope, + edgeId + }; + this.config.columns = this.configureEntityTableColumns(ruleChainScope); + this.config.entitiesFetchFunction = this.configureEntityFunctions(ruleChainScope, edgeId); + this.config.groupActionDescriptors = this.configureGroupActions(ruleChainScope); + this.config.addActionDescriptors = this.configureAddActions(ruleChainScope); + this.config.cellActionDescriptors = this.configureCellActions(ruleChainScope); + if (ruleChainScope === 'tenant' || ruleChainScope === 'edges') { + this.config.entitySelectionEnabled = ruleChain => ruleChain && !ruleChain.root; + this.config.deleteEnabled = (ruleChain) => ruleChain && !ruleChain.root; + this.config.entitiesDeleteEnabled = true; + this.config.tableTitle = this.configureTableTitle(ruleChainScope, null); + } else if (ruleChainScope === 'edge') { + this.config.entitySelectionEnabled = ruleChain => this.config.componentsData.edge.rootRuleChainId.id !== ruleChain.id.id; + this.edgeService.getEdge(edgeId).subscribe(edge => { + this.config.componentsData.edge = edge; + this.config.tableTitle = this.configureTableTitle(ruleChainScope, edge); + }); + this.config.entitiesDeleteEnabled = false; + } + return this.config; + } + + configureEntityTableColumns(ruleChainScope: string): Array> { + const columns: Array> = []; + columns.push( new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), - new EntityTableColumn('name', 'rulechain.name', '100%'), - new EntityTableColumn('root', 'rulechain.root', '60px', - entity => { - return checkBoxCell(entity.root); - }), + new EntityTableColumn('name', 'rulechain.name', '100%') ); + if (ruleChainScope === 'tenant' || ruleChainScope === 'edge') { + columns.push( + new EntityTableColumn('root', 'rulechain.root', '60px', + entity => { + if (ruleChainScope === 'edge') { + return checkBoxCell((this.config.componentsData.edge.rootRuleChainId.id === entity.id.id)); + } else { + return checkBoxCell(entity.root); + } + }) + ); + } else if (ruleChainScope === 'edges') { + columns.push( + new EntityTableColumn('root', 'rulechain.edge-template-root', '60px', + entity => { + return checkBoxCell(entity.root); + }), + new EntityTableColumn('assignToEdge', 'rulechain.assign-to-edge', '60px', + entity => { + return checkBoxCell(this.isAutoAssignToEdgeRuleChain(entity)); + }) + ); + } + return columns; + } - this.config.addActionDescriptors.push( - { - name: this.translate.instant('rulechain.create-new-rulechain'), - icon: 'insert_drive_file', - isEnabled: () => true, - onAction: ($event) => this.config.table.addEntity($event) - }, - { - name: this.translate.instant('rulechain.import'), - icon: 'file_upload', - isEnabled: () => true, - onAction: ($event) => this.importRuleChain($event) - } - ); + configureAddActions(ruleChainScope: string): Array { + const actions: Array = []; + if (ruleChainScope === 'tenant' || ruleChainScope === 'edges') { + actions.push( + { + name: this.translate.instant('rulechain.create-new-rulechain'), + icon: 'insert_drive_file', + isEnabled: () => true, + onAction: ($event) => this.config.table.addEntity($event) + }, + { + name: this.translate.instant('rulechain.import'), + icon: 'file_upload', + isEnabled: () => true, + onAction: ($event) => this.importRuleChain($event) + } + ); + } + if (ruleChainScope === 'edge') { + actions.push( + { + name: this.translate.instant('rulechain.assign-new-rulechain'), + icon: 'add', + isEnabled: () => true, + onAction: ($event) => this.addRuleChainsToEdge($event) + } + ); + } + return actions; + } + + configureEntityFunctions(ruleChainScope: string, edgeId: string): (pageLink) => Observable> { + if (ruleChainScope === 'tenant') { + return pageLink => this.fetchRuleChains(pageLink); + } else if (ruleChainScope === 'edges') { + return pageLink => this.fetchEdgeRuleChains(pageLink); + } else if (ruleChainScope === 'edge') { + return pageLink => this.ruleChainService.getEdgeRuleChains(edgeId, pageLink); + } + } + + configureTableTitle(ruleChainScope: string, edge: Edge): string { + if (ruleChainScope === 'tenant') { + return this.translate.instant('rulechain.rulechains'); + } else if (ruleChainScope === 'edges') { + return this.translate.instant('edge.rulechain-templates'); + } else if (ruleChainScope === 'edge') { + return this.config.tableTitle = edge.name + ': ' + this.translate.instant('edge.rulechains'); + } + } - this.config.cellActionDescriptors.push( + configureGroupActions(ruleChainScope: string): Array> { + const actions: Array> = []; + if (ruleChainScope === 'edge') { + actions.push( + { + name: this.translate.instant('rulechain.unassign-rulechains'), + icon: 'assignment_return', + isEnabled: true, + onAction: ($event, entities) => this.unassignRuleChainsFromEdge($event, entities) + } + ); + } + return actions; + } + + configureCellActions(ruleChainScope: string): Array> { + const actions: Array> = []; + actions.push( { name: this.translate.instant('rulechain.open-rulechain'), icon: 'settings_ethernet', @@ -90,44 +223,72 @@ export class RuleChainsTableConfigResolver implements Resolve true, onAction: ($event, entity) => this.exportRuleChain($event, entity) - }, - { - name: this.translate.instant('rulechain.set-root'), - icon: 'flag', - isEnabled: (ruleChain) => !ruleChain.root, - onAction: ($event, entity) => this.setRootRuleChain($event, entity) } ); - - this.config.deleteEntityTitle = ruleChain => this.translate.instant('rulechain.delete-rulechain-title', - { ruleChainName: ruleChain.name }); - this.config.deleteEntityContent = () => this.translate.instant('rulechain.delete-rulechain-text'); - this.config.deleteEntitiesTitle = count => this.translate.instant('rulechain.delete-rulechains-title', {count}); - this.config.deleteEntitiesContent = () => this.translate.instant('rulechain.delete-rulechains-text'); - - this.config.entitiesFetchFunction = pageLink => this.ruleChainService.getRuleChains(pageLink); - this.config.loadEntity = id => this.ruleChainService.getRuleChain(id.id); - this.config.saveEntity = ruleChain => this.ruleChainService.saveRuleChain(ruleChain); - this.config.deleteEntity = id => this.ruleChainService.deleteRuleChain(id.id); - this.config.onEntityAction = action => this.onRuleChainAction(action); - this.config.deleteEnabled = (ruleChain) => ruleChain && !ruleChain.root; - this.config.entitySelectionEnabled = (ruleChain) => ruleChain && !ruleChain.root; - } - - resolve(): EntityTableConfig { - this.config.tableTitle = this.translate.instant('rulechain.rulechains'); - - return this.config; + if (ruleChainScope === 'tenant') { + actions.push( + { + name: this.translate.instant('rulechain.set-root'), + icon: 'flag', + isEnabled: (entity) => this.isNonRootRuleChain(entity), + onAction: ($event, entity) => this.setRootRuleChain($event, entity) + } + ); + } + if (ruleChainScope === 'edges') { + actions.push( + { + name: this.translate.instant('rulechain.set-edge-template-root-rulechain'), + icon: 'flag', + isEnabled: (entity) => this.isNonRootRuleChain(entity), + onAction: ($event, entity) => this.setEdgeTemplateRootRuleChain($event, entity) + }, + { + name: this.translate.instant('rulechain.set-auto-assign-to-edge'), + icon: 'bookmark_outline', + isEnabled: (entity) => this.isNotAutoAssignToEdgeRuleChain(entity), + onAction: ($event, entity) => this.setAutoAssignToEdgeRuleChain($event, entity) + }, + { + name: this.translate.instant('rulechain.unset-auto-assign-to-edge'), + icon: 'bookmark', + isEnabled: (entity) => this.isAutoAssignToEdgeRuleChain(entity), + onAction: ($event, entity) => this.unsetAutoAssignToEdgeRuleChain($event, entity) + } + ); + } + if (ruleChainScope === 'edge') { + actions.push( + { + name: this.translate.instant('rulechain.set-root'), + icon: 'flag', + isEnabled: (entity) => this.isNonRootRuleChain(entity), + onAction: ($event, entity) => this.setRootRuleChain($event, entity) + }, + { + name: this.translate.instant('edge.unassign-from-edge'), + icon: 'assignment_return', + isEnabled: (entity) => entity.id.id !== this.config.componentsData.edge.rootRuleChainId.id, + onAction: ($event, entity) => this.unassignFromEdge($event, entity) + } + ); + } + return actions; } importRuleChain($event: Event) { if ($event) { $event.stopPropagation(); } - this.importExport.importRuleChain().subscribe((ruleChainImport) => { + const expectedRuleChainType = this.config.componentsData.ruleChainScope === 'tenant' ? RuleChainType.CORE : RuleChainType.EDGE; + this.importExport.importRuleChain(expectedRuleChainType).subscribe((ruleChainImport) => { if (ruleChainImport) { this.itembuffer.storeRuleChainImport(ruleChainImport); - this.router.navigateByUrl(`ruleChains/ruleChain/import`); + if (this.config.componentsData.ruleChainScope === 'edges') { + this.router.navigateByUrl(`edges/ruleChains/ruleChain/import`); + } else { + this.router.navigateByUrl(`ruleChains/ruleChain/import`); + } } }); } @@ -136,7 +297,27 @@ export class RuleChainsTableConfigResolver implements Resolve { if (res) { - this.ruleChainService.setRootRuleChain(ruleChain.id.id).subscribe( - () => { - this.config.table.updateData(); - } - ); + if (this.config.componentsData.ruleChainScope === 'edge') { + this.ruleChainService.setEdgeRootRuleChain(this.config.componentsData.edgeId, ruleChain.id.id).subscribe( + (edge) => { + this.config.componentsData.edge = edge; + this.config.table.updateData(); + } + ); + } else { + this.ruleChainService.setRootRuleChain(ruleChain.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } } } ); @@ -179,8 +369,207 @@ export class RuleChainsTableConfigResolver implements Resolve { + if (res) { + this.ruleChainService.setEdgeTemplateRootRuleChain(ruleChain.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + addRuleChainsToEdge($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AddEntitiesToEdgeDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + edgeId: this.config.componentsData.edgeId, + entityType: EntityType.RULE_CHAIN + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.edgeService.findMissingToRelatedRuleChains(this.config.componentsData.edgeId).subscribe( + (missingRuleChains) => { + if (missingRuleChains && Object.keys(missingRuleChains).length > 0) { + const formattedMissingRuleChains: Array = new Array(); + for (const missingRuleChain of Object.keys(missingRuleChains)) { + const arrayOfMissingRuleChains = missingRuleChains[missingRuleChain]; + const tmp = '- \'' + missingRuleChain + '\': \'' + arrayOfMissingRuleChains.join('\', ') + '\''; + formattedMissingRuleChains.push(tmp); + } + const message = this.translate.instant('edge.missing-related-rule-chains-text', + {missingRuleChains: formattedMissingRuleChains.join('
')}); + this.dialogService.alert(this.translate.instant('edge.missing-related-rule-chains-title'), + message, this.translate.instant('action.close'), true).subscribe( + () => { + this.config.table.updateData(); + } + ); + } else { + this.config.table.updateData(); + } + } + ); + } + } + ); + } + + unassignFromEdge($event: Event, ruleChain: RuleChain) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('rulechain.unassign-rulechain-title', {ruleChainName: ruleChain.name}), + this.translate.instant('rulechain.unassign-rulechain-from-edge-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.ruleChainService.unassignRuleChainFromEdge(this.config.componentsData.edgeId, ruleChain.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + unassignRuleChainsFromEdge($event: Event, ruleChains: Array) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('rulechain.unassign-rulechains-from-edge-title', {count: ruleChains.length}), + this.translate.instant('rulechain.unassign-rulechains-from-edge-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + const tasks: Observable[] = []; + ruleChains.forEach( + (ruleChain) => { + tasks.push(this.ruleChainService.unassignRuleChainFromEdge(this.config.componentsData.edgeId, ruleChain.id.id)); + } + ); + forkJoin(tasks).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + setAutoAssignToEdgeRuleChain($event: Event, ruleChain: RuleChain) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('rulechain.set-auto-assign-to-edge-title', {ruleChainName: ruleChain.name}), + this.translate.instant('rulechain.set-auto-assign-to-edge-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.ruleChainService.setAutoAssignToEdgeRuleChain(ruleChain.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + unsetAutoAssignToEdgeRuleChain($event: Event, ruleChain: RuleChain) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('rulechain.unset-auto-assign-to-edge-title', {ruleChainName: ruleChain.name}), + this.translate.instant('rulechain.unset-auto-assign-to-edge-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.ruleChainService.unsetAutoAssignToEdgeRuleChain(ruleChain.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + isNonRootRuleChain(ruleChain: RuleChain) { + if (this.config.componentsData.ruleChainScope === 'edge') { + return this.config.componentsData.edge.rootRuleChainId && + this.config.componentsData.edge.rootRuleChainId.id !== ruleChain.id.id; + } + return !ruleChain.root; + } + + isAutoAssignToEdgeRuleChain(ruleChain) { + return !ruleChain.root && this.config.componentsData.autoAssignToEdgeRuleChainIds.includes(ruleChain.id.id); + } + + isNotAutoAssignToEdgeRuleChain(ruleChain) { + return !ruleChain.root && !this.config.componentsData.autoAssignToEdgeRuleChainIds.includes(ruleChain.id.id); + } + + fetchRuleChains(pageLink: PageLink) { + return this.ruleChainService.getRuleChains(pageLink, RuleChainType.CORE); + } + + fetchEdgeRuleChains(pageLink: PageLink) { + return this.ruleChainService.getAutoAssignToEdgeRuleChains().pipe( + mergeMap((ruleChains) => { + this.config.componentsData.autoAssignToEdgeRuleChainIds = []; + ruleChains.map(ruleChain => this.config.componentsData.autoAssignToEdgeRuleChainIds.push(ruleChain.id.id)); + return this.ruleChainService.getRuleChains(pageLink, RuleChainType.EDGE); + }) + ); + } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts index 1bc80e96db..6583d17852 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts @@ -19,6 +19,7 @@ import { Component, OnInit } from '@angular/core'; import { FcNodeComponent } from 'ngx-flowchart/dist/ngx-flowchart'; import { FcRuleNode, RuleNodeType } from '@shared/models/rule-node.models'; import { Router } from '@angular/router'; +import { RuleChainType } from '@app/shared/models/rule-chain.models'; @Component({ // tslint:disable-next-line:component-selector @@ -48,7 +49,12 @@ export class RuleNodeComponent extends FcNodeComponent implements OnInit { $event.stopPropagation(); } if (node.targetRuleChainId) { - this.router.navigateByUrl(`/ruleChains/${node.targetRuleChainId}`); + if (node.ruleChainType === RuleChainType.EDGE) { + this.router.navigateByUrl(`/edges/ruleChains/${node.targetRuleChainId}`); + } else { + this.router.navigateByUrl(`/ruleChains/${node.targetRuleChainId}`); + } + } } } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts index f81c5d8eaa..735b56a783 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts @@ -33,11 +33,12 @@ import { WidgetsBundleComponent } from '@modules/home/pages/widget/widgets-bundl import { NULL_UUID } from '@shared/models/id/has-uuid'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { getCurrentAuthUser } from '@app/core/auth/auth.selectors'; +import { getCurrentAuthState, getCurrentAuthUser } from '@app/core/auth/auth.selectors'; import { Authority } from '@shared/models/authority.enum'; import { DialogService } from '@core/services/dialog.service'; import { ImportExportService } from '@home/components/import-export/import-export.service'; -import { Direction } from "@shared/models/page/sort-order"; +import { Direction } from '@shared/models/page/sort-order'; +import { map } from 'rxjs/operators'; @Injectable() export class WidgetsBundlesTableConfigResolver implements Resolve> { @@ -106,7 +107,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve this.translate.instant('widgets-bundle.delete-widgets-bundles-title', {count}); this.config.deleteEntitiesContent = () => this.translate.instant('widgets-bundle.delete-widgets-bundles-text'); - this.config.entitiesFetchFunction = pageLink => this.widgetsService.getWidgetBundles(pageLink); + this.config.loadEntity = id => this.widgetsService.getWidgetsBundle(id.id); this.config.saveEntity = widgetsBundle => this.widgetsService.saveWidgetsBundle(widgetsBundle); this.config.deleteEntity = id => this.widgetsService.deleteWidgetsBundle(id.id); @@ -119,6 +120,15 @@ export class WidgetsBundlesTableConfigResolver implements Resolve this.isWidgetsBundleEditable(widgetsBundle, authUser.authority); this.config.entitySelectionEnabled = (widgetsBundle) => this.isWidgetsBundleEditable(widgetsBundle, authUser.authority); this.config.detailsReadonly = (widgetsBundle) => !this.isWidgetsBundleEditable(widgetsBundle, authUser.authority); + const authState = getCurrentAuthState(this.store); + this.config.entitiesFetchFunction = pageLink => this.widgetsService.getWidgetBundles(pageLink).pipe( + map((widgetBundles) => { + if (!authState.edgesSupportEnabled) { + widgetBundles.data = widgetBundles.data.filter(widgetBundle => widgetBundle.alias !== 'edge_widgets'); + } + return widgetBundles; + }) + ); return this.config; } diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index 245d09ad5e..b3bbb861e2 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -162,6 +162,11 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit this.noEntitiesMatchingText = 'device.no-devices-matching'; this.entityRequiredText = 'device.device-required'; break; + case EntityType.EDGE: + this.entityText = 'edge.edge'; + this.noEntitiesMatchingText = 'edge.no-edges-matching'; + this.entityRequiredText = 'edge.edge-required'; + break; case EntityType.ENTITY_VIEW: this.entityText = 'entity-view.entity-view'; this.noEntitiesMatchingText = 'entity-view.no-entity-views-matching'; diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts index 9d110f2897..72baf88cfe 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts @@ -60,6 +60,9 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV @Input() entityType: EntityType; + @Input() + subType: string; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -216,8 +219,9 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV fetchEntities(searchText?: string): Observable>> { this.searchText = searchText; + return this.entityService.getEntitiesByNameFilter(this.entityType, searchText, - 50, '', {ignoreLoading: true}).pipe( + 50, this.subType ? this.subType : '', {ignoreLoading: true}).pipe( map((data) => data ? data : [])); } diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts index 0ec55276cf..14d2fcb228 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts @@ -27,6 +27,7 @@ import { BroadcastService } from '@app/core/services/broadcast.service'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { AssetService } from '@core/http/asset.service'; import { EntityViewService } from '@core/http/entity-view.service'; +import { EdgeService } from '@core/http/edge.service'; @Component({ selector: 'tb-entity-subtype-autocomplete', @@ -82,6 +83,7 @@ export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor, public translate: TranslateService, private deviceService: DeviceService, private assetService: AssetService, + private edgeService: EdgeService, private entityViewService: EntityViewService, private fb: FormBuilder) { this.subTypeFormGroup = this.fb.group({ @@ -115,6 +117,14 @@ export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor, this.subTypes = null; }); break; + case EntityType.EDGE: + this.selectEntitySubtypeText = 'edge.select-edge-type'; + this.entitySubtypeText = 'edge.edge-type'; + this.entitySubtypeRequiredText = 'edge.edge-type-required'; + this.broadcastSubscription = this.broadcast.on('edgeSaved', () => { + this.subTypes = null; + }); + break; case EntityType.ENTITY_VIEW: this.selectEntitySubtypeText = 'entity-view.select-entity-view-type'; this.entitySubtypeText = 'entity-view.entity-view-type'; @@ -202,6 +212,9 @@ export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor, case EntityType.DEVICE: subTypesObservable = this.deviceService.getDeviceTypes({ignoreLoading: true}); break; + case EntityType.EDGE: + subTypesObservable = this.edgeService.getEdgeTypes({ignoreLoading: true}); + break; case EntityType.ENTITY_VIEW: subTypesObservable = this.entityViewService.getEntityViewTypes({ignoreLoading: true}); break; diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts index 8628f1edef..6dffa176cd 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts @@ -27,6 +27,7 @@ import { MatChipInputEvent, MatChipList } from '@angular/material/chips'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { AssetService } from '@core/http/asset.service'; import { DeviceService } from '@core/http/device.service'; +import { EdgeService } from '@core/http/edge.service'; import { EntityViewService } from '@core/http/entity-view.service'; import { BroadcastService } from '@core/services/broadcast.service'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; @@ -96,6 +97,7 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, public translate: TranslateService, private assetService: AssetService, private deviceService: DeviceService, + private edgeService: EdgeService, private entityViewService: EntityViewService, private fb: FormBuilder) { this.entitySubtypeListFormGroup = this.fb.group({ @@ -139,6 +141,16 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, this.entitySubtypes = null; }); break; + case EntityType.EDGE: + this.placeholder = this.required ? this.translate.instant('edge.enter-edge-type') + : this.translate.instant('edge.any-edge'); + this.secondaryPlaceholder = '+' + this.translate.instant('edge.edge-type'); + this.noSubtypesMathingText = 'edge.no-edge-types-matching'; + this.subtypeListEmptyText = 'edge.edge-type-list-empty'; + this.broadcastSubscription = this.broadcast.on('edgeSaved', () => { + this.entitySubtypes = null; + }); + break; case EntityType.ENTITY_VIEW: this.placeholder = this.required ? this.translate.instant('entity-view.enter-entity-view-type') : this.translate.instant('entity-view.any-entity-view'); @@ -260,6 +272,9 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, case EntityType.DEVICE: subTypesObservable = this.deviceService.getDeviceTypes({ignoreLoading: true}); break; + case EntityType.EDGE: + subTypesObservable = this.edgeService.getEdgeTypes({ignoreLoading: true}); + break; case EntityType.ENTITY_VIEW: subTypesObservable = this.entityViewService.getEntityViewTypes({ignoreLoading: true}); break; diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.ts index 7dc9eb1e80..c8a4f0dedf 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.ts @@ -25,6 +25,7 @@ import { DeviceService } from '@core/http/device.service'; import { EntitySubtype, EntityType } from '@app/shared/models/entity-type.models'; import { BroadcastService } from '@app/core/services/broadcast.service'; import { AssetService } from '@core/http/asset.service'; +import { EdgeService } from '@core/http/edge.service'; import { EntityViewService } from '@core/http/entity-view.service'; @Component({ @@ -78,6 +79,7 @@ export class EntitySubTypeSelectComponent implements ControlValueAccessor, OnIni public translate: TranslateService, private deviceService: DeviceService, private assetService: AssetService, + private edgeService: EdgeService, private entityViewService: EntityViewService, private fb: FormBuilder) { this.subTypeFormGroup = this.fb.group({ @@ -111,6 +113,14 @@ export class EntitySubTypeSelectComponent implements ControlValueAccessor, OnIni this.subTypesOptionsSubject.next(''); }); break; + case EntityType.EDGE: + this.entitySubtypeTitle = 'edge.edge-type'; + this.entitySubtypeRequiredText = 'edge.edge-type-required'; + this.broadcastSubscription = this.broadcast.on('edgeSaved',() => { + this.subTypes = null; + this.subTypesOptionsSubject.next(''); + }); + break; case EntityType.ENTITY_VIEW: this.entitySubtypeTitle = 'entity-view.entity-view-type'; this.entitySubtypeRequiredText = 'entity-view.entity-view-type-required'; @@ -208,6 +218,9 @@ export class EntitySubTypeSelectComponent implements ControlValueAccessor, OnIni case EntityType.DEVICE: this.subTypes = this.deviceService.getDeviceTypes({ignoreLoading: true}); break; + case EntityType.EDGE: + this.subTypes = this.edgeService.getEdgeTypes({ignoreLoading: true}); + break; case EntityType.ENTITY_VIEW: this.subTypes = this.entityViewService.getEntityViewTypes({ignoreLoading: true}); break; diff --git a/ui-ngx/src/app/shared/components/file-input.component.ts b/ui-ngx/src/app/shared/components/file-input.component.ts index d70837ac82..261ce1c166 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.ts +++ b/ui-ngx/src/app/shared/components/file-input.component.ts @@ -73,9 +73,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, contentConvertFunction: (content: string) => any; private requiredValue: boolean; + get required(): boolean { return this.requiredValue; } + @Input() set required(value: boolean) { const newVal = coerceBooleanProperty(value); @@ -85,9 +87,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, } private requiredAsErrorValue: boolean; + get requiredAsError(): boolean { return this.requiredAsErrorValue; } + @Input() set requiredAsError(value: boolean) { const newVal = coerceBooleanProperty(value); 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 a5d45d16c7..78652faa18 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 @@ -22,7 +22,7 @@ import { ActionNotificationHide, ActionNotificationShow } from '@core/notificati import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; -import { guid, isUndefined } from '@core/utils'; +import { guid, isDefinedAndNotNull, isLiteralObject, isUndefined } from '@core/utils'; import { ResizeObserver } from '@juggle/resize-observer'; import { getAce } from '@shared/models/ace/ace.models'; @@ -230,8 +230,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va this.contentValue = ''; this.objectValid = false; try { - - if (this.modelValue) { + if (isDefinedAndNotNull(this.modelValue)) { this.contentValue = JSON.stringify(this.modelValue, isUndefined(this.sort) ? undefined : (key, objectValue) => { return this.sort(key, objectValue); @@ -260,6 +259,9 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va if (this.contentValue && this.contentValue.length > 0) { try { data = JSON.parse(this.contentValue); + if (!isLiteralObject(data)) { + throw new TypeError(`Value is not a valid JSON`); + } this.objectValid = true; this.validationError = ''; } catch (ex) { diff --git a/ui-ngx/src/app/shared/components/time/datetime-period.component.html b/ui-ngx/src/app/shared/components/time/datetime-period.component.html index 96b2e3d526..96bffe8696 100644 --- a/ui-ngx/src/app/shared/components/time/datetime-period.component.html +++ b/ui-ngx/src/app/shared/components/time/datetime-period.component.html @@ -15,8 +15,9 @@ limitations under the License. --> -
-
+
+
datetime.date-from @@ -30,7 +31,8 @@
-
+
datetime.date-to diff --git a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts index 3eab8c428d..3f9c712556 100644 --- a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts +++ b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts @@ -21,7 +21,6 @@ import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/mode @Component({ selector: 'tb-quick-time-interval', templateUrl: './quick-time-interval.component.html', - styleUrls: ['./quick-time-interval.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html index 57c596bbc2..f947c51d3e 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html @@ -51,7 +51,7 @@ [fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL" [required]="timewindow.selectedTab === timewindowTypes.REALTIME && timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL" - style="padding-top: 8px; min-width: 364px"> + style="padding-top: 8px">
@@ -75,6 +75,7 @@ timewindow.time-period timewindow.interval + style="padding-top: 8px">
@@ -134,21 +137,23 @@ (ngModelChange)="onHideAggIntervalChanged()">
-
- aggregation.limit - - - - - +
+ +
+ + + + + +
diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss index 6b25d0b1a7..fd8cb0f331 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import "../../../../scss/constants"; + :host { width: 100%; height: 100%; @@ -40,21 +42,29 @@ } .limit-slider-container { - >:first-child { - margin-right: 16px; - } - >:last-child { + .limit-slider-value { margin-left: 16px; - } - >:first-child, >:last-child { min-width: 25px; - max-width: 42px; + max-width: 80px; } mat-form-field input[type=number] { text-align: center; } } + @media #{$mat-gt-sm} { + .history-time-input { + min-width: 364px; + } + .limit-slider-container { + > label { + margin-right: 16px; + width: min-content; + max-width: 40%; + } + } + } + } :host ::ng-deep { diff --git a/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts index 9e5d986c5f..9a538b6560 100644 --- a/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts +++ b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts @@ -17,7 +17,7 @@ import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Observable } from 'rxjs'; -import { share, tap } from 'rxjs/operators'; +import { map, share, tap } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; @@ -25,6 +25,7 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { WidgetService } from '@core/http/widget.service'; import { isDefined } from '@core/utils'; import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; @Component({ selector: 'tb-widgets-bundle-select', @@ -81,6 +82,13 @@ export class WidgetsBundleSelectComponent implements ControlValueAccessor, OnIni ngOnInit() { this.widgetsBundles$ = this.getWidgetsBundles().pipe( + map((widgetsBundles) => { + const authState = getCurrentAuthState(this.store); + if (!authState.edgesSupportEnabled) { + widgetsBundles = widgetsBundles.filter(widgetsBundle => widgetsBundle.alias !== 'edge_widgets'); + } + return widgetsBundles; + }), tap((widgetsBundles) => { this.widgetsBundles = widgetsBundles; if (this.selectFirstBundle) { diff --git a/ui-ngx/src/app/shared/models/ace/service-completion.models.ts b/ui-ngx/src/app/shared/models/ace/service-completion.models.ts index 690b3ae962..62f67836b2 100644 --- a/ui-ngx/src/app/shared/models/ace/service-completion.models.ts +++ b/ui-ngx/src/app/shared/models/ace/service-completion.models.ts @@ -18,13 +18,13 @@ import { FunctionArg, FunctionArgType, TbEditorCompletions } from '@shared/model export const entityIdHref = 'EntityId'; -export const baseDataHref = 'Base data'; +export const baseDataHref = 'Base data'; export const alarmDataHref = 'Alarm data'; -export const alarmDataQueryHref = 'Alarm data query'; +export const alarmDataQueryHref = 'Alarm data query'; -export const attributeScopeHref = 'Attribute scope'; +export const attributeScopeHref = 'Attribute scope'; export const entityTypeHref = 'EntityType'; @@ -32,15 +32,15 @@ export const pageDataHref = 'DeviceInfo'; -export const assetInfoHref = 'AssetInfo'; +export const assetInfoHref = 'AssetInfo'; export const entityViewInfoHref = 'EntityViewInfo'; export const entityRelationsQueryHref = 'EntityRelationsQuery'; -export const entityRelationInfoHref = 'EntityRelationInfo'; +export const entityRelationInfoHref = 'EntityRelationInfo'; -export const dashboardInfoHref = 'DashboardInfo'; +export const dashboardInfoHref = 'DashboardInfo'; export const deviceHref = 'Device'; @@ -50,7 +50,7 @@ export const entityViewHref = 'Entity relation'; -export const dashboardHref = 'Dashboard'; +export const dashboardHref = 'Dashboard'; export const customerHref = 'Customer'; @@ -62,7 +62,7 @@ export const aggregationTypeHref = 'Data Sort Order'; -export const userHref = 'User'; +export const userHref = 'User'; export const entityDataHref = 'Entity data'; diff --git a/ui-ngx/src/app/shared/models/alias.models.ts b/ui-ngx/src/app/shared/models/alias.models.ts index 5c02089b04..1f900d29a1 100644 --- a/ui-ngx/src/app/shared/models/alias.models.ts +++ b/ui-ngx/src/app/shared/models/alias.models.ts @@ -27,11 +27,13 @@ export enum AliasFilterType { stateEntity = 'stateEntity', assetType = 'assetType', deviceType = 'deviceType', + edgeType = 'edgeType', entityViewType = 'entityViewType', apiUsageState = 'apiUsageState', relationsQuery = 'relationsQuery', assetSearchQuery = 'assetSearchQuery', deviceSearchQuery = 'deviceSearchQuery', + edgeSearchQuery = 'edgeSearchQuery', entityViewSearchQuery = 'entityViewSearchQuery' } @@ -44,11 +46,13 @@ export const aliasFilterTypeTranslationMap = new Map( [ AliasFilterType.stateEntity, 'alias.filter-type-state-entity' ], [ AliasFilterType.assetType, 'alias.filter-type-asset-type' ], [ AliasFilterType.deviceType, 'alias.filter-type-device-type' ], + [ AliasFilterType.edgeType, 'alias.filter-type-edge-type' ], [ AliasFilterType.entityViewType, 'alias.filter-type-entity-view-type' ], [ AliasFilterType.apiUsageState, 'alias.filter-type-apiUsageState' ], [ AliasFilterType.relationsQuery, 'alias.filter-type-relations-query' ], [ AliasFilterType.assetSearchQuery, 'alias.filter-type-asset-search-query' ], [ AliasFilterType.deviceSearchQuery, 'alias.filter-type-device-search-query' ], + [ AliasFilterType.edgeSearchQuery, 'alias.filter-type-edge-search-query' ], [ AliasFilterType.entityViewSearchQuery, 'alias.filter-type-entity-view-search-query' ] ] ); @@ -86,6 +90,11 @@ export interface DeviceTypeFilter { deviceNameFilter?: string; } +export interface EdgeTypeFilter { + edgeType?: string; + edgeNameFilter?: string; +} + export interface EntityViewFilter { entityViewType?: string; entityViewNameFilter?: string; @@ -126,6 +135,10 @@ export interface DeviceSearchQueryFilter extends EntitySearchQueryFilter { deviceTypes?: string[]; } +export interface EdgeSearchQueryFilter extends EntitySearchQueryFilter { + edgeTypes?: string[]; +} + export interface EntityViewSearchQueryFilter extends EntitySearchQueryFilter { entityViewTypes?: string[]; } @@ -138,12 +151,14 @@ export type EntityFilters = StateEntityFilter & AssetTypeFilter & DeviceTypeFilter & + EdgeTypeFilter & EntityViewFilter & RelationsQueryFilter & AssetSearchQueryFilter & DeviceSearchQueryFilter & EntityViewSearchQueryFilter & - EntitySearchQueryFilter; + EntitySearchQueryFilter & + EdgeSearchQueryFilter; export interface EntityAliasFilter extends EntityFilters { type?: AliasFilterType; diff --git a/ui-ngx/src/app/shared/models/audit-log.models.ts b/ui-ngx/src/app/shared/models/audit-log.models.ts index a795312ff4..7c90411aba 100644 --- a/ui-ngx/src/app/shared/models/audit-log.models.ts +++ b/ui-ngx/src/app/shared/models/audit-log.models.ts @@ -55,7 +55,9 @@ export enum ActionType { PROVISION_SUCCESS = 'PROVISION_SUCCESS', PROVISION_FAILURE = 'PROVISION_FAILURE', TIMESERIES_UPDATED = 'TIMESERIES_UPDATED', - TIMESERIES_DELETED = 'TIMESERIES_DELETED' + TIMESERIES_DELETED = 'TIMESERIES_DELETED', + ASSIGNED_TO_EDGE = 'ASSIGNED_TO_EDGE', + UNASSIGNED_FROM_EDGE = 'UNASSIGNED_FROM_EDGE' } export enum ActionStatus { @@ -91,7 +93,9 @@ export const actionTypeTranslations = new Map( [ActionType.PROVISION_SUCCESS, 'audit-log.type-provision-success'], [ActionType.PROVISION_FAILURE, 'audit-log.type-provision-failure'], [ActionType.TIMESERIES_UPDATED, 'audit-log.type-timeseries-updated'], - [ActionType.TIMESERIES_DELETED, 'audit-log.type-timeseries-deleted'] + [ActionType.TIMESERIES_DELETED, 'audit-log.type-timeseries-deleted'], + [ActionType.ASSIGNED_TO_EDGE, 'audit-log.type-assigned-to-edge'], + [ActionType.UNASSIGNED_FROM_EDGE, 'audit-log.type-unassigned-from-edge'] ] ); diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 7b72b6a6e4..44327e1c2c 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -115,6 +115,7 @@ export const HelpLinks = { users: helpBaseUrl + '/docs/user-guide/ui/users', devices: helpBaseUrl + '/docs/user-guide/ui/devices', deviceProfiles: helpBaseUrl + '/docs/user-guide/ui/device-profiles', + edges: helpBaseUrl + '/docs/user-guide/ui/edges', assets: helpBaseUrl + '/docs/user-guide/ui/assets', entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views', entitiesImport: helpBaseUrl + '/docs/user-guide/bulk-provisioning', @@ -127,6 +128,8 @@ export const HelpLinks = { widgetsConfigRpc: helpBaseUrl + '/docs/user-guide/ui/dashboards#rpc', widgetsConfigAlarm: helpBaseUrl + '/docs/user-guide/ui/dashboards#alarm', widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static', + ruleNodePushToCloud: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#push-to-cloud', + ruleNodePushToEdge: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#push-to-edge' } }; diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index a92bc3d672..6a90101923 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -86,6 +86,8 @@ export interface DashboardSettings { showDashboardsSelect?: boolean; showEntitiesSelect?: boolean; showFilters?: boolean; + showDashboardLogo?: boolean; + dashboardLogoUrl?: string; showDashboardTimewindow?: boolean; showDashboardExport?: boolean; toolbarAlwaysOpen?: boolean; diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index 904929c2b1..6067eeee38 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -141,6 +141,24 @@ export const defaultAttributesSchema = ' string serialNumber = 2;\n' + '}'; +export const defaultRpcRequestSchema = + 'syntax ="proto3";\n' + + 'package rpc;\n' + + '\n' + + 'message RpcRequestMsg {\n' + + ' string method = 1;\n' + + ' int32 requestId = 2;\n' + + ' string params = 3;\n' + + '}'; + +export const defaultRpcResponseSchema = + 'syntax ="proto3";\n' + + 'package rpc;\n' + + '\n' + + 'message RpcResponseMsg {\n' + + ' string payload = 1;\n' + + '}'; + export const coapDeviceTypeTranslationMap = new Map( [ [CoapTransportDeviceType.DEFAULT, 'device-profile.coap-device-type-default'], @@ -166,14 +184,14 @@ export const deviceTransportTypeConfigurationInfoMap = new Map { + tenantId?: TenantId; + customerId?: CustomerId; + name: string; + type: string; + secret: string; + routingKey: string; + cloudEndpoint: string; + edgeLicenseKey: string; + label?: string; + additionalInfo?: any; + rootRuleChainId?: RuleChainId; +} + +export interface EdgeInfo extends Edge { + customerTitle: string; + customerIsPublic: boolean; +} + +export interface EdgeSearchQuery extends EntitySearchQuery { + edgeTypes: Array; +} + +export enum EdgeEventType { + DASHBOARD = "DASHBOARD", + ASSET = "ASSET", + DEVICE = "DEVICE", + DEVICE_PROFILE = "DEVICE_PROFILE", + ENTITY_VIEW = "ENTITY_VIEW", + ALARM = "ALARM", + RULE_CHAIN = "RULE_CHAIN", + RULE_CHAIN_METADATA = "RULE_CHAIN_METADATA", + EDGE = "EDGE", + USER = "USER", + CUSTOMER = "CUSTOMER", + RELATION = "RELATION", + TENANT = "TENANT", + WIDGETS_BUNDLE = "WIDGETS_BUNDLE", + WIDGET_TYPE = "WIDGET_TYPE", + ADMIN_SETTINGS = "ADMIN_SETTINGS" +} + +export enum EdgeEventActionType { + ADDED = "ADDED", + DELETED = "DELETED", + UPDATED = "UPDATED", + POST_ATTRIBUTES = "POST_ATTRIBUTES", + ATTRIBUTES_UPDATED = "ATTRIBUTES_UPDATED", + ATTRIBUTES_DELETED = "ATTRIBUTES_DELETED", + TIMESERIES_UPDATED = "TIMESERIES_UPDATED", + CREDENTIALS_UPDATED = "CREDENTIALS_UPDATED", + ASSIGNED_TO_CUSTOMER = "ASSIGNED_TO_CUSTOMER", + UNASSIGNED_FROM_CUSTOMER = "UNASSIGNED_FROM_CUSTOMER", + RELATION_ADD_OR_UPDATE = "RELATION_ADD_OR_UPDATE", + RELATION_DELETED = "RELATION_DELETED", + RPC_CALL = "RPC_CALL", + ALARM_ACK = "ALARM_ACK", + ALARM_CLEAR = "ALARM_CLEAR", + ASSIGNED_TO_EDGE = "ASSIGNED_TO_EDGE", + UNASSIGNED_FROM_EDGE = "UNASSIGNED_FROM_EDGE", + CREDENTIALS_REQUEST = "CREDENTIALS_REQUEST", + ENTITY_MERGE_REQUEST = "ENTITY_MERGE_REQUEST" +} + +export enum EdgeEventStatus { + DEPLOYED = "DEPLOYED", + PENDING = "PENDING" +} + +export const edgeEventTypeTranslations = new Map( + [ + [EdgeEventType.DASHBOARD, 'edge-event.type-dashboard'], + [EdgeEventType.ASSET, 'edge-event.type-asset'], + [EdgeEventType.DEVICE, 'edge-event.type-device'], + [EdgeEventType.DEVICE_PROFILE, 'edge-event.type-device-profile'], + [EdgeEventType.ENTITY_VIEW, 'edge-event.type-entity-view'], + [EdgeEventType.ALARM, 'edge-event.type-alarm'], + [EdgeEventType.RULE_CHAIN, 'edge-event.type-rule-chain'], + [EdgeEventType.RULE_CHAIN_METADATA, 'edge-event.type-rule-chain-metadata'], + [EdgeEventType.EDGE, 'edge-event.type-edge'], + [EdgeEventType.USER, 'edge-event.type-user'], + [EdgeEventType.CUSTOMER, 'edge-event.type-customer'], + [EdgeEventType.RELATION, 'edge-event.type-relation'], + [EdgeEventType.TENANT, 'edge-event.type-tenant'], + [EdgeEventType.WIDGETS_BUNDLE, 'edge-event.type-widgets-bundle'], + [EdgeEventType.WIDGET_TYPE, 'edge-event.type-widgets-type'], + [EdgeEventType.ADMIN_SETTINGS, 'edge-event.type-admin-settings'] + ] +); + +export const edgeEventActionTypeTranslations = new Map( + [ + [EdgeEventActionType.ADDED, 'edge-event.action-type-added'], + [EdgeEventActionType.DELETED, 'edge-event.action-type-deleted'], + [EdgeEventActionType.UPDATED, 'edge-event.action-type-updated'], + [EdgeEventActionType.POST_ATTRIBUTES, 'edge-event.action-type-post-attributes'], + [EdgeEventActionType.ATTRIBUTES_UPDATED, 'edge-event.action-type-attributes-updated'], + [EdgeEventActionType.ATTRIBUTES_DELETED, 'edge-event.action-type-attributes-deleted'], + [EdgeEventActionType.TIMESERIES_UPDATED, 'edge-event.action-type-timeseries-updated'], + [EdgeEventActionType.CREDENTIALS_UPDATED, 'edge-event.action-type-credentials-updated'], + [EdgeEventActionType.ASSIGNED_TO_CUSTOMER, 'edge-event.action-type-assigned-to-customer'], + [EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER, 'edge-event.action-type-unassigned-from-customer'], + [EdgeEventActionType.RELATION_ADD_OR_UPDATE, 'edge-event.action-type-relation-add-or-update'], + [EdgeEventActionType.RELATION_DELETED, 'edge-event.action-type-relation-deleted'], + [EdgeEventActionType.RPC_CALL, 'edge-event.action-type-rpc-call'], + [EdgeEventActionType.ALARM_ACK, 'edge-event.action-type-alarm-ack'], + [EdgeEventActionType.ALARM_CLEAR, 'edge-event.action-type-alarm-clear'], + [EdgeEventActionType.ASSIGNED_TO_EDGE, 'edge-event.action-type-assigned-to-edge'], + [EdgeEventActionType.UNASSIGNED_FROM_EDGE, 'edge-event.action-type-unassigned-from-edge'], + [EdgeEventActionType.CREDENTIALS_REQUEST, 'edge-event.action-type-credentials-request'], + [EdgeEventActionType.ENTITY_MERGE_REQUEST, 'edge-event.action-type-entity-merge-request'] + ] +); + +export const edgeEventStatusColor = new Map( + [ + [EdgeEventStatus.DEPLOYED, '#000000'], + [EdgeEventStatus.PENDING, '#9e9e9e'] + ] +); + +export interface EdgeEventBody extends BaseEventBody { + type: string; + action: string; + entityId: string; +} + +export interface EdgeEvent extends BaseData { + tenantId: TenantId; + entityId: string; + edgeId: EdgeId; + action: EdgeEventActionType; + type: EdgeEventType; + uid: string; + body: string; +} diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 2ee8b226cc..892d2a1244 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -29,6 +29,7 @@ export enum EntityType { ALARM = 'ALARM', RULE_CHAIN = 'RULE_CHAIN', RULE_NODE = 'RULE_NODE', + EDGE = 'EDGE', ENTITY_VIEW = 'ENTITY_VIEW', WIDGETS_BUNDLE = 'WIDGETS_BUNDLE', WIDGET_TYPE = 'WIDGET_TYPE', @@ -161,6 +162,20 @@ export const entityTypeTranslations = new Map { uid: string; body: EventBody; } + +export interface BaseFilterEventBody { + server?: string; +} + +export interface ErrorFilterEventBody extends BaseFilterEventBody { + method?: string; +} + +export interface LcFilterEventEventBody extends BaseFilterEventBody { + method?: string; + status?: string; + isError?: boolean; +} + +export interface StatsFilterEventBody extends BaseFilterEventBody { + messagesProcessed?: string; + errorsOccurred?: string; +} + +export interface DebugFilterRuleNodeEventBody extends BaseFilterEventBody { + msgDirectionType?: string; + entityId?: string; + entityName?: EntityType; + msgId?: string; + msgType?: string; + relationType?: string; + dataSearch?: string; + metadataSearch?: string; + isError?: boolean; +} + +export type FilterEventBody = ErrorFilterEventBody & LcFilterEventEventBody & StatsFilterEventBody & DebugFilterRuleNodeEventBody; diff --git a/ui-ngx/src/app/shared/models/id/edge-id.ts b/ui-ngx/src/app/shared/models/id/edge-id.ts new file mode 100644 index 0000000000..15f3fb3065 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/edge-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class EdgeId implements EntityId { + entityType = EntityType.EDGE; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/public-api.ts b/ui-ngx/src/app/shared/models/id/public-api.ts index e3093f263c..534b50b0f5 100644 --- a/ui-ngx/src/app/shared/models/id/public-api.ts +++ b/ui-ngx/src/app/shared/models/id/public-api.ts @@ -33,3 +33,4 @@ export * from './tenant-profile-id'; export * from './user-id'; export * from './widget-type-id'; export * from './widgets-bundle-id'; +export * from './edge-id'; diff --git a/ui-ngx/src/app/shared/models/public-api.ts b/ui-ngx/src/app/shared/models/public-api.ts index c551d6df8a..fddcd55033 100644 --- a/ui-ngx/src/app/shared/models/public-api.ts +++ b/ui-ngx/src/app/shared/models/public-api.ts @@ -29,6 +29,7 @@ export * from './contact-based.model'; export * from './customer.model'; export * from './dashboard.models'; export * from './device.models'; +export * from './edge.models'; export * from './entity.models'; export * from './entity-type.models'; export * from './entity-view.models'; diff --git a/ui-ngx/src/app/shared/models/resource.models.ts b/ui-ngx/src/app/shared/models/resource.models.ts index 154f3c65f9..082d09ac24 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -45,7 +45,7 @@ export const ResourceTypeTranslationMap = new Map( [ResourceType.LWM2M_MODEL, 'LWM2M model'], [ResourceType.PKCS_12, 'PKCS #12'], [ResourceType.JKS, 'JKS'] - ] + ] ); export interface ResourceInfo extends BaseData { diff --git a/ui-ngx/src/app/shared/models/rule-chain.models.ts b/ui-ngx/src/app/shared/models/rule-chain.models.ts index dd45739794..925ad0b432 100644 --- a/ui-ngx/src/app/shared/models/rule-chain.models.ts +++ b/ui-ngx/src/app/shared/models/rule-chain.models.ts @@ -27,8 +27,10 @@ export interface RuleChain extends BaseData { firstRuleNodeId: RuleNodeId; root: boolean; debugMode: boolean; + type: string; configuration?: any; additionalInfo?: any; + isDefault?: boolean; } export interface RuleChainMetaData { @@ -110,3 +112,8 @@ export const inputNodeComponent: RuleNodeComponentDescriptor = { name: 'Input', clazz: 'tb.internal.Input' }; + +export enum RuleChainType { + CORE = 'CORE', + EDGE = 'EDGE' +} diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts index 1e54baf370..73ff97b061 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -25,6 +25,7 @@ import { AfterViewInit, EventEmitter, Inject, OnInit, Directive } from '@angular import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { AbstractControl, FormGroup } from '@angular/forms'; +import { RuleChainType } from '@shared/models/rule-chain.models'; export interface RuleNodeConfiguration { [key: string]: any; @@ -313,6 +314,7 @@ export interface FcRuleNode extends FcRuleNodeType { error?: string; highlighted?: boolean; componentClazz?: string; + ruleChainType?: RuleChainType; } export interface FcRuleEdge extends FcEdge { @@ -421,7 +423,9 @@ const ruleNodeClazzHelpLinkMap = { 'org.thingsboard.rule.engine.rabbitmq.TbRabbitMqNode': 'ruleNodeRabbitMq', 'org.thingsboard.rule.engine.rest.TbRestApiCallNode': 'ruleNodeRestApiCall', 'org.thingsboard.rule.engine.mail.TbSendEmailNode': 'ruleNodeSendEmail', - 'org.thingsboard.rule.engine.sms.TbSendSmsNode': 'ruleNodeSendSms' + 'org.thingsboard.rule.engine.sms.TbSendSmsNode': 'ruleNodeSendSms', + 'org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode': 'ruleNodePushToCloud', + 'org.thingsboard.rule.engine.edge.TbMsgPushToEdgeNode': 'ruleNodePushToEdge' }; export function getRuleNodeHelpLink(component: RuleNodeComponentDescriptor): string { diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 02cd3b93d4..15d36d6753 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -141,6 +141,7 @@ import { FileSizePipe } from '@shared/pipe/file-size.pipe'; import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component'; import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe'; import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-interval.component'; +import { MAT_DATE_LOCALE } from '@angular/material/core'; @NgModule({ providers: [ @@ -154,6 +155,10 @@ import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-i { provide: FlowInjectionToken, useValue: Flow + }, + { + provide: MAT_DATE_LOCALE, + useValue: 'en-GB' } ], declarations: [ diff --git a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json index 888154e3bf..92941c9404 100644 --- a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json +++ b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json @@ -1332,7 +1332,6 @@ "body": "Tělo", "method": "Metoda", "type": "Typ", - "entity": "Entita", "message-id": "Id zprávy", "message-type": "Typ zprávy", "data-type": "Typ dat", @@ -1344,7 +1343,9 @@ "success": "Úspěch", "failed": "Neúspěch", "messages-processed": "Zpracované zprávy", - "errors-occurred": "Vyskytly se chyby" + "errors-occurred": "Vyskytly se chyby", + "all-events": "Vše", + "entity-type": "Typ entity" }, "extension": { "extensions": "Rozšíření", diff --git a/ui-ngx/src/assets/locale/locale.constant-de_DE.json b/ui-ngx/src/assets/locale/locale.constant-de_DE.json index d05662f027..3dcf03e1b0 100644 --- a/ui-ngx/src/assets/locale/locale.constant-de_DE.json +++ b/ui-ngx/src/assets/locale/locale.constant-de_DE.json @@ -183,6 +183,8 @@ "filter-type-entity-view-type": "Entitätsansichtstyp", "filter-type-entity-view-type-description": "Entitätsansichten vom Typ '{{entityView}}'", "filter-type-entity-view-type-and-name-description": "Entitätsansichten vom Typ '{{entityView}}' und Name beginnend mit '{{prefix}}'", + "filter-type-edge-type": "Randtyp", + "filter-type-edge-type-description": "Rand vom Typ '{{edgeType}}'", "filter-type-relations-query": "Beziehungsabfrage", "filter-type-relations-query-description": "{{entities}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Objektabfrage", @@ -191,6 +193,8 @@ "filter-type-device-search-query-description": "Geräte vom Typ {{deviceTypes}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}", "filter-type-entity-view-search-query": "Entitätsansichtsabfrage", "filter-type-entity-view-search-query-description": "Entitätsansichten vom Typ {{entityViewTypes}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}", + "filter-type-edge-search-query": "Randabfrage", + "filter-type-edge-search-query-description": "Rand vom Typ {{edgeTypes}} mit {{relationType}} Beziehung {{direction}} {{rootEntity}}", "entity-filter": "Entitätsfilter", "resolve-multiple": "Als mehrere Entitäten auflösen", "filter-type": "Filtertyp", @@ -269,7 +273,15 @@ "no-assets-matching": "Es wurden keine zu '{{entity}}' passenden Objekte gefunden.", "asset-required": "Objekt ist erforderlich", "name-starts-with": "Name des Objekts beginnt mit", - "label": "Bezeichnung" + "label": "Bezeichnung", + "assign-asset-to-edge": "Objekte dem Rand zuordnen", + "assign-asset-to-edge-text":"Bitte wählen Sie die Objekte aus, die dem Rand zugeordnet werden sollen", + "unassign-asset-from-edge": "Objekte von Rand entfernen", + "unassign-asset-from-edge-title": "Sind Sie sicher, dass Sie die Zuordnung für das Objekt '{{assetName}}' aufheben möchten?", + "unassign-asset-from-edge-text": "Nach Bestätigung wird die Zuordnung des Objekts aufgehoben und es ist für den Kunden nicht mehr zugänglich.", + "unassign-assets-from-edge-action-title": "Rand { count, plural, 1 {1 Objektzuordnung} other {# Objektzuordnungen} } aufheben", + "unassign-assets-from-edge-title": "Sind Sie sicher, dass Sie die Zuordnung für das Objekt '{{assetName}}' wirklich aufheben möchten?", + "unassign-assets-from-edge-text": "Nach der Bestätigung werden alle ausgewählten Objekte nicht zugewiesen und sind für den Rand nicht zugänglich." }, "attribute": { "attributes": "Eigenschaften", @@ -317,6 +329,8 @@ "type-credentials-updated": "Anmeldeinformationen wurden aktualisiert", "type-assigned-to-customer": "Kunden Zuordnung", "type-unassigned-from-customer": "Kunden Zuordnung aufgehoben", + "type-assigned-to-edge": "Rand Zuordnung", + "type-unassigned-from-edge": "Rand Zuordnung aufgehoben", "type-activated": "Aktiviert", "type-suspended": "Ausgesetzt", "type-credentials-read": "Anmeldeinformationen gelesen", @@ -381,6 +395,7 @@ "public-devices": "Öffentliche Geräte", "public-assets": "Öffentliche Objekte", "public-entity-views": "Öffentliche Entitätsansichten", + "public-edges": "Öffentliche Rand", "add": "Kunde hinzufügen", "delete": "Kunde löschen", "manage-customer-users": "Kundenbenutzer verwalten", @@ -389,7 +404,9 @@ "manage-public-devices": "Öffentliche Geräte verwalten", "manage-public-dashboards": "Öffentliche Dashboards verwalten", "manage-customer-assets": "Kundenobjekte verwalten", + "manage-customer-edges": "Randobjekte verwalten", "manage-public-assets": "Öffentliche Objekte verwalten", + "manage-public-edges": "Öffentliche Rand verwalten", "add-customer-text": "Neuen Kunden hinzufügen", "no-customers-text": "Keine Kunden gefunden", "customer-details": "Kundendetails", @@ -414,6 +431,7 @@ "customer-required": "Kunde ist erforderlich", "select-default-customer": "Wählen Sie den Standardkunden aus.", "default-customer": "Standardkunde", + "edge-instances": "Kunden Rand", "default-customer-required": "Ein Standardkunde ist erforderlich, um das Dashboard auf Mandantenebene zu testen." }, "datetime": { @@ -561,7 +579,11 @@ "show-details": "Details anzeigen", "hide-details": "Details ausblenden", "select-state": "Soll-Zustand auswählen", - "state-controller": "Zustandssteuerung" + "state-controller": "Zustandssteuerung", + "unassign-dashboard-from-edge-text": "Nach der Bestätigung wird die Zuordnung des Dashboards aufgehoben und es ist für der Rand nicht mehr zugänglich.", + "unassign-dashboards-from-edge-text": "Nach der Bestätigung wird die Zuordnung aller ausgewählten Dashboards aufgehoben und sie sind für den Rand nicht mehr zugänglich.", + "assign-dashboard-to-edge": "Dashboard(s) dem Rand zuordnen", + "assign-dashboard-to-edge-text": "Bitte wählen Sie die Dashboards aus, die Sie dem Rand zuordnen möchten" }, "datakey": { "settings": "Einstellungen", @@ -692,11 +714,165 @@ "is-gateway": "Ist ein Gateway", "public": "Öffentlich", "device-public": "Gerät ist öffentlich", - "select-device": "Gerät auswählen" + "select-device": "Gerät auswählen", + "assign-device-to-edge-text":"Bitte wählen Sie die Geräte aus, die Sie dem Rand zuordnen möchten", + "unassign-device-from-edge-title": "Sind Sie sicher, dass Sie die Zuordnung zum Gerät '{{deviceName}}' wirklich aufheben möchten?", + "unassign-device-from-edge-text": "Nach der Bestätigung ist das Gerät nicht zugeordnet und für den Kunden nicht zugänglich.", + "unassign-devices-from-edge-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Gerät} other {# Geräte} } nicht mehr zuordnen möchten?", + "unassign-devices-from-edge-text": "Nach der Bestätigung werden alle ausgewählten Geräte nicht zugewiesen und sind für den Rand nicht zugänglich." }, "dialog": { "close": "Dialog schließen" }, + "edge": { + "edge": "Rand", + "edge-instances": "Kanteninstanzen", + "management": "Rand verwalten", + "no-edges-matching": "Keine passenden Rand '{{entity}}' gefunden.", + "rulechain-templates": "Regelkettenvorlagen", + "add": "Rand hinzufügen", + "no-edges-text": "Kein Rand gefunden.", + "edge-details": "Details der Rand", + "add-edge-text": "Neue Rand hinzufügen", + "delete": "Rand löschen", + "delete-edges": "Rand löschen", + "delete-edge-title": "Möchten Sie des Rands wirklich löschen '{{edgeName}}'?", + "delete-edge-text": "Seien Sie vorsichtig, nach der Bestätigung werden der Rand und alle zugehörigen Daten nicht wiederhergestellt.", + "delete-edges-title": "Sind Sie sicher, dass Sie die Rand löschen möchten { count, plural, 1 {1 Rand} other {# Rand} }?", + "delete-edges-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Rand entfernt und alle zugehörigen Daten werden nicht wiederhergestellt.", + "name": "Name", + "name-required": "Name ist erforderlich.", + "edge-license-key": "Edge Lizenzschlüssel", + "edge-license-key-required": "Edge Lizenzschlüssel ist erforderlich.", + "cloud-endpoint": "Cloud-Endpunkt", + "cloud-endpoint-required": "Cloud-Endpunkt ist erforderlich.", + "description": "Beschreibung", + "events": "Ereignisse", + "details": "Details", + "copy-id": "Regelketten-ID kopieren", + "id-copied-message": "Regelketten-ID wurde in die Zwischenablage kopiert", + "sync": "Sync Edge", + "sync-message": "Edge wurde synchronisiert", + "edge-required": "Rand ist erforderlich.", + "edge-type": "Randtyp", + "edge-type-required": "Randtyp ist erforderlich.", + "select-edge-type": "Randtyp auswählen", + "assign-to-customer": "Einem Kunden zuordnen", + "assign-to-customer-text": "Bitte wählen Sie den Kunden aus, dem die Rand zugeordnet werden sollen", + "assign-edge-to-customer": "Rand dem Kunden zuordnen", + "assign-edge-to-customer-text": "Bitte wählen Sie die Rand aus, die dem Kunden zugeordnet werden sollen", + "assigned-to-customer": "Kunden Zuordnung", + "unassign-from-customer": "Kunden Zuordnung aufgehoben", + "assign-edges-text": "{ count, plural, 1 {1 Gerät} other {# Geräte} } dem Rand zuordnen", + "unassign-edge-title": "Sind Sie sicher, dass Sie die Zuordnung zum Rand '{{edgeName}}' wirklich aufheben möchten?", + "unassign-edge-text": "Nach der Bestätigung ist der Rand nicht zugeordnet und für den Kunden nicht zugänglich.", + "make-public": "Rand öffentlich machen", + "make-public-edge-title": "Sind Sie sicher, dass Sie der Rand '{{edgeName}}' öffentlich machen möchten?", + "make-public-edge-text": "Nach Bestätigung wird der Rabd und alle zugehörigen Daten anderen zugänglich gemacht.", + "make-private": "Rand privat machen", + "public": "Öffentlich", + "make-private-edge-title": "Sind Sie sicher, dass Sie der Rand '{{edgeName}}' privat machen möchten?", + "make-private-edge-text": "Nach der Bestätigung werden der Rand und dessen Daten privat und sind für andere nicht mehr zugänglich.", + "import": "Rand importieren", + "label": "Bezeichnung", + "assign-new-edge": "Neue Rand zuordnen", + "manage-edge-dashboards": "Rand-Dashboards verwalten", + "unassign-from-edge": "Rand zuweisen", + "dashboards": "Rand Dashboards", + "manage-edge-rulechains": "Randregelkette verwalten", + "rulechains": "Rand Regelketten", + "edge-key": "Rand Schlüssel", + "copy-edge-key": "Rand Schlüssel kopieren", + "edge-key-copied-message": "Rand Schlüssel wurde in die Zwischenablage kopiert", + "edge-secret": "Rand Geheimnis", + "copy-edge-secret": "Rand Geheimnis kopieren", + "edge-secret-copied-message": "Rand Geheimnis wurde in die Zwischenablage kopiert", + "manage-edge-assets": "Rand-Objekte verwalten", + "manage-edge-devices": "Rand-Geräte verwalten", + "manage-edge-entity-views": "Rand-Entitätsansichten verwalten", + "assets": "Rand Objekte", + "devices": "Objekte Geräte", + "entity-views": "Objekte Entitätsansichten", + "entity-id": "Entität ID", + "event-action": "Ereignisaktion", + "load-entity-error": "Entität nicht gefunden. Fehler beim Laden der Informationen", + "unassign-edges-text": "Nach der Bestätigung werden alle ausgewählten Kanten nicht zugewiesen und sind für den Kunden nicht zugänglich.", + "unassign-edges-title": "Sind Sie sicher, dass Sie die Zuordnung aufheben möchten { count, plural, 1 {1 Rand} other {# Rand} }?", + "edge-rulechains": "Kantenregelketten", + "edge-license-key-hint": "Um Ihre Lizenz zu erhalten, navigieren Sie zur Preisseite und wählen Sie die beste Lizenzoption für Ihre aus Fall.", + "assignedToCustomer": "Dem Kunden zugewiesen", + "edge-public": "Edge ist öffentlich", + "search": "Kanten durchsuchen", + "selected-edges": "{Anzahl, Plural, 1 {1 Kante} andere {# Kanten}} ausgewählt", + "any-edge": "Beliebige Kante", + "dashboard": "Kanten-Dashboard", + "sync-process-started-successfully": "Synchronisierungsprozess erfolgreich gestartet!", + "delete-edges-action-title": "Löschen { count, plural, 1 {1 Rand} other {# Rand} }", + "set-root-rule-chain-text": "Bitte wählen Sie die Regelkette zur Wurzel rule chain für die Rand", + "set-root-rule-chain-to-edges": "Regelkette zur Wurzel machen für die Rand", + "set-root-rule-chain-to-edges-text": "Die Regelkette zur Wurzel für { count, plural, 1 {1 Rand} other {# Rand} } machen", + "status": "Von Rand empfangen", + "deployed": "Bereitgestellt", + "pending": "Steht aus", + "edge-file": "Edge-Datei", + "name-starts-with": "Der Kantenname beginnt mit", + "rulechain-template": "Regelkettenvorlage", + "unassign-edges-action-title": "Heben Sie die Zuordnung von {count, plural, 1 {1 edge} other {# edge}} vom Kunden auf", + "enter-edge-type": "Geben Sie den Kantentyp ein", + "no-edge-types-matching": "Es wurden keine Kantentypen gefunden, die mit '{{entitySubtype}}' übereinstimmen.", + "edge-type-list-empty": "Keine Kantentypen ausgewählt.", + "edge-types": "Kantentypen", + "license-key-hint": "Um Ihre Lizenz zu erhalten, navigieren Sie zur Preisseite und wählen Sie die beste Lizenzoption für Ihre aus Fall.", + "cloud-endpoint-hint": "Edge erfordert HTTP-Zugriff auf die Cloud (ThingsBoard CE / PE), um den Lizenzschlüssel zu überprüfen. Bitte geben Sie die Cloud-URL an, zu der Edge eine Verbindung herstellen kann.", + "missing-related-rule-chains-title": "In Edge fehlen verwandte Regelketten.", + "missing-related-rule-chains-text": "Randregelkette (n) zugewiesen Verwenden Sie Regelknoten, die Nachrichten an Regelkette (n) weiterleiten, die dieser Kante nicht zugeordnet sind.

Liste der fehlenden Regelketten:
{{missingRuleChains}}", + "downlinks": "Downlinks", + "no-downlinks-prompt": "Keine Downlinks gefunden", + "assigned-to-customer-widget": "Zugewiesen an: {{customerTitle}}", + "widget-datasource-error": "Dieses Widget unterstützt nur EDGE-Entitätsdatenquellen" + }, + "edge-event": { + "type-dashboard": "Dashboard", + "type-asset": "Asset", + "type-device": "Device", + "type-device-profile": "Device Profile", + "type-entity-view": "Entity View", + "type-alarm": "Alarm", + "type-rule-chain": "Rule Chain", + "type-rule-chain-metadata": "Rule Chain Metadata", + "type-edge": "Edge", + "type-entity-group": "Entity Group", + "type-scheduler-event": "Scheduler Event", + "type-white-labeling": "White Labeling", + "type-login-white-labeling": "White Labeling Login", + "type-user": "User", + "type-tenant": "Tenant", + "type-customer": "Customer", + "type-custom-translation": "Custom Translation", + "type-relation": "Relation", + "type-widgets-bundle": "Widgets Bundle", + "type-widgets-type": "Widgets Type", + "type-admin-settings": "Admin Settings", + "action-type-added": "Added", + "action-type-deleted": "Deleted", + "action-type-updated": "Updated", + "action-type-post-attributes": "Post Attributes", + "action-type-attributes-updated": "Attributes Updated", + "action-type-attributes-deleted": "Attributes Deleted", + "action-type-timeseries-updated": "Timeseries Updated", + "action-type-credentials-updated": "Credentials Updated", + "action-type-assigned-to-customer": "Assigned to Customer", + "action-type-unassigned-from-customer": "Unassigned from Customer", + "action-type-relation-add-or-update": "Relation Add or Update", + "action-type-relation-deleted": "Relation Deleted", + "action-type-rpc-call": "RPC Call", + "action-type-alarm-ack": "Alarm Ack", + "action-type-alarm-clear": "Alarm Clear", + "action-type-assigned-to-edge": "Assigned to Edge", + "action-type-unassigned-from-edge": "Unassigned from Edge", + "action-type-credentials-request": "Credentials Request", + "action-type-entity-merge-request": "Entity Merge Request" + }, "error": { "unable-to-connect": "Es konnte keine Verbindung zum Server hergestellt werden! Bitte überprüfen Sie Ihre Internetverbindung.", "unhandled-error-code": "Unbehandelter Fehlercode: {{errorCode}}", @@ -790,6 +966,10 @@ "type-rulenodes": "Regelknoten", "list-of-rulenodes": "{ count, plural, 1 {Ein Regelknoten} other {Liste von # Regelknoten} }", "rulenode-name-starts-with": "Regelknoten beginnend mit '{{prefix}}'", + "type-edge": "Randtyp", + "type-edges": "Randtyp", + "list-of-edges": "{ count, plural, 1 {1 Rand} other {# Rand} }", + "edge-name-starts-with": "Rand beginnend mit '{{prefix}}'", "type-current-customer": "Aktueller Kunde", "search": "Entitäten suchen", "selected-entities": "{ count, plural, 1 {Entität} other {# Entitäten} } ausgewählt", @@ -862,6 +1042,14 @@ "entity-view-types": "Entitätsansichtstypen", "name": "Name", "name-required": "Name ist erforderlich.", + "assign-entity-view-to-edge": "Entitätsansicht dem Rand zuordnen", + "assign-entity-view-to-edge-text":"Bitte wählen Sie die Entitätsansicht aus, die dem Rand zugeordnet werden sollen", + "unassign-entity-view-from-edge-title": "Sind Sie sicher, dass Sie die Zuordnung für Entitätsansicht '{{entityViewName}}' aufheben möchten?", + "unassign-entity-view-from-edge-text": "Nach Bestätigung wird die Zuordnung des Entitätsansichts aufgehoben und es ist für den Kunden nicht mehr zugänglich.", + "unassign-entity-views-from-edge-action-title": "Rand { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichte} } aufheben", + "unassign-entity-view-from-edge": "Entitätsansichtzuordnung aufheben", + "unassign-entity-views-from-edge-title": "Sind Sie sicher, dass Sie { count, plural, 1 {1 Entitätsansicht} other {# Entitätsansichte} } nicht mehr zuordnen möchten?", + "unassign-entity-views-from-edge-text": "Nach der Bestätigung werden alle ausgewählten Entitätsansicht nicht zugewiesen und sind für den Rand nicht zugänglich.", "description": "Beschreibung", "events": "Ereignisse", "details": "Details", @@ -899,13 +1087,13 @@ "type-debug-rule-chain": "Fehlersuche", "no-events-prompt": "Keine Ereignisse gefunden", "error": "Fehler", + "type-edge-event": "Downlink", "alarm": "Alarm", "event-time": "Ereigniszeit", "server": "Server", "body": "Inhalt", "method": "Methode", "type": "Typ", - "entity": "Entität", "message-id": "Nachrichten-Id", "message-type": "Nachrichten-Typ", "data-type": "Datentyp", @@ -917,7 +1105,9 @@ "success": "Erfolg", "failed": "Fehlgeschlagen", "messages-processed": "Nachrichten verarbeitet", - "errors-occurred": "Fehler aufgetreten" + "errors-occurred": "Fehler aufgetreten", + "all-events": "Alle", + "entity-type": "Entitätstyp" }, "extension": { "extensions": "Erweiterungen", @@ -1257,7 +1447,34 @@ "no-rulechains-matching": "Es wurden keine passenden Regelketten für '{{entity}}' gefunden.", "rulechain-required": "Regelkette ist erforderlich", "management": "Regelverwaltung", - "debug-mode": "Modus zur Fehlersuche" + "debug-mode": "Modus zur Fehlersuche", + "assign-rulechains": "Regelketten zuweisen", + "assign-new-rulechain": "Neues Regelkette zuweisen", + "delete-rulechains": "Regelketten löschen", + "unassign-rulechain": "Nicht zugeordnete Regelkette", + "unassign-rulechains": "Nicht zugeordnete Regelketten", + "unassign-rulechain-title": "Möchten Sie die Zuordnung die Regelkette '{{ruleChainTitle}}' wirklich aufheben?", + "unassign-rulechain-from-edge-text": "Nach der Bestätigung wird die Zuordnung aller ausgewählten Regelkette aufgehoben und sie sind für den Rand nicht mehr zugänglich.", + "unassign-rulechains-from-edge-action-title": "Zuordnung { count, plural, 1 {1 Regelkette} other {# Regelketten} } vom Rand aufheben", + "unassign-rulechains-from-edge-text": "Nach der Bestätigung wird die Zuordnung aller ausgewählten Regelketten aufgehoben und sie sind für den Rand nicht mehr zugänglich.", + "assign-rulechain-to-edge-title": "Regelkette(n) dem Rand zuordnen", + "assign-rulechain-to-edge-text": "Bitte wählen Sie die Regelketten aus, die Sie dem Rand zuordnen möchten", + "set-edge-template-root-rulechain": "Erstellen Sie den Stamm der Regelkettenkantenvorlage", + "set-edge-template-root-rulechain-title": "Möchten Sie die Kantenvorlage der Regelkette '{{ruleChainName}}' wirklich als Root festlegen?", + "set-edge-template-root-rulechain-text": "Nach der Bestätigung wird die Regelkette zum Stamm der Kantenvorlage und zur Stammregelkette für neu erstellte Kanten.", + "invalid-rulechain-type-error": "Regelkette konnte nicht importiert werden: Ungültige Regelkettentyp. Erwarteter Typ ist {{expectedRuleChainType}}.", + "set-auto-assign-to-edge": "Weisen Sie bei der Erstellung den Kanten die Regelkette zu", + "set-auto-assign-to-edge-title": "Möchten Sie die Kantenregelkette '{{ruleChainName}}' bei der Erstellung automatisch den Kanten zuweisen?", + "set-auto-assign-to-edge-text": "Nach der Bestätigung wird die Kantenregelkette bei der Erstellung automatisch den Kanten zugewiesen.", + "unset-auto-assign-to-edge": "Deaktiviert die Zuordnung der Regelkette zu Kanten bei der Erstellung", + "unset-auto-assign-to-edge-title": "Möchten Sie die Kantenregelkette '{{ruleChainName}}' bei der Erstellung unbedingt den Kanten zuweisen?", + "unset-auto-assign-to-edge-text": "Nach der Bestätigung wird die Kantenregelkette bei der Erstellung nicht mehr automatisch den Kanten zugewiesen.", + "edge-template-root": "Vorlagenstamm", + "search": "Suchen Sie nach Regelketten", + "selected-rulechains": "{count, plural, 1 {1 Regelkette} andere {# Regelketten}} ausgewählt", + "open-rulechain": "Regelkette öffnen", + "assign-to-edge": "Rand zuweisen", + "edge-rulechain": "Kantenregelkette" }, "rulenode": { "details": "Details", diff --git a/ui-ngx/src/assets/locale/locale.constant-el_GR.json b/ui-ngx/src/assets/locale/locale.constant-el_GR.json index 2d2ab96de5..d92b6e7884 100644 --- a/ui-ngx/src/assets/locale/locale.constant-el_GR.json +++ b/ui-ngx/src/assets/locale/locale.constant-el_GR.json @@ -1247,7 +1247,6 @@ "out": "Έξοδος", "metadata": "Μεταδεδομένα", "message": "Μήνυμα", - "entity": "Οντότητα", "message-id": "ID Μηνύματος", "message-type": "Τύπος Μηνύματος", "data-type": "Τύπος Δεδομένων", @@ -1258,7 +1257,9 @@ "success": "Επιτυχία", "failed": "Απέτυχε", "messages-processed": "Επεξεργασμένα μηνύματα", - "errors-occurred": "Παρουσιάστηκαν σφάλματα" + "errors-occurred": "Παρουσιάστηκαν σφάλματα", + "all-events": "Όλοι", + "entity-type": "Τύπος οντοτήτων" }, "extension": { "extensions": "Επεκτάσεις", 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 f8672eed56..a34693c7d5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -311,8 +311,11 @@ "filter-type-device-type-description": "Devices of type '{{deviceType}}'", "filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'", "filter-type-entity-view-type": "Entity View type", - "filter-type-entity-view-type-description": "Entity Views of type '{{entityView}}'", - "filter-type-entity-view-type-and-name-description": "Entity Views of type '{{entityView}}' and with name starting with '{{prefix}}'", + "filter-type-entity-view-type-description": "Entity Views of type '{{entityViewType}}'", + "filter-type-entity-view-type-and-name-description": "Entity Views of type '{{entityViewType}}' and with name starting with '{{prefix}}'", + "filter-type-edge-type": "Edge type", + "filter-type-edge-type-description": "Edges of type '{{edgeType}}'", + "filter-type-edge-type-and-name-description": "Edges of type '{{edgeType}}' and with name starting with '{{prefix}}'", "filter-type-relations-query": "Relations query", "filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Asset search query", @@ -322,6 +325,8 @@ "filter-type-entity-view-search-query": "Entity view search query", "filter-type-entity-view-search-query-description": "Entity views with types {{entityViewTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-apiUsageState": "Api Usage State", + "filter-type-edge-search-query": "Edge search query", + "filter-type-edge-search-query-description": "Edges with types {{edgeTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", "entity-filter": "Entity filter", "resolve-multiple": "Resolve as multiple entities", "filter-type": "Filter type", @@ -377,6 +382,8 @@ "asset-details": "Asset details", "assign-assets": "Assign assets", "assign-assets-text": "Assign { count, plural, 1 {1 asset} other {# assets} } to customer", + "assign-asset-to-edge-title": "Assign Asset(s) To Edge", + "assign-asset-to-edge-text":"Please select the assets to assign to the edge", "delete-assets": "Delete assets", "unassign-assets": "Unassign assets", "unassign-assets-action-title": "Unassign { count, plural, 1 {1 asset} other {# assets} } from customer", @@ -395,6 +402,7 @@ "unassign-asset": "Unassign asset", "unassign-assets-title": "Are you sure you want to unassign { count, plural, 1 {1 asset} other {# assets} }?", "unassign-assets-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the customer.", + "unassign-assets-from-edge": "Unassign assets from edge", "copyId": "Copy asset Id", "idCopiedMessage": "Asset Id has been copied to clipboard", "select-asset": "Select asset", @@ -403,9 +411,14 @@ "name-starts-with": "Asset name starts with", "import": "Import assets", "asset-file": "Asset file", - "search": "Search assets", - "selected-assets": "{ count, plural, 1 {1 asset} other {# assets} } selected", - "label": "Label" + "label": "Label", + "assign-asset-to-edge": "Assign Asset(s) To Edge", + "unassign-asset-from-edge": "Unassign asset", + "unassign-asset-from-edge-title": "Are you sure you want to unassign the asset '{{assetName}}'?", + "unassign-asset-from-edge-text": "After the confirmation the asset will be unassigned and won't be accessible by the edge.", + "unassign-assets-from-edge-title": "Are you sure you want to unassign { count, plural, 1 {1 asset} other {# assets} }?", + "unassign-assets-from-edge-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the edge.", + "selected-assets": "{ count, plural, 1 {1 asset} other {# assets} } selected" }, "attribute": { "attributes": "Attributes", @@ -508,6 +521,8 @@ "type-credentials-updated": "Credentials updated", "type-assigned-to-customer": "Assigned to Customer", "type-unassigned-from-customer": "Unassigned from Customer", + "type-assigned-to-edge": "Assigned to Edge", + "type-unassigned-from-edge": "Unassigned from Edge", "type-activated": "Activated", "type-suspended": "Suspended", "type-credentials-read": "Credentials read", @@ -578,6 +593,7 @@ "public-dashboards": "Public Dashboards", "public-devices": "Public Devices", "public-assets": "Public Assets", + "public-edges": "Public Edges", "public-entity-views": "Public Entity Views", "add": "Add Customer", "delete": "Delete customer", @@ -588,6 +604,8 @@ "manage-public-dashboards": "Manage public dashboards", "manage-customer-assets": "Manage customer assets", "manage-public-assets": "Manage public assets", + "manage-customer-edges": "Manage customer edges", + "manage-public-edges": "Manage customer edges", "add-customer-text": "Add new customer", "no-customers-text": "No customers found", "customer-details": "Customer details", @@ -614,7 +632,9 @@ "default-customer": "Default customer", "default-customer-required": "Default customer is required in order to debug dashboard on Tenant level", "search": "Search customers", - "selected-customers": "{ count, plural, 1 {1 customer} other {# customers} } selected" + "selected-customers": "{ count, plural, 1 {1 customer} other {# customers} } selected", + "edges": "Customer edge instances", + "manage-edges": "Manage edges" }, "datetime": { "date-from": "Date from", @@ -630,6 +650,7 @@ "add": "Add Dashboard", "assign-dashboard-to-customer": "Assign Dashboard(s) To Customer", "assign-dashboard-to-customer-text": "Please select the dashboards to assign to the customer", + "assign-dashboard-to-edge-title": "Assign Dashboard(s) To Edge", "assign-to-customer-text": "Please select the customer to assign the dashboard(s)", "assign-to-customer": "Assign to customer", "unassign-from-customer": "Unassign from customer", @@ -725,6 +746,8 @@ "display-filters": "Display filters", "display-dashboard-timewindow": "Display timewindow", "display-dashboard-export": "Display export", + "display-dashboard-logo": "Display logo in dashboard fullscreen mode", + "dashboard-logo-image": "Dashboard logo image", "import": "Import dashboard", "export": "Export dashboard", "export-failed-error": "Unable to export dashboard: {{error}}", @@ -773,7 +796,12 @@ "search": "Search dashboards", "selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } selected", "home-dashboard": "Home dashboard", - "home-dashboard-hide-toolbar": "Hide home dashboard toolbar" + "home-dashboard-hide-toolbar": "Hide home dashboard toolbar", + "unassign-dashboard-from-edge-text": "After the confirmation the dashboard will be unassigned and won't be accessible by the edge.", + "unassign-dashboards-from-edge-title": "Are you sure you want to unassign { count, plural, 1 {1 dashboard} other {# dashboards} }?", + "unassign-dashboards-from-edge-text": "After the confirmation all selected dashboards will be unassigned and won't be accessible by the edge.", + "assign-dashboard-to-edge": "Assign Dashboard(s) To Edge", + "assign-dashboard-to-edge-text": "Please select the dashboards to assign to the edge" }, "datakey": { "settings": "Settings", @@ -846,6 +874,8 @@ "assign-to-customer": "Assign to customer", "assign-device-to-customer": "Assign Device(s) To Customer", "assign-device-to-customer-text": "Please select the devices to assign to the customer", + "assign-device-to-edge-title": "Assign Device(s) To Edge", + "assign-device-to-edge-text":"Please select the devices to assign to the edge", "make-public": "Make device public", "make-private": "Make device private", "no-devices-text": "No devices found", @@ -861,6 +891,9 @@ "unassign-from-customer": "Unassign from customer", "unassign-devices": "Unassign devices", "unassign-devices-action-title": "Unassign { count, plural, 1 {1 device} other {# devices} } from customer", + "unassign-device-from-edge-title": "Are you sure you want to unassign the device '{{deviceName}}'?", + "unassign-device-from-edge-text": "After the confirmation the device will be unassigned and won't be accessible by the edge.", + "unassign-devices-from-edge": "Unassign devices from edge", "assign-new-device": "Assign new device", "make-public-device-title": "Are you sure you want to make the device '{{deviceName}}' public?", "make-public-device-text": "After the confirmation the device and all its data will be made public and accessible by others.", @@ -959,7 +992,9 @@ "specific-configuration": "Specific configuration", "customer-to-assign-device": "Customer to assign the device", "add-credential": "Add credential" - } + }, + "unassign-devices-from-edge-title": "Are you sure you want to unassign { count, plural, 1 {1 device} other {# devices} }?", + "unassign-devices-from-edge-text": "After the confirmation all selected devices will be unassigned and won't be accessible by the edge." }, "device-profile": { "device-profile": "Device profile", @@ -1028,8 +1063,13 @@ "telemetry-proto-schema-required": "Telemetry proto schema is required.", "attributes-proto-schema": "Attributes proto schema", "attributes-proto-schema-required": "Attributes proto schema is required.", + "rpc-response-proto-schema": "RPC response proto schema", + "rpc-response-proto-schema-required": "RPC response proto schema is required.", "rpc-response-topic-filter": "RPC response topic filter", "rpc-response-topic-filter-required": "RPC response topic filter is required.", + "rpc-request-proto-schema": "RPC request proto schema", + "rpc-request-proto-schema-required": "RPC request proto schema is required.", + "rpc-request-proto-schema-hint": "RPC request message should have always fields: string method = 1; int32 requestId = 2; and params = 3 of any data type.", "not-valid-pattern-topic-filter": "Not valid pattern topic filter", "not-valid-single-character": "Invalid use of a single-level wildcard character", "not-valid-multi-character": "Invalid use of a multi-level wildcard character", @@ -1138,17 +1178,32 @@ "instances-list": "Instances list", "instances-input": "Input Instance Id value", "instances-input-holder": "Input Instance number...", - "resource-label": "Resource", + "instance-label": "Instance", + "resource-label": " Resource name", "observe-label": "Observe", "attribute-label": "Attribute", "telemetry-label": "Telemetry", "key-name-label": "Key Name", + "attribute-lwm2m-label": "AttrLwm2m", + "resource-tip": "ID & Original Name of the Resource", "is-observe-tip": "Is Observe", + "not-observe-tip": "To observe select telemetry or attributes first", "is-attr-tip": "Is Attribute", "is-telemetry-tip": "Is Telemetry", - "key-name-tip": "Key Name", + "key-name-tip": "Key Name in Camel format", + "attribute-lwm2m-tip": "Attributes Lwm2m", + "attribute-lwm2m-disable-tip": "To edit Attributes Lwm2m select telemetry or attributes first", + "valid-attribute-lwm2m-key": "Name have be only '{{attrEnums}}'", + "valid-attribute-lwm2m-value": "Value have be Long, Double and greater than zero or null/empty", + "no-data": "No attributes", "key-name": "Key Name", - "key-name_label": "Key Name in Camel format", + "attribute-lwm2m-name": "Name attribute", + "attribute-lwm2m-value": "Value", + "attribute-lwm2m-toolbar-edit": "Edit attributes Lwm2m", + "attribute-lwm2m-toolbar-view": "View Attributes Lwm2m", + "attribute-lwm2m-destination": "Destination:", + "attribute-lwm2m-add-tip": "Add attribute lwm2m", + "attribute-lwm2m-remove-tip": "Remove attribute lwm2m", "required": " value is required.", "mode": "Security config mode", "pattern_hex_dec": "{ count, plural, 0 {must be hex decimal format} other {must be # characters} }", @@ -1182,6 +1237,144 @@ "column": "Column", "row": "Row" }, + "edge": { + "edge": "Edge", + "edge-instances": "Edge instances", + "edge-file": "Edge file", + "management": "Edge management", + "no-edges-matching": "No edges matching '{{entity}}' were found.", + "rulechain-templates": "Rule chain templates", + "rulechains": "Rule chains", + "edge-rulechains": "Edge Rule chains", + "add": "Add Edge", + "no-edges-text": "No edges found", + "edge-details": "Edge details", + "add-edge-text": "Add new edge", + "delete": "Delete edge", + "delete-edges": "Delete edges", + "delete-edge-title": "Are you sure you want to delete the edge '{{edgeName}}'?", + "delete-edge-text": "Be careful, after the confirmation the edge and all related data will become unrecoverable.", + "delete-edges-title": "Are you sure you want to edge { count, plural, 1 {1 edge} other {# edges} }?", + "delete-edges-text": "Be careful, after the confirmation all selected edges will be removed and all related data will become unrecoverable.", + "name": "Name", + "name-starts-with": "Edge name starts with", + "name-required": "Name is required.", + "edge-license-key": "Edge License Key", + "edge-license-key-required": "Edge License Key is required.", + "edge-license-key-hint": "To obtain your license please navigate to the pricing page and select the best license option for your case.", + "cloud-endpoint": "Cloud Endpoint", + "cloud-endpoint-required": "Cloud Endpoint is required.", + "cloud-endpoint-hint": "Edge requires HTTP(s) access to Cloud (ThingsBoard CE/PE) to verify the license key. Please specify Cloud URL that Edge is able to connect to.", + "description": "Description", + "details": "Details", + "events": "Events", + "copy-id": "Copy Edge Id", + "id-copied-message": "Edge Id has been copied to clipboard", + "sync": "Sync Edge", + "sync-message": "Edge has been synchronized", + "edge-required": "Edge required", + "edge-type": "Edge type", + "edge-type-required": "Edge type is required.", + "event-action": "Event action", + "entity-id": "Entity ID", + "select-edge-type": "Select edge type", + "assign-to-customer": "Assign to customer", + "assign-to-customer-text": "Please select the customer to assign the edge(s)", + "assign-edge-to-customer": "Assign Edge(s) To Customer", + "assign-edge-to-customer-text": "Please select the edges to assign to the customer", + "assignedToCustomer": "Assigned to customer", + "edge-public": "Edge is public", + "assigned-to-customer": "Assigned to: {{customerTitle}}", + "unassign-from-customer": "Unassign from customer", + "assign-edges-text": "Assign { count, plural, 1 {1 edge} other {# edges} } to customer", + "unassign-edge-title": "Are you sure you want to unassign the edge '{{edgeName}}'?", + "unassign-edge-text": "After the confirmation the edge will be unassigned and won't be accessible by the customer.", + "unassign-edges-title": "Are you sure you want to unassign { count, plural, 1 {1 edge} other {# edges} }?", + "unassign-edges-text": "After the confirmation all selected edges will be unassigned and won't be accessible by the customer.", + "make-public": "Make edge public", + "make-public-edge-title": "Are you sure you want to make the edge '{{edgeName}}' public?", + "make-public-edge-text": "After the confirmation the edge and all its data will be made public and accessible by others.", + "make-private": "Make edge private", + "public": "Public", + "make-private-edge-title": "Are you sure you want to make the edge '{{edgeName}}' private?", + "make-private-edge-text": "After the confirmation the edge and all its data will be made private and won't be accessible by others.", + "import": "Import edge", + "label": "Label", + "load-entity-error": "Failed to load data. Entity not found or has been deleted.", + "assign-new-edge": "Assign new edge", + "manage-edge-dashboards": "Edge dashboards", + "unassign-from-edge": "Unassign from edge", + "dashboards": "Edge Dashboards", + "manage-edge-rulechains": "Edge rule chains", + "rulechain-template": "Rule chain template", + "edge-key": "Edge key", + "copy-edge-key": "Copy Edge key", + "edge-key-copied-message": "Edge key has been copied to clipboard", + "edge-secret": "Edge secret", + "copy-edge-secret": "Copy Edge secret", + "edge-secret-copied-message": "Edge secret has been copied to clipboard", + "manage-edge-assets": "Edge assets", + "manage-edge-devices": "Edge devices", + "manage-edge-entity-views": "Edge entity views", + "assets": "Edge assets", + "devices": "Edge devices", + "entity-views": "Edge entity views", + "set-root-rule-chain-text": "Please select root rule chain for edge(s)", + "set-root-rule-chain-to-edges": "Set root rule chain for Edge(s)", + "set-root-rule-chain-to-edges-text": "Set root rule chain for { count, plural, 1 {1 edge} other {# edges} }", + "search": "Search edges", + "selected-edges": "{ count, plural, 1 {1 edge} other {# edges} } selected", + "any-edge": "Any edge", + "no-edge-types-matching": "No edge types matching '{{entitySubtype}}' were found.", + "edge-type-list-empty": "No edge types selected.", + "edge-types": "Edge types", + "dashboard": "Edge dashboard", + "enter-edge-type": "Enter edge type", + "deployed": "Deployed", + "pending": "Pending", + "downlinks": "Downlinks", + "no-downlinks-prompt": "No downlinks found", + "sync-process-started-successfully": "Sync process started successfully!", + "missing-related-rule-chains-title": "Edge has missing related rule chain(s)", + "missing-related-rule-chains-text": "Assigned to edge rule chain(s) use rule nodes that forward message(s) to rule chain(s) that are not assigned to this edge.

List of missing rule chain(s):
{{missingRuleChains}}", + "widget-datasource-error": "This widget supports only EDGE entity datasource" + }, + "edge-event": { + "type-dashboard": "Dashboard", + "type-asset": "Asset", + "type-device": "Device", + "type-device-profile": "Device Profile", + "type-entity-view": "Entity View", + "type-alarm": "Alar", + "type-rule-chain": "Rule Chain", + "type-rule-chain-metadata": "Rule Chain Metadata", + "type-edge": "Edge", + "type-user": "User", + "type-customer": "Customer", + "type-relation": "Relation", + "type-widgets-bundle": "Widgets Bundle", + "type-widgets-type": "Widgets Type", + "type-admin-settings": "Admin Settings", + "action-type-added": "Added", + "action-type-deleted": "Deleted", + "action-type-updated": "Updated", + "action-type-post-attributes": "Post Attributes", + "action-type-attributes-updated": "Attributes Updated", + "action-type-attributes-deleted": "Attributes Deleted", + "action-type-timeseries-updated": "Timeseries Updated", + "action-type-credentials-updated": "Credentials Updated", + "action-type-assigned-to-customer": "Assigned to Customer", + "action-type-unassigned-from-customer": "Unassigned from Customer", + "action-type-relation-add-or-update": "Relation Add or Update", + "action-type-relation-deleted": "Relation Deleted", + "action-type-rpc-call": "RPC Call", + "action-type-alarm-ack": "Alarm Ack", + "action-type-alarm-clear": "Alarm Clear", + "action-type-assigned-to-edge": "Assigned to Edge", + "action-type-unassigned-from-edge": "Unassigned from Edge", + "action-type-credentials-request": "Credentials Request", + "action-type-entity-merge-request": "Entity Merge Request" + }, "error": { "unable-to-connect": "Unable to connect to the server! Please check your internet connection.", "unhandled-error-code": "Unhandled error code: {{errorCode}}", @@ -1296,7 +1489,11 @@ "no-entities-prompt": "No entities found", "no-data": "No data to display", "columns-to-display": "Columns to Display", - "type-api-usage-state": "Api Usage State" + "type-api-usage-state": "Api Usage State", + "type-edge": "Edge", + "type-edges": "Edges", + "list-of-edges": "{ count, plural, 1 {One edge} other {List of # edges} }", + "edge-name-starts-with": "Edges whose names start with '{{prefix}}'" }, "entity-field": { "created-time": "Created time", @@ -1348,6 +1545,7 @@ "assign-to-customer": "Assign to customer", "assign-entity-view-to-customer": "Assign Entity View(s) To Customer", "assign-entity-view-to-customer-text": "Please select the entity views to assign to the customer", + "assign-entity-view-to-edge-title": "Assign Entity View(s) To Edge", "no-entity-views-text": "No entity views found", "assign-to-customer-text": "Please select the customer to assign the entity view(s)", "entity-view-details": "Entity view details", @@ -1414,11 +1612,18 @@ "make-public-entity-view-text": "After the confirmation the entity view and all its data will be made public and accessible by others.", "make-private-entity-view-title": "Are you sure you want to make the entity view '{{entityViewName}}' private?", "make-private-entity-view-text": "After the confirmation the entity view and all its data will be made private and won't be accessible by others.", - "search": "Search entity views", - "selected-entity-views": "{ count, plural, 1 {1 entity view} other {# entity views} } selected" + "assign-entity-view-to-edge": "Assign Entity View(s) To Edge", + "assign-entity-view-to-edge-text":"Please select the entity views to assign to the edge", + "unassign-entity-view-from-edge-title": "Are you sure you want to unassign the entity view '{{entityViewName}}'?", + "unassign-entity-view-from-edge-text": "After the confirmation the entity view will be unassigned and won't be accessible by the edge.", + "unassign-entity-views-from-edge-action-title": "Unassign { count, plural, 1 {1 entity view} other {# entity views} } from edge", + "unassign-entity-view-from-edge": "Unassign entity view", + "unassign-entity-views-from-edge-title": "Are you sure you want to unassign { count, plural, 1 {1 entity view} other {# entity views} }?", + "unassign-entity-views-from-edge-text": "After the confirmation all selected entity views will be unassigned and won't be accessible by the edge." }, "event": { "event-type": "Event type", + "events-filter": "Events Filter", "type-error": "Error", "type-lc-event": "Lifecycle event", "type-stats": "Statistics", @@ -1432,7 +1637,6 @@ "body": "Body", "method": "Method", "type": "Type", - "entity": "Entity", "message-id": "Message Id", "message-type": "Message Type", "data-type": "Data Type", @@ -1444,7 +1648,14 @@ "success": "Success", "failed": "Failed", "messages-processed": "Messages processed", - "errors-occurred": "Errors occurred" + "min-messages-processed": "Minimum messages processed", + "errors-occurred": "Errors occurred", + "min-errors-occurred": "Minimum errors occurred", + "min-value": "Minimum value is 0.", + "all-events": "All", + "has-error": "Has error", + "entity-id": "Entity Id", + "entity-type": "Entity type" }, "extension": { "extensions": "Extensions", @@ -1856,7 +2067,11 @@ "access-token": "Access token", "isgateway": "Is Gateway", "activity-time-from-gateway-device": "Activity time from gateway device", - "description": "Description" + "description": "Description", + "edge-license-key": "License Key", + "cloud-endpoint": "Cloud Endpoint", + "routing-key": "Edge key", + "secret": "Edge secret" }, "stepper-text":{ "select-file": "Select a file", @@ -2056,7 +2271,28 @@ "debug-mode": "Debug mode", "search": "Search rule chains", "selected-rulechains": "{ count, plural, 1 {1 rule chain} other {# rule chains} } selected", - "open-rulechain": "Open rule chain" + "open-rulechain": "Open rule chain", + "assign-new-rulechain": "Assign new rulechain", + "edge-template-root": "Template Root", + "assign-to-edge": "Assign to Edge", + "edge-rulechain": "Edge Rule chain", + "unassign-rulechain-from-edge-text": "After the confirmation the rulechain will be unassigned and won't be accessible by the edge.", + "unassign-rulechains-from-edge-title": "Are you sure you want to unassign { count, plural, 1 {1 rulechain} other {# rulechains} }?", + "unassign-rulechains-from-edge-text": "After the confirmation all selected rulechains will be unassigned and won't be accessible by the edge.", + "assign-rulechain-to-edge-title": "Assign Rule Chain(s) To Edge", + "assign-rulechain-to-edge-text": "Please select the rulechains to assign to the edge", + "set-edge-template-root-rulechain": "Make rule chain as edge template root", + "set-edge-template-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' edge template root?", + "set-edge-template-root-rulechain-text": "After the confirmation the rule chain will become edge template root and will be root rule chain for a newly created edges.", + "invalid-rulechain-type-error": "Unable to import rule chain: Invalid rule chain type. Expected type is {{expectedRuleChainType}}.", + "set-auto-assign-to-edge": "Assign rule chain to edge(s) on creation", + "set-auto-assign-to-edge-title": "Are you sure you want to assign the edge rule chain '{{ruleChainName}}' to edge(s) on creation?", + "set-auto-assign-to-edge-text": "After the confirmation the edge rule chain will be automatically assigned to edge(s) on creation.", + "unset-auto-assign-to-edge": "Do not assign rule chain to edge(s) on creation", + "unset-auto-assign-to-edge-title": "Are you sure you do not want to assign the edge rule chain '{{ruleChainName}}' to edge(s) on creation?", + "unset-auto-assign-to-edge-text": "After the confirmation the edge rule chain will no longer be automatically assigned to edge(s) on creation.", + "unassign-rulechain-title": "Are you sure you want to unassign the rulechain '{{ruleChainName}}'?", + "unassign-rulechains": "Unassign rulechains" }, "rulenode": { "details": "Details", diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index 37e6ce3931..9d18102a43 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -309,6 +309,8 @@ "filter-type-entity-view-type": "Tipo de vista de entidad", "filter-type-entity-view-type-description": "Vistas de entidad del tipo '{{entityView}}'", "filter-type-entity-view-type-and-name-description": "Vistas de entidad del tipo '{{entityView}}' y cuyo nombre comience por '{{prefix}}'", + "filter-type-edge-type": "Tipo de borde", + "filter-type-edge-type-description": "Bordes del tipo '{{edgeType}}'", "filter-type-relations-query": "Consulta de relaciones", "filter-type-relations-query-description": "{{entities}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Búsqueda de activos", @@ -318,11 +320,17 @@ "filter-type-entity-view-search-query": "Consulta de búsqueda de vista de entidad", "filter-type-entity-view-search-query-description": "Vistas de entidad con tipos {{entityViewTypes}} que tienen tipo de relación {{relationType}} con dirección {{direction}} {{rootEntity}}", "filter-type-apiUsageState": "Uso de API", + "filter-type-edge-search-query": "Consultar búsqueda de borde", + "filter-type-edge-search-query-description": "Bordes con tipos {{edgeTypes}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}", + "type-assigned-to-edge": "Asignado a borde", + "type-unassigned-from-edge": "Sin asignar desde bordes", "entity-filter": "Filtro por entidad", "resolve-multiple": "Tomar como múltiples entidades", "filter-type": "Filtro por tipo", "filter-type-required": "Se requiere filtro por tipo.", - "entity-filter-no-entity-matched": "No se han encontrado entidades con el filtro especificado.", + "filter-type-edge-type": "Tipo de borde", + "filter-type-edge-type-description": "Bordes del tipo '{{edgeType}}'", + "entity-filter-no-entity-matched": "No se han encontrado entidades con el filtro especificado.", "no-entity-filter-specified": "No hay filtro de entidades especificado", "root-state-entity": "Usar estado de panel como raíz", "last-level-relation": "Buscar sólo la relación de último nivel", @@ -401,7 +409,15 @@ "asset-file": "Archivo del activo", "search": "Buscar activos", "selected-assets": "{ count, plural, 1 {1 activo} other {# activos} } seleccionados", - "label": "Etiqueta" + "label": "Etiqueta", + "assign-asset-to-edge": "Asignar activo(s) al borde", + "assign-asset-to-edge-text":"Por favor, seleccione los activos para asignar al borde", + "unassign-asset-from-edge": "Anular activo de bodre", + "unassign-asset-from-edge-title": "¿Está seguro de que desea desasignar el activo '{{assetName}}'?", + "unassign-asset-from-edge-text": "Después de la confirmación, el activo no será asignado y el borde no podrá acceder a él", + "unassign-assets-from-edge-action-title": "Anular asignación {count, plural, 1 {1 activo} other {# activos}} desde el borde", + "unassign-assets-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 activo} other {# activos}}?", + "unassign-assets-from-edge-text": "Después de la confirmación, todos los activos seleccionados quedarán sin asignar y el borde no podrá acceder a ellos." }, "attribute": { "attributes": "Atributos", @@ -575,6 +591,7 @@ "public-devices": "Dispositivos Públicos", "public-assets": "Activos Públicos", "public-entity-views": "Vistas de Entidad Públicas", + "public-edges": "Bordes públicos", "add": "Agregar cliente", "delete": "Borrar cliente", "manage-customer-users": "Gestionar usuarios del cliente", @@ -583,6 +600,8 @@ "manage-public-devices": "Gestionar dispositivos públicos", "manage-public-dashboards": "Gestionar paneles públicos", "manage-customer-assets": "Gestionar activos del cliente", + "manage-customer-edges": "Administrar bordes de clientes", + "manage-public-edges": "Administrar bordes públicos", "manage-public-assets": "Gestionar activos públicos", "add-customer-text": "Agregar nuevo cliente", "no-customers-text": "No se encontraron clientes", @@ -601,6 +620,7 @@ "description": "Descripción", "details": "Detalles", "events": "Eventos", + "edges": "Bordes del cliente", "copyId": "Copiar ID de cliente", "idCopiedMessage": "El ID de cliente se ha copiado al portapapeles", "select-customer": "Seleccionar Cliente", @@ -764,7 +784,11 @@ "select-state": "Seleccionar estado destino (target state)", "state-controller": "Controlador de estados", "search": "Buscar paneles", - "selected-dashboards": "{ count, plural, 1 {1 panel} other {# paneles} } seleccionados" + "selected-dashboards": "{ count, plural, 1 {1 panel} other {# paneles} } seleccionados", + "unassign-dashboard-from-edge-text": "Después de la confirmación, el tablero no será asignado y el borde no podrá acceder a él", + "unassign-dashboards-from-edge-text": "Después de la confirmación, se anulará la asignación de todos los paneles seleccionados y no serán accesibles por de borde", + "assign-dashboard-to-edge": "Asignar panel(es) al borde", + "assign-dashboard-to-edge-text": "Por favor selecciona los paneles para asignar al borde" }, "datakey": { "settings": "Ajustes", @@ -924,7 +948,12 @@ "specific-configuration": "Configuración específica", "customer-to-assign-device": "Cliente al que asignar el dispositivo", "add-credential": "Añadir credencial" - } + }, + "assign-device-to-edge-text": "Seleccione los dispositivos para asignar al borde", + "unassign-device-from-edge-title": "¿Está seguro de que desea desasignar el dispositivo '{{deviceName}}'?", + "unassign-device-from-edge-text": "Después de la confirmación, el dispositivo no será asignado y el borde no podrá acceder a él", + "unassign-devices-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 dispositivo} other {# dispositivos}}?", + "unassign-devices-from-edge-text": "Después de la confirmación, todos los dispositivos seleccionados quedarán sin asignar y el borde no podrá acceder a ellos." }, "device-profile": { "device-profile": "Perfil de dispositivo", @@ -1083,6 +1112,153 @@ "column": "Columna", "row": "Fila" }, + "edge": { + "edge": "Borde", + "edge-instances": "Instancias de Borde", + "management": "Gestión de bordes", + "no-edges-matching": "No se encontraron bordes que coincidan con '{{entity}}'", + "rulechain-templates": "Plantillas, de cadena de reglas", + "add": "Agregar borde", + "no-edges-text": "No se encontraron bordes", + "edge-details": "Detalles del borde", + "add-edge-text": "Agregar nuevo borde", + "delete": "Eliminar borde", + "delete-edges": "Eliminar bordes", + "delete-edge-title": "¿Está seguro de que desea eliminar el borde '{{edgeName}}'?", + "delete-edge-text": "Tenga cuidado, después de la confirmación, el borde y todos los datos relacionados serán irrecuperables", + "delete-edges-title": "¿Está seguro de que desea edge {count, plural, 1 {1 borde} other {# bordes}}?", + "delete-edges-text": "Tenga cuidado, después de la confirmación se eliminarán todos los bordes seleccionados y todos los datos relacionados se volverán irrecuperables", + "name": "Nombre", + "name-required": "Se requiere nombre", + "edge-license-key": "Edge Clave de licencia", + "edge-license-key-required": "Se requiere edge clave de licencia", + "cloud-endpoint": "Punto final de la nube", + "cloud-endpoint-required": "Se requiere punto final de la nube", + "description": "Descripción", + "events": "Eventos", + "details": "Detalles", + "copy-id": "Copiar ID de borde", + "id-copied-message": "El ID de borde se ha copiado al portapapeles", + "sync": "Sinc Edge", + "sync-message": "Edge se ha sincronizado", + "edge-required": "Edge required", + "edge-type": "Type de la bordure", + "edge-type-required": "El tipo de borde es requerido.", + "select-edge-type": "Seleccionar tipo de borde", + "assign-to-customer": "Asignar al cliente", + "assign-to-customer-text": "Seleccione el cliente para asignar los bordes", + "assign-edge-to-customer": "Asignar borde(s) al cliente", + "assign-edge-to-customer-text": "Seleccione los bordes para asignar al cliente", + "assigned-to-customer": "Asignado al cliente", + "unassign-from-customer": "Anular asignación del cliente", + "assign-edges-text": "Asignar {cuenta, plural, 1 {1 borde} other {# bordes}} al cliente", + "unassign-edge-title": "¿Está seguro de que desea desasignar el borde '{{edgeName}}'?", + "unassign-edge-text": "Después de la confirmación, el borde quedará sin asignar y el cliente no podrá acceder a él", + "make-public": "Hacer público el borde", + "make-public-edge-title": "¿Estás seguro de que quieres hacer público el edge '{{edgeName}}'?", + "make-public-edge-text": "Después de la confirmación, el borde y todos sus datos serán públicos y accesibles para otros", + "make-private": "Hacer que edge sea privado", + "public": "Public", + "make-private-edge-title": "¿Está seguro de que desea que el borde '{{edgeName}}' sea privado?", + "make-private-edge-text": "Después de la confirmación, el borde y todos sus datos se harán privados y otros no podrán acceder a ellos", + "import": "Importar borde", + "label": "Etiqueta", + "assign-new-edge": "Asignar nuevo borde", + "manage-edge-dashboards": "Administrar paneles de borde", + "unassign-from-edge": "Anular asignación de borde", + "dashboards": "Paneles de borde", + "manage-edge-rulechains": "Administrar cadenas de reglas de borde", + "rulechains": "Cadenas de regla de borde", + "edge-key": "Clave de borde", + "copy-edge-key": "Copiar clave de borde", + "edge-key-copied-message": "La clave de borde se ha copiado al portapapeles", + "edge-secret": "Borde secreto", + "copy-edge-secret": "Copiar borde secreto", + "edge-secret-copied-message": "El secreto de borde se ha copiado al portapapeles", + "manage-edge-assets": "Gestionar activos de bordes", + "manage-edge-devices": "Gestionar dispositivos de borde", + "manage-edge-entity-views": "Gestionar vistas de entidad de borde", + "assets": "Activos de borde", + "devices": "Dispositivos de borde", + "entity-views": "Vistas de entidad de borde", + "entity-id": "ID de entidad", + "event-action": "Información de la entidad", + "load-entity-error": "Entidad no encontrada. No se pudo cargar la información", + "unassign-edges-text": "Después de la confirmación de todos los bordes seleccionados, se anulará la asignación y el cliente no podrá acceder a ellos.", + "unassign-edges-title": "¿Está seguro de que desea anular la asignación de {count, plural, 1 {1 borde} other {# bordes}}?", + "edge-rulechains": "Cadenas de reglas de borde", + "edge-license-key-hint": "Para obtener su licencia, vaya a la página de precios y seleccione la mejor opción de licencia para su caso.", + "assignedToCustomer": "Asignada a la cliente", + "edge-public": "Edge es pública", + "set-root-rule-chain-text": "Seleccione la cadena de reglas raíz para los bordes", + "set-root-rule-chain-to-edges": "Establecer cadena de reglas raíz para Edge (s)", + "set-root-rule-chain-to-edges-text": "Establecer cadena de reglas raíz para {count, plural, 1 {1 edge} other {# ends}}", + "search": "Bordes de búsqueda", + "selected-edges": "{count, plural, 1 {1 borde} other {# bordes}} seleccionados", + "any-edge": "Cualquier bordee", + "dashboard": "Panel de control Edge", + "deployed": "Desplegada", + "pending": "Pending", + "sync-process-started-successfully": "¡El proceso de sincronización se inició correctamente!", + "edge-file": "Archivo de borde", + "name-starts-with": "Edge name starts with", + "rulechain-template": "Plantilla de cadena de reglas", + "unassign-edges-action-title": "Anular la asignación de {count, plural, 1 {1 borde} other {# bordes}} del cliente", + "enter-edge-type": "Ingrese el tipo de borde", + "no-edge-types-matching": "No se encontraron tipos de aristas que coincidan con '{{entitySubtype}}'.", + "edge-type-list-empty": "No se seleccionó ningún tipo de borde.", + "edge-types": "Tipos de bordes", + "license-key-hint": "Para obtener su licencia, vaya a la página de precios y seleccione la mejor opción de licencia para su caso.", + "cloud-endpoint-hint": "Edge requiere acceso HTTP (s) a la nube (ThingsBoard CE / PE) para verificar la clave de licencia. Especifique la URL de la nube a la que Edge puede conectarse.", + "missing-related-rule-chains-title": "Al borde le faltan cadenas de reglas relacionadas", + "missing-related-rule-chains-text": "Asignado a la (s) cadena (s) de reglas de borde usa nodos de reglas que reenvían mensajes a cadenas de reglas que no están asignadas a este borde.

Lista de cadenas de reglas faltantes:
{{missingRuleChains}}", + "downlinks": "Enlaces descendentes", + "no-downlinks-prompt": "No se encontraron enlaces descendentes", + "assigned-to-customer-widget": "Asignado a: {{customerTitle}}", + "widget-datasource-error": "Este widget solo admite la fuente de datos de la entidad EDGE" + }, + "edge-event": { + "type-dashboard": "Dashboard", + "type-asset": "Asset", + "type-device": "Device", + "type-device-profile": "Device Profile", + "type-entity-view": "Entity View", + "type-alarm": "Alarm", + "type-rule-chain": "Rule Chain", + "type-rule-chain-metadata": "Rule Chain Metadata", + "type-edge": "Edge", + "type-entity-group": "Entity Group", + "type-scheduler-event": "Scheduler Event", + "type-white-labeling": "White Labeling", + "type-login-white-labeling": "White Labeling Login", + "type-user": "User", + "type-tenant": "Tenant", + "type-customer": "Customer", + "type-custom-translation": "Custom Translation", + "type-relation": "Relation", + "type-widgets-bundle": "Widgets Bundle", + "type-widgets-type": "Widgets Type", + "type-admin-settings": "Admin Settings", + "action-type-added": "Added", + "action-type-deleted": "Deleted", + "action-type-updated": "Updated", + "action-type-post-attributes": "Post Attributes", + "action-type-attributes-updated": "Attributes Updated", + "action-type-attributes-deleted": "Attributes Deleted", + "action-type-timeseries-updated": "Timeseries Updated", + "action-type-credentials-updated": "Credentials Updated", + "action-type-assigned-to-customer": "Assigned to Customer", + "action-type-unassigned-from-customer": "Unassigned from Customer", + "action-type-relation-add-or-update": "Relation Add or Update", + "action-type-relation-deleted": "Relation Deleted", + "action-type-rpc-call": "RPC Call", + "action-type-alarm-ack": "Alarm Ack", + "action-type-alarm-clear": "Alarm Clear", + "action-type-assigned-to-edge": "Assigned to Edge", + "action-type-unassigned-from-edge": "Unassigned from Edge", + "action-type-credentials-request": "Credentials Request", + "action-type-entity-merge-request": "Entity Merge Request" + }, "error": { "unable-to-connect": "Imposible conectar con el servidor! Por favor, revise su conexión a internet.", "unhandled-error-code": "Código de error no controlado: {{errorCode}}", @@ -1196,7 +1372,11 @@ "no-entities-prompt": "No se han encontrado entidades", "no-data": "No hay datos que mostrar", "columns-to-display": "Columnas a Mostrar", - "type-api-usage-state": "Estado de uso de la API" + "type-api-usage-state": "Estado de uso de la API", + "type-edge": "Borde", + "type-edges": "Bordes", + "list-of-edges": "{cuenta, plural, 1 {Un borde} other {Lista de # bordes}}", + "edge-name-starts-with": "Bordes cuyos nombres comienzan con '{{prefijo}}'" }, "entity-field": { "created-time": "Hora de creación", @@ -1315,7 +1495,15 @@ "make-private-entity-view-title": "¿Está seguro de que desea que la vista de entidad '{{entityViewName}}' sea privada?", "make-private-entity-view-text": "Tras la confirmación, la vista de la entidad y todos sus datos se harán privados y no serán accesibles para otros.", "search": "Buscar vistas de entidad", - "selected-entity-views": "{ count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } seleccionadas" + "selected-entity-views": "{ count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } seleccionadas", + "assign-entity-view-to-edge": "Asignar vista (s) de entidad a borde", + "assign-entity-view-to-edge-text": "Seleccione las vistas de entidad para asignar al borde", + "unassign-entity-view-from-edge-title": "¿Está seguro de que desea anular la asignación de la vista de entidad '{{entityViewName}}'?", + "unassign-entity-view-from-edge-text": "Después de la confirmación, la vista de entidad quedará sin asignar y el borde no podrá acceder a ella", + "unassign-entity-views-from-edge-action-title": "Anular asignación {recuento, plural, 1 {1 vista de entidad} otras {# vistas de entidad}} del borde", + "unassign-entity-view-from-edge": "Anular asignación de vista de entidad", + "unassign-entity-views-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 vista de entidad} other {# vistas de entidad}}?", + "unassign-entity-views-from-edge-text": "Después de la confirmación, todas las vistas de entidad seleccionadas no serán asignadas y el borde no podrá acceder a ellas" }, "event": { "event-type": "Tipo de evento", @@ -1326,13 +1514,13 @@ "type-debug-rule-chain": "Debug", "no-events-prompt": "Ningún evento encontrado.", "error": "Error", + "type-edge-event": "Downlink", "alarm": "Alarma", "event-time": "Hora del evento", "server": "Servidor", "body": "Cuerpo", "method": "Método", "type": "Tipo", - "entity": "Entidad", "message-id": "Id Mensaje", "message-type": "Tipo Mensaje", "data-type": "Tipo de Datos", @@ -1344,7 +1532,9 @@ "success": "Éxito", "failed": "Fallo", "messages-processed": "Mensajes procesados", - "errors-occurred": "Ocurrieron errores" + "errors-occurred": "Ocurrieron errores", + "all-events": "Todos", + "entity-type": "Tipo de entidad" }, "extension": { "extensions": "Extensiones", @@ -1897,7 +2087,34 @@ "debug-mode": "Modo Debug", "search": "Buscar cadenas de reglas", "selected-rulechains": "{ count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} } seleccionadas", - "open-rulechain": "Abrir cadena de reglas" + "open-rulechain": "Abrir cadena de reglas", + "assign-rulechains": "Asignar cadenas de reglas", + "assign-new-rulechain": "Asignar nueva cadena de reglas", + "delete-rulechains": "Eliminar cadenas de reglas", + "unassign-rulechain": "Anular asignación de cadena de reglas", + "unassign-rulechains": "Anular asignación de cadenas de reglas", + "unassign-rulechain-title": "¿Está seguro de que desea desasignar la cadena de reglas '{{ruleChainTitle}}'?", + "unassign-rulechain-from-edge-text": "Después de la confirmación, la cadena de reglas quedará sin asignar y el borde no podrá acceder a ella", + "unassign-rulechains-from-edge-action-title": "Anular asignación {count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas}} des bordes", + "unassign-rulechains-from-edge-text": "Después de la confirmación, todas las cadenas de reglas seleccionadas quedarán sin asignar y el borde no podrá acceder a ellas", + "assign-rulechain-to-edge-title": "Asignar cadena (s) de reglas a borde", + "assign-rulechain-to-edge-text": "Seleccione las cadenas de reglas para asignar al borde", + "set-edge-template-root-rulechain": "Hacer raíz de plantilla de borde de cadena de reglas", + "set-edge-template-root-rulechain-title": "¿Está seguro de que desea que la cadena de reglas '{{ruleChainName}}' sea la raíz de la plantilla de borde?", + "set-edge-template-root-rulechain-text": "Después de la confirmación, la cadena de reglas se convertirá en la raíz de la plantilla de borde y será la cadena de reglas raíz para los bordes recién creados.", + "invalid-rulechain-type-error": "No se puede importar la cadena de reglas: Tipo de cadena de reglas no válido. El tipo esperado es {{expectedRuleChainType}}", + "set-auto-assign-to-edge": "Asignar cadena de reglas a los bordes en la creación", + "set-auto-assign-to-edge-title": "¿Está seguro de que desea asignar automáticamente la cadena de reglas de borde '{{ruleChainName}}' a los bordes en la creación?", + "set-auto-assign-to-edge-text": "Después de la confirmación, la cadena de reglas de borde se asignará automáticamente a los bordes en la creación.", + "unset-auto-assign-to-edge": "Desmarcar asignar cadena de reglas a los bordes en la creación", + "unset-auto-assign-to-edge-title": "¿Está seguro de que desea anular la asignación de la cadena de reglas de borde '{{ruleChainName}}' a los bordes en la creación?", + "unset-auto-assign-to-edge-text": "Después de la confirmación, la cadena de reglas de borde ya no se asignará automáticamente a los bordes en la creación.", + "edge-template-root": "Raíz de plantilla", + "search": "Cadenas de reglas de búsqueda", + "selected-rulechains": "{count, plural, 1 {1 cadena de reglas} otras {# cadenas de reglas}} seleccionadas", + "open-rulechain": "Cadena de reglas abierta", + "assign-to-edge": "Asignar a Edge", + "edge-rulechain": "Cadena de regla de borde" }, "rulenode": { "details": "Detalles", diff --git a/ui-ngx/src/assets/locale/locale.constant-fa_IR.json b/ui-ngx/src/assets/locale/locale.constant-fa_IR.json index 5327f0b33b..90a10f765c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-fa_IR.json +++ b/ui-ngx/src/assets/locale/locale.constant-fa_IR.json @@ -881,7 +881,6 @@ "body": "بدنه", "method": "روش", "type": "نوع", - "entity": "موجودي", "message-id": "پيام ID", "message-type": "نوع پيام", "data-type": "نوع داده", @@ -893,7 +892,9 @@ "success": "موفقيت", "failed": "عدم موفقيت", "messages-processed": "پيام پردازش شد", - "errors-occurred": "خطاها رخ دادند" + "errors-occurred": "خطاها رخ دادند", + "all-events": "همه", + "entity-type": "نوع موجودي" }, "extension": { "extensions": "دنباله ها", diff --git a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json index 8ab40b2cfe..d3a6cf51a5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json +++ b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json @@ -190,9 +190,13 @@ "filter-type-entity-name": "Nom d'entité", "filter-type-entity-view-search-query": "Requête de recherche vue d'entité", "filter-type-entity-view-search-query-description": "Vues d'entité avec les types {{entityViewTypes}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-edge-search-query": "Requête de recherche de bordure", + "filter-type-edge-search-query-description": "Bordures de types {{edgeTypes}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-entity-view-type": "Type de vue d'entité", "filter-type-entity-view-type-and-name-description": "Vues d'entité de type '{{entityView}}' et dont le nom commence par '{{prefix}}'", "filter-type-entity-view-type-description": "Vues d'entité de type '{{entityView}}'", + "filter-type-edge-type": "Type de la bordure", + "filter-type-edge-type-description": "Dispositifs de type '{{edgeType}}'", "filter-type-relations-query": "Interrogation des relations", "filter-type-relations-query-description": "{{entities}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-required": "Le type de filtre est requis.", @@ -273,7 +277,15 @@ "unassign-assets-title": "Êtes-vous sûr de vouloir retirer l'attribution de {count, plural, 1 {1 asset} other {# assets} }?", "unassign-from-customer": "Retirer du client", "view-assets": "Afficher les actifs", - "label": "Label" + "label": "Label", + "assign-asset-to-edge": "Attribuer des actifs a la bordure", + "assign-asset-to-edge-text": "Veuillez sélectionner les actifs à attribuer a la bordure", + "unassign-asset-from-edge": "Retirer de la bordure", + "unassign-asset-from-edge-title": "Êtes-vous sûr de vouloir retirer l'attribution de l'actif '{{assetName}}'?", + "unassign-asset-from-edge-text": "Après la confirmation, l'actif sera non attribué et ne sera pas accessible a la bordure.", + "unassign-assets-from-edge-action-title": "Retirer {count, plural, 1 {1 asset} other {# assets}} de la bordure", + "unassign-assets-from-edge-title": "Êtes-vous sûr de vouloir retirer l'attribution de {count, plural, 1 {1 asset} other {# assets}}?", + "unassign-assets-from-edge-text": "Après la confirmation, tous les actifs sélectionnés ne seront pas attribués et ne seront pas accessibles a la bordure." }, "attribute": { "add": "Ajouter un attribut", @@ -324,6 +336,8 @@ "type-alarm-ack": "Acquitté", "type-alarm-clear": "Effacé", "type-assigned-to-customer": "Attribué au client", + "type-assigned-to-edge": "Attribué a la bordure", + "type-unassigned-from-edge": "Non attribué de la bordure", "type-attributes-deleted": "Attributs supprimés", "type-attributes-read": "Attributs lus", "type-attributes-updated": "Attributs mis à jour", @@ -407,11 +421,13 @@ "description": "Description", "details": "Détails", "devices": "Dispositifs du client", + "edges": "Bordures du client", "entity-views": "Vues de l'entité client", "events": "Événements", "idCopiedMessage": "L'Id du client a été copié dans le presse-papier", "manage-assets": "Gérer les actifs", "manage-customer-assets": "Gérer les actifs du client", + "manage-customer-edges": "Gérer les bordures du client", "manage-customer-dashboards": "Gérer les tableaux de bord du client", "manage-customer-devices": "Gérer les dispositifs du client", "manage-customer-users": "Gérer les utilisateurs du client", @@ -420,6 +436,7 @@ "manage-public-assets": "Gérer les actifs publics", "manage-public-dashboards": "Gérer les tableaux de bord publics", "manage-public-devices": "Gérer les dispositifs publics", + "manage-public-edges": "Gérer les bordures publics", "manage-users": "Gérer les utilisateurs", "management": "Gestion des clients", "no-customers-matching": "Aucun client correspondant à '{{entity}} n'a été trouvé.", @@ -428,6 +445,7 @@ "public-dashboards": "Tableaux de bord publics", "public-devices": "Dispositifs publics", "public-entity-views": "Vues d'entités publiques", + "public-edges": "Bordures publics", "select-customer": "Sélectionner un client", "select-default-customer": "Sélectionnez le client par défaut", "title": "Titre", @@ -572,7 +590,11 @@ "view-dashboards": "Afficher les tableaux de bord", "widget-file": "Fichier du Widget", "widget-import-missing-aliases-title": "Configurer les alias utilisés par le widget importé", - "widgets-margins": "Marge entre les widgets" + "widgets-margins": "Marge entre les widgets", + "unassign-dashboard-from-edge-text": "Après la confirmation, tableau de bord sera non attribué et ne sera pas accessible a la bordure.", + "unassign-dashboards-from-edge-text": "Après la confirmation, tous les tableaux de bord sélectionnés ne seront pas attribués et ne seront pas accessibles a la bordure.", + "assign-dashboard-to-edge": "Attribuer des tableaux de bord a la bordure", + "assign-dashboard-to-edge-text": "Veuillez sélectionner la bordure pour attribuer le ou les tableaux de bord" }, "datakey": { "advanced": "Avancé", @@ -710,11 +732,159 @@ "unassign-from-customer": "Retirer du client", "use-device-name-filter": "Utiliser le filtre", "view-credentials": "Afficher les informations d'identification", - "view-devices": "Afficher les dispositifs" + "view-devices": "Afficher les dispositifs", + "assign-device-to-edge-text":"Veuillez sélectionner la bordure pour attribuer le ou les dispositifs", + "unassign-device-from-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{deviceName}} '?", + "unassign-device-from-edge-text": "Après la confirmation, dispositif sera non attribué et ne sera pas accessible a la bordure.", + "unassign-devices-from-edge-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 device} other {# devices}}?", + "unassign-devices-from-edge-text": "Après la confirmation, tous les dispositifs sélectionnés ne seront pas attribues et ne seront pas accessibles par la bordure." }, "dialog": { "close": "Fermer le dialogue" }, + "edge": { + "edge": "Bordure", + "edge-instances": "Instances de Bord", + "management": "Gestion des bordures", + "no-edges-matching": "Aucun bordure correspondant à {{entity}} n'a été trouvé.", + "rulechain-templates": "Modèles de chaîne de règles", + "add": "Ajouter un bordure", + "no-edges-text": "Aucun bordure trouvé", + "edge-details": "Détails de la bordure", + "add-edge-text": "Ajouter une nouveau bordure", + "delete": "Supprimer la bordure", + "delete-edges": "Supprimer les bordures", + "delete-edge-title": "Êtes-vous sûr de vouloir supprimer la bordure '{{edgeName}}'?", + "delete-edge-text": "Faites attention, après la confirmation, la bordure et toutes les données associées deviendront irrécupérables", + "delete-edges-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 bordure} other {# bordure}}?", + "delete-edges-text": "Faites attention, après la confirmation, tous les bordures sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "name": "Nom", + "name-required": "Le nom de la bordure est requis", + "edge-license-key": "Edge Clé de licence", + "edge-license-key-required": "La edge clé de licence est requise", + "cloud-endpoint": "Clé de licence", + "cloud-endpoint-required": "La clé de licence est requise", + "description": "Dispositifs", + "events": "Événements", + "details": "Détails de l'entité", + "copy-id": "Copier borudre Id", + "id-copied-message": "Id de la bordure a été copié dans le presse-papier", + "sync": "Sync Edge", + "sync-message": "Edge a été synchronisé", + "edge-required": "Bordure est requise", + "edge-type": "Type de la bordure", + "edge-type-required": "Type de la bordure est requise.", + "select-edge-type": "Selectionner un type de la bordure", + "assign-to-customer": "Attribuer au client", + "assign-to-customer-text": "Veuillez sélectionner la bordure pour attribuer le ou les dispositifs", + "assign-edge-to-customer": "Attribuer la bordure au client", + "assign-edge-to-customer-text": "Veuillez sélectionner la bordure pour attribuer le ou les dispositifs", + "assigned-to-customer": "Attribué au client", + "unassign-from-customer": "Retirer du client", + "assign-edges-text": "Attribuer {count, plural, 1 {1 bordure} other {# bordures}} au client", + "unassign-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{edgeName}}", + "unassign-edge-text": "Après la confirmation, le dispositif ne sera pas attribué et ne sera pas accessible au client", + "make-public": "Make edge public", + "make-public-edge-title": "Are you sure you want to make the edge '{{edgeName}}' public?", + "make-public-edge-text": "After the confirmation the edge and all its data will be made public and accessible by others.", + "make-private": "Rendre public Edge", + "public": "Public", + "make-private-edge-title": "Are you sure you want to make the edge '{{edgeName}}' private?", + "make-private-edge-text": "Après la confirmation, la bordure et toutes ses données seront rendues privées et ne seront pas accessibles par d'autres", + "import": "Importer bordure", + "label": "Etiquette", + "assign-new-edge": "Attribuer un nouvel bordure", + "manage-edge-dashboards": "Gérer les tableaux de bord", + "unassign-from-edge": "Retirer de la bordure", + "dashboards": "Tableau de bord de la bordure", + "manage-edge-rulechains": "Gérer les chaînes de règles", + "rulechains": "Chaînes de règles de la bordure", + "edge-key": "Clé de la bordure", + "copy-edge-key": "Copier clé de la bordure", + "edge-key-copied-message": "Clé de la bordure a été copié dans le presse-papier", + "edge-secret": "Secret de la bordure", + "copy-edge-secret": "Copier secret de la bordure", + "edge-secret-copied-message": "Secret de la bordure a été copié dans le presse-papier", + "manage-edge-assets": "Gérer les actifs de la bordure", + "manage-edge-devices": "Gérer les dispositifs de la bordure", + "manage-edge-entity-views": "Vues de l'entité vues de l'entité", + "assets": "Actifs de la bordure", + "devices": "Dispositifs de la bordure", + "entity-views": "Vues de l'entité bordure", + "entity-id": "ID d'entité", + "event-action": "Action d'événement", + "load-entity-error": "Entité introuvable. Échec du chargement des informations", + "unassign-edges-text": "Après la confirmation, tous les bordures sélectionnés ne seront plus attribués et ne seront pas accessibles par le client.", + "unassign-edges-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 bordure} other {# bordures}}?", + "edge-file": "Fichier Edge", + "edge-rulechains": "Chaînes de règles Edge", + "name-starts-with": "Le nom du bord commence par", + "edge-license-key-hint": "Pour obtenir votre licence, accédez à la page de tarification and select the best license option for your case.", + "cloud-endpoint-hint": "Edge nécessite un accès HTTP (s) au Cloud (ThingsBoard CE / PE) pour vérifier la clé de licence. Veuillez spécifier l'URL du cloud à laquelle Edge peut se connecter.", + "assignedToCustomer": "Attribué au client", + "edge-public": "Edge est public", + "set-root-rule-chain-text": "Veuillez sélectionner la chaîne de règles racine pour les arêtes", + "set-root-rule-chain-to-edges": "Définir la chaîne de règles racine pour Edge (s)", + "set-root-rule-chain-to-edges-text": "Définir la chaîne de règles racine pour {count, plural, 1 {1 edge} other {# edges}}", + "search": "Rechercher les bords", + "selected-edges": "{count, plural, 1 {1 edge} other {# bords}} sélectionné", + "any-edge": "Tout bord", + "no-edge-types-matching": "Aucun type d'arête correspondant à \"{{entitySubtype}}\" n'a été trouvé.", + "edge-type-list-empty": "Aucun type d'arête sélectionné.", + "edge-types": "Types de bords", + "dashboard": "Tableau de bord Edge", + "enter-edge-type": "Entrez le type d'arête", + "deployed": "Déployé", + "pending": "En attente", + "downlinks": "Liens descendants", + "no-downlinks-prompt": "Aucun lien descendant trouvé", + "sync-process-started-successfully": "Le processus de synchronisation a démarré avec succès!", + "missing-related-rule-chains-title": "Edge n'a pas de chaîne (s) de règles associées", + "missing-related-rule-chains-text": "Les chaînes de règles affectées aux tronçons utilisent des nœuds de règles qui transfèrent les messages vers les chaînes de règles non affectées à ce tronçon.

Liste des chaînes de règles manquantes:
{{missingRuleChains}}", + "widget-datasource-error": "Ce widget prend en charge uniquement la source de données d'entité EDGE" + }, + "edge-event": { + "type-dashboard": "Dashboard", + "type-asset": "Asset", + "type-device": "Device", + "type-device-profile": "Device Profile", + "type-entity-view": "Entity View", + "type-alarm": "Alarm", + "type-rule-chain": "Rule Chain", + "type-rule-chain-metadata": "Rule Chain Metadata", + "type-edge": "Edge", + "type-entity-group": "Entity Group", + "type-scheduler-event": "Scheduler Event", + "type-white-labeling": "White Labeling", + "type-login-white-labeling": "White Labeling Login", + "type-user": "User", + "type-tenant": "Tenant", + "type-customer": "Customer", + "type-custom-translation": "Custom Translation", + "type-relation": "Relation", + "type-widgets-bundle": "Widgets Bundle", + "type-widgets-type": "Widgets Type", + "type-admin-settings": "Admin Settings", + "action-type-added": "Added", + "action-type-deleted": "Deleted", + "action-type-updated": "Updated", + "action-type-post-attributes": "Post Attributes", + "action-type-attributes-updated": "Attributes Updated", + "action-type-attributes-deleted": "Attributes Deleted", + "action-type-timeseries-updated": "Timeseries Updated", + "action-type-credentials-updated": "Credentials Updated", + "action-type-assigned-to-customer": "Assigned to Customer", + "action-type-unassigned-from-customer": "Unassigned from Customer", + "action-type-relation-add-or-update": "Relation Add or Update", + "action-type-relation-deleted": "Relation Deleted", + "action-type-rpc-call": "RPC Call", + "action-type-alarm-ack": "Alarm Ack", + "action-type-alarm-clear": "Alarm Clear", + "action-type-assigned-to-edge": "Assigned to Edge", + "action-type-unassigned-from-edge": "Unassigned from Edge", + "action-type-credentials-request": "Credentials Request", + "action-type-entity-merge-request": "Entity Merge Request" + }, "entity": { "add-alias": "Ajouter un alias d'entité", "alarm-name-starts-with": "Les actifs dont le nom commence par '{{prefix}}'", @@ -775,6 +945,10 @@ "rule-name-starts-with": "Régles dont les noms commencent par '{{prefix}}'", "rulechain-name-starts-with": "Chaînes de régles dont les noms commencent par '{{prefix}}'", "rulenode-name-starts-with": "Les noeuds de régles dont le nom commence par '{{prefix}}'", + "type-edge": "Bordure", + "type-edges": "Bordures", + "list-of-edges": "{ count, plural, 1 {Une bordure} other {List of # bordures} }", + "edge-name-starts-with": "Bordures dont les noms commencent par '{{prefix}}'", "search": "Recherche d'entités", "select-entities": "Sélectionner des entités", "selected-entities": "{count, plural, 1 {1 entité} other {# entités} } sélectionnées", @@ -885,6 +1059,14 @@ "make-public": "Rendre la vue d'entité publique", "make-public-entity-view-text": "Après la confirmation, la vue de l'entité et toutes ses données seront rendues publiques et accessibles à d'autres", "make-public-entity-view-title": "Voulez-vous vraiment que la vue de l'entité '{{entityViewName}}' soit publique?", + "assign-entity-view-to-edge": "Attribuer a la bordure", + "assign-entity-view-to-edge-text":"Veuillez sélectionner la bordure auquel attribuer la ou les vues d'entité.", + "unassign-entity-view-from-edge-title": "Voulez-vous vraiment annuler l'attribution de la vue d'entité '{{entityViewName}}'?", + "unassign-entity-view-from-edge-text": "Après la confirmation, la vue de l'entité sera non attribuée et ne sera pas accessible par la bordure.", + "unassign-entity-views-from-edge-action-title": "Annuler l'attribution { count, plural, 1 {1 entityView} other {# entityViews} } de la bordure", + "unassign-entity-view-from-edge": "Annuler l'attribution des vues d'entité", + "unassign-entity-views-from-edge-title": "Êtes-vous sûr de vouloir annuler l'attribution { count, plural, 1 {1 entityView} other {# entityViews} }?", + "unassign-entity-views-from-edge-text": "Après la confirmation, toutes les vues des entités sélectionnées seront non attribuées et ne seront pas accessibles par la bordure.", "management": "Gestion de vue d'entité", "name": "Nom", "name-required": "Un nom est requis.", @@ -933,8 +1115,8 @@ "body": "Corps", "data": "Données", "data-type": "Type de données", - "entity": "Entité", "error": "erreur", + "type-edge-event": "Downlink", "errors-occurred": "Des erreurs sont survenues", "event": "événement", "event-time": "Heure de l'événement", @@ -955,7 +1137,9 @@ "type-debug-rule-node": "Debug", "type-error": "Erreur", "type-lc-event": "Evénement du cycle de vie", - "type-stats": "Statistiques" + "type-stats": "Statistiques", + "all-events": "Tout", + "entity-type": "Type d'entité" }, "extension": { "add": "Ajouter une extension", @@ -1302,7 +1486,35 @@ "set-root": "Rend la chaîne de règles racine (root) ", "set-root-rulechain-text": "Après la confirmation, la chaîne de règles deviendra racine (root) et gérera tous les messages de transport entrants.", "set-root-rulechain-title": "Voulez-vous vraiment que la chaîne de règles '{{ruleChainName}} soit racine (root) ?", - "system": "Système" + "system": "Système", + "assign-rulechains": "Attribuer aux chaînes de règles", + "assign-new-rulechain": "Attribuer une nouvele chaînes de règles", + "delete-rulechains": "Supprimer une chaînes de règles", + "unassign-rulechain": "Retirer chaîne de règles", + "unassign-rulechains": "Retirer chaînes de règles", + "unassign-rulechain-title": "AÊtes-vous sûr de vouloir retirer l'attribution de chaînes de règles '{{ruleChainTitle}}'?", + "unassign-rulechain-from-edge-text": "Après la confirmation, l'actif sera non attribué et ne sera pas accessible a la bordure.", + "unassign-rulechains-from-edge-action-title": "Retirer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles}} de la bordure", + "unassign-rulechains-from-edge-text": "Après la confirmation, tous les chaînes de règles sélectionnés ne seront pas attribués et ne seront pas accessibles a la bordure.", + "assign-rulechain-to-edge-title": "Attribuer les chaînes de règles a la bordure", + "assign-rulechain-to-edge-text": "Veuillez sélectionner la bordure pour attribuer le ou les chaînes de règles", + "set-edge-template-root-rulechain": "Rendre le modèle de bord de chaîne de règles racine", + "set-edge-template-root-rulechain-title": "Voulez-vous vraiment définir la racine du modèle d'arête de la chaîne de règles '{{ruleChainName}}'?", + "set-edge-template-root-rulechain-text": "Après la confirmation, la chaîne de règles deviendra la racine du modèle d'arête et sera la chaîne de règles racine pour les arêtes nouvellement créées.", + "invalid-rulechain-type-error": "Impossible d'importer la chaîne de règles: type de chaîne de règles non valide. Le type attendu est {{attenduRuleChainType}}.", + "set-auto-assign-to-edge": "Attribuer une chaîne de règles aux arêtes lors de la création", + "set-auto-assign-to-edge-title": "Voulez-vous vraiment attribuer automatiquement la chaîne de règles d'arête '{{ruleChainName}}' à l'arête (s) lors de la création?", + "set-auto-assign-to-edge-text": "Après la confirmation, la chaîne de règles d'arête sera automatiquement affectée à l'arête (s) lors de la création.", + "unset-auto-assign-to-edge": "Non défini, attribuer une chaîne de règles aux arêtes lors de la création", + "unset-auto-assign-to-edge-title": "Voulez-vous vraiment annuler l'attribution de la chaîne de règles d'arête \"{{ruleChainName}}\" aux arêtes lors de la création?", + "unset-auto-assign-to-edge-text": "Après la confirmation, la chaîne de règles d'arêtes ne sera plus automatiquement affectée aux arêtes lors de la création.", + "edge-template-root": "Racine du modèle", + "search": "Rechercher des chaînes de règles", + "selected-rulechains": "{count, plural, 1 {1 rule chain} other {# rule chains}} sélectionné", + "open-rulechain": "Chaîne de règles ouverte", + "assign-to-edge": "Attribuer à Edge", + "edge-rulechain": "Chaîne de règles Edge", + "unassign-rulechains-from-edge-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 rulechain} other {# rulechains}}?" }, "rulenode": { "add": "Ajouter un noeud de règle", diff --git a/ui-ngx/src/assets/locale/locale.constant-it_IT.json b/ui-ngx/src/assets/locale/locale.constant-it_IT.json index 363801ebc3..4050e48ad7 100644 --- a/ui-ngx/src/assets/locale/locale.constant-it_IT.json +++ b/ui-ngx/src/assets/locale/locale.constant-it_IT.json @@ -915,7 +915,6 @@ "body": "Body", "method": "Metodo", "type": "Tipo", - "entity": "Entità", "message-id": "Id Messaggio", "message-type": "Tipo Messaggio", "data-type": "Tipo di dato", @@ -927,7 +926,9 @@ "success": "Success", "failed": "Failed", "messages-processed": "Messaggi elaborati", - "errors-occurred": "Si sono verificati degli errori" + "errors-occurred": "Si sono verificati degli errori", + "all-events": "Tutte", + "entity-type": "Tipo entità" }, "extension": { "extensions": "Estensioni", diff --git a/ui-ngx/src/assets/locale/locale.constant-ja_JA.json b/ui-ngx/src/assets/locale/locale.constant-ja_JA.json index 88d666423c..f3696e9a23 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ja_JA.json +++ b/ui-ngx/src/assets/locale/locale.constant-ja_JA.json @@ -768,7 +768,6 @@ "body": "体", "method": "方法", "type": "タイプ", - "entity": "エンティティ", "message-id": "メッセージID", "message-type": "メッセージタイプ", "data-type": "データ・タイプ", @@ -780,7 +779,9 @@ "success": "成功", "failed": "失敗", "messages-processed": "処理されたメッセージ", - "errors-occurred": "エラーが発生しました" + "errors-occurred": "エラーが発生しました", + "all-events": "すべて", + "entity-type": "エンティティタイプ" }, "extension": { "extensions": "拡張機能", diff --git a/ui-ngx/src/assets/locale/locale.constant-ka_GE.json b/ui-ngx/src/assets/locale/locale.constant-ka_GE.json index 06bde1bb02..6f5ba73674 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ka_GE.json +++ b/ui-ngx/src/assets/locale/locale.constant-ka_GE.json @@ -959,7 +959,6 @@ "body": "სხეული", "method": "მეთოდი", "type": "ტიპი", - "entity": "ობიექტი", "message-id": "მესიჯის-ID", "message-type": "მესიჯის ტიპი", "data-type": "მონაცემთა ტიპი", @@ -971,7 +970,9 @@ "success": "წარმატება", "failed": "ვერ მოხერხდა", "messages-processed": "შეტყობინებების დამუშავება", - "errors-occurred": "შეცდომები მოხდა" + "errors-occurred": "შეცდომები მოხდა", + "all-events": "ყველა", + "entity-type": "ობიექტის ტიპი" }, "extension": { "extensions": "დამატებითი აპი", diff --git a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json index f52c94a0f0..cbe962bc2a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json +++ b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json @@ -1329,7 +1329,6 @@ "body": "Body", "method": "방법", "type": "유형", - "entity": "개체", "message-id": "메시지 ID", "message-type": "메시지 유형", "data-type": "데이터 유형", @@ -1341,7 +1340,9 @@ "success": "성공", "failed": "실패", "messages-processed": "처리된 메시지", - "errors-occurred": "오류가 발생했습니다" + "errors-occurred": "오류가 발생했습니다", + "all-events": "모두", + "entity-type": "개체 유형" }, "extension": { "extensions": "확장", diff --git a/ui-ngx/src/assets/locale/locale.constant-lv_LV.json b/ui-ngx/src/assets/locale/locale.constant-lv_LV.json index 76282308e7..c2b7ce6e79 100644 --- a/ui-ngx/src/assets/locale/locale.constant-lv_LV.json +++ b/ui-ngx/src/assets/locale/locale.constant-lv_LV.json @@ -894,7 +894,6 @@ "body": "Galvenā daļa", "method": "Metode", "type": "Tips", - "entity": "Vienība", "message-id": "Ziņojuma Id", "message-type": "Ziņojuma tips", "data-type": "Datu tips", @@ -906,7 +905,9 @@ "success": "Sekmīgi", "failed": "Kļūda", "messages-processed": "Ziņojumi apstrādāti", - "errors-occurred": "Kļūdas konstatētas" + "errors-occurred": "Kļūdas konstatētas", + "all-events": "Visi", + "entity-type": "Vienības tips" }, "extension": { "extensions": "Paplašinājumi", diff --git a/ui-ngx/src/assets/locale/locale.constant-pt_BR.json b/ui-ngx/src/assets/locale/locale.constant-pt_BR.json index 325229b47b..7f5c7ce239 100644 --- a/ui-ngx/src/assets/locale/locale.constant-pt_BR.json +++ b/ui-ngx/src/assets/locale/locale.constant-pt_BR.json @@ -997,7 +997,6 @@ "body": "Corpo", "method": "Método", "type": "Tipo", - "entity": "Entidade", "message-id": "ID de mensagem", "message-type": "Tipo de Mensagem", "data-type": "Tipo de Dados", @@ -1009,7 +1008,9 @@ "success": "Êxito", "failed": "Falhou", "messages-processed": "Mensagens processadas", - "errors-occurred": "Erros" + "errors-occurred": "Erros", + "all-events": "Tudo", + "entity-type": "Tipo de entidade" }, "extension": { "extensions": "Extensões", diff --git a/ui-ngx/src/assets/locale/locale.constant-ro_RO.json b/ui-ngx/src/assets/locale/locale.constant-ro_RO.json index 971ad02d00..80c9941f2a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ro_RO.json +++ b/ui-ngx/src/assets/locale/locale.constant-ro_RO.json @@ -945,7 +945,6 @@ "body": "Corp", "method": "Metodă", "type": "Tip", - "entity": "Entitate", "message-id": "ID Mesaj", "message-type": "Tip Mesaj", "data-type": "Tip Date", @@ -957,7 +956,9 @@ "success": "Succes", "failed": "Eşuat", "messages-processed": "Mesaje procesate", - "errors-occurred": "Au apărut erori" + "errors-occurred": "Au apărut erori", + "all-events": "Toate", + "entity-type": "Tip Entitate" }, "extension": { "extensions": "Extensii", @@ -1174,7 +1175,7 @@ "entity-field": "Câmp Entitate", "access-token": "Token De Acces" }, - "stepper-text":{ + "stepper-text": { "select-file": "Selectează un fişier", "configuration": "Importă configurație", "column-type": "Selectează tipul de coloane", @@ -1796,4 +1797,4 @@ "language": { "language": "Limba" } -} +} \ No newline at end of file diff --git a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json index a78416a201..f015d01b18 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json +++ b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json @@ -948,7 +948,6 @@ "body": "Тело", "method": "Метод", "type": "Тип", - "entity": "Объект", "message-id": "ИД сообщения", "message-type": "Тип сообщения", "data-type": "Тип данных", @@ -960,7 +959,9 @@ "success": "Успех", "failed": "Неудача", "messages-processed": "Сообщения обработаны", - "errors-occurred": "Возникли ошибки" + "errors-occurred": "Возникли ошибки", + "all-events": "Все", + "entity-type": "Тип объекта" }, "extension": { "extensions": "Расширение", diff --git a/ui-ngx/src/assets/locale/locale.constant-sl_SI.json b/ui-ngx/src/assets/locale/locale.constant-sl_SI.json index 4dffbc6be2..7dd60aaa2f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-sl_SI.json +++ b/ui-ngx/src/assets/locale/locale.constant-sl_SI.json @@ -1329,7 +1329,6 @@ "body": "Vsebina", "method": "Metoda", "type": "Vrsta", - "entity": "Entiteta", "message-id": "ID sporočila", "message-type": "Vrsta sporočila", "data-type": "Vrsta podatkov", @@ -1341,7 +1340,9 @@ "success": "Uspeh", "failed": "Ni uspelo", "messages-processed": "Obdelana sporočila", - "errors-occurred": "Prišlo je do napak" + "errors-occurred": "Prišlo je do napak", + "all-events": "Vse", + "entity-type": "Vrsta entitete" }, "extension": { "extensions": "Razširitve", diff --git a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json index 06d259dace..dfe3dbbaef 100644 --- a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json +++ b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json @@ -915,7 +915,6 @@ "body": "İçerik //(Body)", "method": "Yöntem", "type": "Tür", - "entity": "Varlık", "message-id": "Mesaj Kimliği", "message-type": "Mesaj tipi", "data-type": "Veri tipi", @@ -927,7 +926,9 @@ "success": "Başarı", "failed": "Başarısız oldu", "messages-processed": "Mesajlar işlendi", - "errors-occurred": "Hatalar oluştu" + "errors-occurred": "Hatalar oluştu", + "all-events": "Tümü", + "entity-type": "Öğe türü" }, "extension": { "extensions": "Uzantılar", diff --git a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json index f9f33d48b9..c45b19ad5f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json +++ b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json @@ -1201,7 +1201,6 @@ "out": "Out", "metadata": "Метадані", "message": "Повідомлення", - "entity": "Сутність", "message-id": "Id повідомлення", "message-type": "Тип повідомлення", "data-type": "Тип даних", @@ -1212,7 +1211,9 @@ "success": "Успіх", "failed": "Невдача", "messages-processed": "Повідомлення опрацьовані", - "errors-occurred": "Виникли помилки" + "errors-occurred": "Виникли помилки", + "all-events": "Всі", + "entity-type": "Тип сутності" }, "extension": { "extensions": "Розширення", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index bbd7f7b61d..b7a3078c3b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -1334,7 +1334,6 @@ "body": "整体", "data": "数据", "data-type": "数据类型", - "entity": "实体", "error": "错误", "errors-occurred": "错误发生", "event": "事件", @@ -1356,7 +1355,9 @@ "type-debug-rule-node": "调试", "type-error": "错误", "type-lc-event": "生命周期事件", - "type-stats": "类型统计" + "type-stats": "类型统计", + "all-events": "全部", + "entity-type": "实体类型" }, "extension": { "add": "添加扩展", @@ -1943,7 +1944,7 @@ "selected-rulechains": "已选择 { count, plural, 1 {# 个规则链} other {# 个规则链} }", "set-root": "设置为根规则链", "set-root-rulechain-text": "确认之后,规则链将变为根规格链,并将处理所有传入的传输消息。", - "set-root-rulechain-title": "您确定要生成规则链'{{RuleChainName}}'根吗?", + "set-root-rulechain-title": "您确定要生成规则链'{{ruleChainName}}'根吗?", "system": "系统" }, "rulenode": { diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_TW.json b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json index 14f4268724..bc8374a1c1 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_TW.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json @@ -865,7 +865,6 @@ "body": "整體", "method": "方法", "type": "類型", - "entity": "實體", "message-id": "消息ID", "message-type": "消息類型", "data-type": "資料類型", @@ -877,7 +876,9 @@ "success": "成功", "failed": "失敗", "messages-processed": "消息處理", - "errors-occurred": "錯誤發生" + "errors-occurred": "錯誤發生", + "all-events": "所有", + "entity-type": "實體類型" }, "extension": { "extensions": "擴展",