diff --git a/application/pom.xml b/application/pom.xml
index f473986211..fe235cc85c 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -361,6 +361,10 @@
com.slack.api
slack-api-client
+
+ com.google.oauth-client
+ google-oauth-client
+
diff --git a/application/src/main/data/certs/azure/BaltimoreCyberTrustRoot.crt.pem b/application/src/main/data/certs/azure/BaltimoreCyberTrustRoot.crt.pem
deleted file mode 100644
index 2bd16ebd47..0000000000
--- a/application/src/main/data/certs/azure/BaltimoreCyberTrustRoot.crt.pem
+++ /dev/null
@@ -1,22 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ
-RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD
-VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX
-DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y
-ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy
-VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr
-mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr
-IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK
-mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu
-XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy
-dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye
-jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1
-BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3
-DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92
-9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx
-jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0
-Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz
-ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS
-R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp
------END CERTIFICATE-----
-
diff --git a/application/src/main/data/certs/azure/DigiCertGlobalRootG2.crt.pem b/application/src/main/data/certs/azure/DigiCertGlobalRootG2.crt.pem
new file mode 100644
index 0000000000..798e002751
--- /dev/null
+++ b/application/src/main/data/certs/azure/DigiCertGlobalRootG2.crt.pem
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
+MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT
+MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
+b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI
+2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx
+1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ
+q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz
+tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ
+vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP
+BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV
+5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY
+1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4
+NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG
+Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91
+8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe
+pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl
+MrY=
+-----END CERTIFICATE-----
diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json
index 4930219741..473d3d215b 100644
--- a/application/src/main/data/json/system/widget_bundles/cards.json
+++ b/application/src/main/data/json/system/widget_bundles/cards.json
@@ -3,7 +3,9 @@
"alias": "cards",
"title": "Cards",
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAT5UlEQVR42u3d95MUVffHcf86/UEtqwxVapmzaH0VE2YR82PAgBkTKCYUAyrGR0mSxERwBXRV0EdFRcAc5vvaPnptZ5bdZWbDwJ5TW1s93T23b5/7vufe7t3PuXv8+OOP36YNZrzUqCzdNUR37fHdd9810gaz4qV01xDdlWAlWAlWgpVgJVgJVnoqwUqwEqwEK8FKsNISrAQrwdqlwfrtt9/+V7Off/55gIJ++OGH8m46bOvWrb/88sv4Aeubb76pu8vb5z/++MPvEaqPwocL8RUrVqhwfc8rr7zSYdsNBFZvb+/hhx/+n79t/fr1ree8+eabn376qY03KrPx2muvff311zaeffbZd955Z/yANWvWLF76v8psPPLII19++eVdd901QvX57LPP7rvvvqGfv3nz5ldffbXfQzfffPPy5cvre9zC999/P4JgnXfeefU9n3/+uX6pEkHMxo0bL7vsMh5UCU786quvfGXSpElz584VwByNkvUtX3nrrbdsRDmrVq1auHDhtm3bdr+h8LHHHpszZ05s//TTT9EbN2zYwG+LFy/mH+OACMGBf/75Z5zW09Pj0Pbt2wsxuuimTZvi46+//qr3vvvuu3H+hx9++MUXX/g6D3/88cdxzkcffbRo0aItW7bER+FHCa5VamW0ef75588555z4iootW7Zs5cqV0SLA0hyuUmJHAUutHOo3pnQElqpsq8xt2DNjxowzzjjjySefPP/8819++eUPPvjAxh133AGpJ5544plnnnHDp512mtNUa/r06QsWLOCOKVOmPPTQQ7rXDTfcoJCHH374+uuvd/KECROaRs/dDCwOvPjii21Mnjz58ssvF8JPPvnkK664wsYll1zCAw5xpvD21FNPcazRR5fj8xdeeIEbuff333/nYWXedttt/On8s88++8ILL5w3b56jV155ZQxbPKxMNJh+QEdRgVGJQ1pw5syZTnj77beRfdZZZ6nk7bffrtgAK/b4CowKWO5UUUoWPlxlOME69NBDL67MtQOsl156yYYa33rrrTbUbMmSJTYCLBvuVhe0EWDpYVwT0w5u1VduvPFG96AP6cfjByxBJToVkmysXr0aTwLGUUcdJbRzztSpU5cuXWq0uvbaa83MvqyMn/nTUVHq2GOP1UuBZdxQQgHrxBNPVLhzOFxv95VLL73UdxVSwh5bt24duG3A1xUVog5aJMBy6agwagtYjz/+OByV7FqYG8GhEFgxThvXArVBwRJgTznllFv/NtUFkxqrqI965DgBK8YgNy4AFCy44pBDDinO0dLQMUlFhpBmVqQbT5w4sZyACWAZH+pg1UuIEGVYdEh/BlArWPo2ejTlo48+esIJJ9TnWFoHUgUsE0T1j5IRNqpgCafmB01grV27toCl0winMZbHfZqTRaDiPvFsPINlQ8ww/bIhxhikjEQIiCFy9uzZTlNCzK7Ce61gnXnmmfH8BETjgAnce++9F0PkvffeWypmnsThNhwVLG1omuOPPz7AevHFF+Px8Oqrry5gGXDvueeemCVHmBz+oZC9//77rWC9/vrrpkpr1qwpYHGr+YFbDbDCR27eXfkKH82fP98E30fu3oXeR4wQWGalp59+ulgiRCn5k08+4RyH9EazeCc88MADorsSoo1bwdI5nawEcSgGULHKIcXG0FHeB8HIo6sN57uKrxx88MGg0S6TK0N51DPAAjrO3IICQTbaL0iFn/KAU+6h6RxDnlqWj84XkPMFaf2pbQAHct2gc4amElobJcopPbnV/w6Vx/ah7M837/nmvcvevKclWAlWgpVgJVgJVlqClWAlWAlWgpVgpSVYCVaClWAlWAlWWoKVYCVYCVaClWClJVgJVoKVYCVYCVZ6KsFKsBKscQPWJ5UlWLu9o0YbrP9W1t53y/9c07iFfr+9QuirmnYOXX+2s2BRMI++o9qwej2JYjhk0JoPxWkDgUU8FCITOl1Ky7ECi+6FaOTUU0+lCrdNv9+2B4877rj6HhIU0hRyFAKkzsG66qqrjj766IMOOogSkDyGym2swKK9adoYwOiHi+yCzDAUxdH6/Z5PpnXNNddQZIVQu02w9ttvP7LBAIsMje8oxOm4faQZuuiii8iJzj33XJo1whISXoqipvwqRRZS99fQe7Oq0wq7c7o2mklg0S25or5FBYUM6m+6PDV0txRzNGf2kPD7Lpn5BRdcQG3mctddd50TmsBypv0kWdRswxKxyN1CIM5X9HCK1UihkfebcN4h2QbotEL6N4yO2hFYio3EJKTFEjG4XzozLct12ksUnzZtWqMSe+pjBx54ILDsIZrdZ599MHTLLbfEd4sKyL3wtj0kpe1HLKWg2FWRRIYq1YTGIG7kGp1S0dzHCza0qPPvv//+JtKlbAj5dvGXwDP0NClaiCaxHr0Y7TV00KxAnlIHbqLPNNKR3WGRQ2EnfghFpJHcpIER3wQWIn1F5d3LoOF9p8BSoB6oW1NiwkutdLyoj3hGdnzSSSc1fbdDR9Vtzz33nFjZ3nvvTQcW13J1HqDTd6cHHHCA+sjXQP55xBFH8BXP2H/YYYcByx7nH3nkkY1KDavdCUXrXV2CCSJH9xJS23bA0smkA9h///2B5SZxqiGfe+45YJlgPvjgg5EsANfigewDXCkvSr0QKVBuuukmKIS/aG2dNvSsUbosj8TsSmiJoRANSCIUhpfuRSruo50yXoipTuYagVqoU5+nn37aV0S71qEw7lFSEyXThY4EWCpMUKo3Rq3oew3r9kcKjGF01I4iFlDirgMsM4oCTWSUUCtNCX17KFqbwAKArwtR9fJptTV9iGzbB8sGtCObBadIQaEhC1gcFGAZmxyyv3VYUT4fRQoAkWNnnxVchaiXR6SBqIMlcE6oTO8JsKAjT4RW3HfffXUs46D6uKiOJe7a77dmDsV2dD5BQjkAxd8ogCV+8CEVuJvq9xKdOGpHcyz6ZlfkkwJWoFPAilwjzjHzKWDxlcYV2Pbaay81EVBK+RrCfvPvuK9heCqsq5lbTf12NKDojjwljVF7XRAxrbre1vqYMaDfzKAMNOUEh6JuABIM2niCG8bXDQO4sUNHtddw/T7r/V6ZMCEs6Q8h5286YQCF9Oi9bviuspG+ioFf3B5g7G87v82ovccaHUcN0QxZ4cyd9Vu+ec8377v4C9IEK8FKS7ASrAQrwUqwEqy0BCvBSrASrAQrwUpLsDoDy993t6QNZmUZoy5x1/LlWx5+uHt/Vq/+X0asXTJi+aP5scd278+SJTkUJlgJVoKVYCVYCVaClWAlWAlWgpVgJVhpCVaClWDttmB9W1l9T79ih+Gy+j/tt7HE2ZiANXA9O19athOwLH85xmBRkNXFisXeqKy+J9Z3HAmjWqEpJf3TGHRapGaEjeUoiRWxVOsKiaMPliUkaYsJnRvV8pa2qaPqmSboZkMv5VDcVOy3dOUtlTWqdQZpSp0wqMp+ALCuuIKopKE827NmNSy/SvMbhyZMsMKqFakp5ccUrNsrizWr3TmlIdRo3zgIWCTIjUpl5gS6KzHMyVxGlqm/Ut9rcsLRTprTcrSxejv5kZYjaYztOOrPdmpFUTgKa0sPCpa/39FahnbZ2rj07CRDoZilc3eUc2JVZoI+v8mqiPh0yO2VxXK6pMlkkkTenUQsa/9Ondo444y+baWuWfMPWJMm0cs3KFg16ZiBRYym62i8WDSW/tNvOkwqPFJGYNluVMvR0mdqbHhFIgCOs961Dqq9p0yZ0klzhu6UJpYel/eJJGP18jgqrwRJqhwNIRMd86HQYrsBVnhGT4gA1qjSH2ArwNL33Ehde428AMtq9fYrhNK1PbAoUrdvb+iA0BGf7Fm16h+w4ufJJ/si2ZiBJRpDhAKdeFcDR5wI18RQGIG9DlYkbuBWeuWVK1c2qhwsHbao0YRsMpS4/G5byIxDIJOywUZkxekesFCiW6pqqK5Dw86TRMbSgdCjNqpcN2ViWsCS+wR/9geabYAFJmOfDUPe+ef3A5Yo8frrfUFrzMDCTShohQQ5BQIsgnR+ESeARSeu88GuFazonUaxzude5itIkprCNkW8bZHApEqtxE7XEq5GbobXHliySMQcS74NbJVMMnyii6rz3XffzY2GwpgvFrD4UGd2j01T2J0aCuWFIPZ+++2/6AmwTNgXLuxDTV0cXb68MXFilz0V1vXaA4vTR3TqU7r7UPTjo/9U6N6jhv0+LxfUWo+WbACdPBWeeGK+x8r3WPkeK8FKsBKsBCvBSrASrAQrwUqwEqwEK8FKsBKsBCvBSrDGDqxUQu+KSug5c7ZMnty9P2++mUroXTNizZ7dOOSQ7v1ZvDiHwgQrwUqwEqwEK8FKsBKsBCvBSrASrAQrLcFKsBKs3Qespn/NpmUob6L7teH9b/eBSxsFUWGj+5TQuzBY9KhludQmGYzFHUM72qjWy+zt7V24cKG/M8Qea5FRa1EtU4BZQZOAh/KJvKc9pHydKpWsxVXuqMxioSHiIO90lMJsFJZ9Hx0ldKPShljdlA6sbbAuukgFGvRy8dGZixf/c5TciU7acqVdAZZty00TWoXCPcCiI+VBYFE7UeIXjSUBk0XudFm6P2B1IoZ2Uddq/K1nbFRpI+68887YVrKP27Zts+jtmIM1LEroRrWirMVOOwGrp4do0cKqfdtnndWnhK6DxZ2kx/5O3BVgkcIZ/opfNDaNqIXsS8SilqYmjZM5xRLRIha8SsQKPtow6JAxWvC8BMhyoTCrtGvRbhgKO1dCRxoBzmwbLMRs24bOxsaN1oHu0w9eeum/wDJykEH39jYOPbRrwOKR2EbJpEmTyOcNVa1gUb77TatJ7NthxHJR6xaXiCVnRn1xYiZnhFG4S+ZYnSuh3Z1hwULXg65fvyOwjjqqDxobBKsiuywQ2nDz5sa55/bttIK4nAc2Nmzok7OOGVjmBNwRaRqawNKxOE63C7DmzZsXMmhm8Vzf4ikpHkrEIlxuoznNpQwZBhE93kej7SrC3go4BZrDHXPMMQrvd+3uMQSrbSV0WCcRy48ily7tkztHTDrnnL6Iddhhfcp6e8yuliz5Vwzr9tcNdVV0LFU9XI1aGmaAB9VueypsWwk9LE+FFqJv+2i+x8r3WPmCNMFKsNISrAQrwUqwEqwEKy3BSrASrG4Fq/zxOG0oYHWJu7ocrD7Bqj/GjU62xV3XvPstYHWJu7oZrNNO+7W397s9vPz1DwLfpu3Y+Ke8Iu8Sd/X0fLtwYTf+LFnybW9vn7v2yICUNhL2V8T6Pi1tmCwCfM6x0obZ4ASqfCpMG5GH6HyPlZZgpSVYaQlWgpWWYKUlWGkJVlpagpWWYKUlWGlpCVZagpWWYKWlJVhpCVZagpWWlmClJVhpCdYomYTecgVGJshGlaTPx5Kcsm3z39bKkVA5Pkpx5uPq1aslJ36jxaQmlCg1tuvp3eQNjG/ZXrt2bdPRDk2yPxkuBz5HbZ02OlkwdzewZESeNm2a1LQh2Zs/fz5XasUOi4WUchYtWhQfpS31UfZKDN1dmRyNUn3GtpbbsGHDtZVJchxfoQWQhtmeOXPmNKpU5LYpTxKsXWYoXLp0KfetWbMGZBJ9y2gakGlFoUtLy1MdZ2r+iG2yfPf09MTyBTZk1JVpuJ41eUdglRNkMo70sqVkJ8jxXNKJKzb2DBEs+UKlJVdOEbWKviovj21dqKLOThMg62BxvoSluhMP9AuWYrmiXN3t2yP56kZZkSvbtGmTPZs3b474Wk6wf/yCZXyRtXbGjBlSxvNjpOzmMll0r6tM5AgPTp8+PZLnajNnyqXbqHIMi3k+yqLbBJb491NlRsahgIUqJAXHMrPLlgv0oYCF4FgQICIctiS0VSuluYr9uG9UKeljUYU4M8By6Rsqs1/4VNsmsJQzdepUGxL4iqMKl3G4XE4a/UaVN1oJUqNLf9/vCeN08r5ixYpwxMyZM6PHz5o1y8dtlWndSJW+I7BwuX79eiLJJrCabFCwVEMT+q11HZUQ2u9BwVIZTChczRcsWOA0kSNaWmQSLAEhBbIzBWOUOL8esdyUmYDeJZjZKX43gWWNDyWb4dnWwdxp4GLn3LlzXcV3FRX195V169a1njBOwZJdmPe5QwPHHlSV5QL0P97hph2B5YR+51iSYPdUZkgaCljGDucIVMYdrMBoKHMsQ7BDKKzvlDy8LA4g27ZKGuZQW6oaYCHYhp4zo7JyU61zrFhAxUzARxtIdX44TXRUVMl933pCfc2Vcfe6IaK3/h0feaQs6GBDRx8gYu0IrJ2dY4kW0d1dSJnR6oOCZYbn0LJly+JCAo8IgSqFxAkRvXQMd1F6S4BlMqT+burdv608ye4IrJgwWJOnfIU36mCpiROsLFQ/IcH6CyzLOfGUMMChgofhwE4LEdhpphLjwkiApaVjxuZhIr5VB8sJ0VSiYPmueU8MdgrRwGq7devWqKHzvbMQqGLUUwG1dUeW7ShDocr7ilFMmbYNowOD5QTnC6sup3xOgGwdrDjBDdZPSLB+LCho0esrcyj2c2uZ9vLjSIBl20OAxwW4tIJVrCzAEWY1CvMk+30RAfFE4ivx8CFKRajzDIG/WB7MoiYBlmeF2bNnu83YX3+E7BeseAgw1MasNI7WwYq3bk0n5Jv3f5nWrS910agSUzU9k3ePAbFpgUIfW2tbf+6rzzJLp2r7cjt7wvgFK22XtgQrLcFKS7DSEqwEKy3BSkuw0hKsBCstwUpLsNISrLS0BCstwUpLsNLSEqy0BCstwUpLS7DSEqy08QiW/4/2r/65LmjacBmcQPX/9jjCDkWUOTQAAAAASUVORK5CYII=",
- "description": "Tables and cards to display latest and historical values for multiple entities simultaneously."
+ "description": "Tables and cards to display latest and historical values for multiple entities simultaneously.",
+ "externalId": null,
+ "name": "Cards"
},
"widgetTypes": [
{
@@ -62,7 +64,9 @@
"settingsDirective": "tb-timeseries-table-widget-settings",
"dataKeySettingsDirective": "tb-timeseries-table-key-settings",
"latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings",
- "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true}"
+ "hasBasicMode": true,
+ "basicModeDirective": "tb-timeseries-table-basic-config",
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true,\"configMode\":\"basic\"}"
}
},
{
@@ -100,7 +104,9 @@
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-simple-card-widget-settings",
- "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Simple card\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}"
+ "hasBasicMode": true,
+ "basicModeDirective": "tb-simple-card-basic-config",
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Simple card\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true}"
}
},
{
@@ -139,7 +145,9 @@
"dataKeySettingsSchema": "",
"settingsDirective": "tb-entities-table-widget-settings",
"dataKeySettingsDirective": "tb-entities-table-key-settings",
- "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"entitiesTitle\":\"\",\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":true,\"entityNameColumnTitle\":\"\",\"displayEntityLabel\":false,\"displayEntityType\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"useRowStyleFunction\":false},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"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));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"useCellContentFunction\":false,\"defaultColumnVisibility\":\"visible\",\"columnSelectionToDisplay\":\"enabled\"},\"_hash\":0.6401141393938932,\"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;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}]}"
+ "hasBasicMode": true,
+ "basicModeDirective": "tb-entities-table-basic-config",
+ "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"name\",\"useRowStyleFunction\":false,\"entitiesTitle\":\"Entities\"},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity name\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.782057645776538,\"funcBody\":\"return 'Device';\",\"decimals\":null,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.904797781901171,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.1961430898042078,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7678057538205878,\"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;\",\"decimals\":2}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"displayTimewindow\":false,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"list\",\"iconColor\":null}"
}
},
{
diff --git a/application/src/main/data/json/system/widget_bundles/charts.json b/application/src/main/data/json/system/widget_bundles/charts.json
index de32604dbb..660bd9b16d 100644
--- a/application/src/main/data/json/system/widget_bundles/charts.json
+++ b/application/src/main/data/json/system/widget_bundles/charts.json
@@ -3,7 +3,9 @@
"alias": "charts",
"title": "Charts",
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAfRklEQVR42u2deXwb1bXH89+DAqV0eV3f62uhe6Hs+9ayFRpogEcgoSwhbGFrgZbkASVhJ5BASCAhxImzmiR27MROvMZxLHmRJdvyvsi7LW+yHduSRttImveTrqzI0kgajWZkm9zzueRjbI3mzp3vnHPuOefemcdRoSKDzMN/Lperp6cHPzAMU1dXZ7Va6bjMLVGpVKWlpfihsLAwJyentrZ2VoB18ODB9vZ2/LBv376BgYEDBw7QWzWHxGQy7dq1q7Ky0u12p6am4jf79++febCMRuP69evB1tDQEEGKgjVXxG6349+urq7s7Ozq6uqqqipy79LT0/Gvw+GALZoxsEZGRgoKCiYnJ6FCCelpaWn0ns1CASVQToODg71TMjw8jN9DNYAkrVZbU1OTkpIyOjoKNYHfm81mvV5PPtnX1zc2NgbUEgcW9Ge6V7q7u4uKinJzc4m1xmWQPul0ul4qXsF9SjxPcHkBE5xgwHHixAleOPAn+FW4lfiAWq222Wy8UMLPId8zMTGBD8vuY0UWDCjVFokXlmUNBgOedrgoxORJqPkAFgjDcwKtRsE6VQQqimhHTNJlPZHT6YQjBBcNVlJyBUbBmkVisVjAEwwfbnmC55VQYFCQEjr7FKxZIfCccGvhjM/gPA54QXuNj49TsL4OAhsEVQGq4FSJoYGxDIyMtfX2mxhPWNvl5kz2uIwazCLwij9ITsGaYduHuwhVIdjvdvcOGY6pa7YcyH7zi13PvLv+0X9/RFq9rgsfMDDun200nbfJdPE2852pzAv51nVqe2E3e8IaA20wxLDIJJZBwZp7At2AsRVi+xwsW9mkS0rPef6Dz/0kBbVAsHjbzSnMe2W2in6nUxhjCI8BetGhLwrWDAhUAkYVMafogz9o2HX46LPvbQjHk3Cw/O3y7ea3SmytY9GBhnVGvEO4QqVgzbCfDk0QGsMMkpau3jU7UqPyJAIs0n6+0bT0iLVq0BnVBezv74dypWDNagFPoCqynw4v6sPkfcKREgeWvy0+ZGkciaK9ML2I1eU6ZcBymrihnSLbiDRZeQQ8YVkiOFUWm31HZv5jK9fESlU8YKGdu8m0vMgW2cGH4YbqomCFiLWbK/4Pka3iFxKc32pFTCFCgLuxvfvlNZtFIBU/WKRdmmzO7YikSlGpIJwtClYiwEKyL4KuQhAhNV+x5I2PRFMlCVikQXXZnJH0lkCbSMGSHSx4VJ2dneGyNIhwivCo5AMLDQGwPmNYe430IipzKFgzDBZsX4Ro0Mj4xIp1SfFTJS1YaJclmxvCe/QwiFFjEBQsecHCPQhXmqIfHnnxo02SUCU5WGjnJ5nL9M5wTwsse+S5LQVLRrDg7YbzSIZGT/x99UapqJIDLLTffmlSDzjDRePIAhwKVqLBwtDjseb909iEMZ4JYMLAInqrzuCK9bGhYMkIVriCBbuDXbVpp7RUyQcW8bcGTPxREuSqw6UQKFiygIUMLmZPPN4Jx23clyU5VbKCReaJVj6HClPdcFqZgiU9WGQmyBsLLSivkoMqucFCW1HEr5kQekAFPQUrEWBh+QNvuXq/YfTJtz6Zo2ChFXTxTwN5nyIKlsRg+fcrCFVjKM2TiarEgIWcD28+kdfuU7AkBgsTJV51VaSukY+qxICF9noxv0GEpxWktKKDVduufyTLIq69XGg9pcAKp67MFutz73/2NQALRc/1fNEHhB6CarZ8YBUXF2Pe2NbWlpeXFzQ06la96H5cu8t8SoEFi8Cb60grUMhKVcLAQns4yxLO0woGCynStWvXYkSw2wwXslcJBUs4WLxzb8ZqC1z1MNfBQqvkqztFMWCgDzAPQbysrCwoKoBFdikh/1KwYgULKh/zwdDfHzxWKjdVCQbrsSNW3phW4N4W8zo6OrA5yYYNG7CZBNVY8YCFLTdCN1mA1yVhpnmWgIV6+e4JF2+ywe/C+3ysxsZGPHDYWAaqC5aRC9htpqi6VXQPrkyemCUbxQx0losGiy39mZDdZnjtYHVzWwKoSjBYaB+qePYpwSpqf7A0+qyQaiwhGguVx7wZ2Z7B4R1ZBdIWMswsWOd9YXoo05KpY3knxdgjiYIlJVhwXbGsOUKSp7W7b0924UtrvpijYP1qs8e1Sm12RF5z4Q8pULCkAYu/OMlp5Nix0MQO3PkVn26dE2D9erPp8WxreitrFLYlBBxNUi5LwZIGLP78xOB2z+GaCz1ryBwjvIS9un7bLAQLJX6EJ7Mjti1GkN4hkVIKlgRghQs0cM2PTv+e33ADSZwj2BUznJjIL696NyklnoU6koD1h63mZ/M8PDEOkVvW+N0sCpYEYCGhwVs6wlX8kv8Ly3/K9a3jbH3Bgftx8YTFA9ZFW81IvmFTGoeLv1gjpv3+iFdAwZIALMwHeQopnZNc8WlRvrn0B1zPe5y101sCeFJQuwzCPty+/7GVa+UD67a9THqrY9DsZl1R8lQxbVpJvII5Apbb5TYZRTbGLDdYGEqeZ3pSFcMpSr/P9XzAWdqCCMOqw9KaxnW705euWisVWPceYPI7WZNgY4fZLua8wu8VTCEM4twAyz04YL7pcnGNWXxXAsDiMyG7xJxL+S2uYwXHNMJdCaqPIIQ9vupjcWAtzLDkgqfY9/vDM+OPTgkREnmhYMkGVu9q8Wf0EHYW1/YCZ6rl3M6glLa6oQU7+j319johYD2ZYy3pc1rYaTyxLmf1sPYz7ca28Xbh1k2gYBk+8s4ULNnAav9nXGCdbGdwrU9xxgrOPS3YjdU+NS3tIGzZO5+GgvVMnhVLAu3TqxAcLod6UPNJ1fpFR/42P2MBWnZnruRgYR6D2QwFSzawWpZKBJa/nc61PM5NgjBHEGFVTW0DBs9+CggTYBlgkDNuc9pK+8s+1KxdmLWY8ORvm+u2SA4W1BWUFgVLNrCaH5EarIDWeD83oeBckQp0LayluE/xXsXqezPvD+LJ39ZVbZAcLEwhsXSHgiUbWE0PygiWv9X9hRvL4ZwnwwF2p/1oz7G3yt+9J/O+cDz5G9SY5GAhHw//nYI1NzVWUPNEwqbGinMvV7wWFSnSPtNukhwsbIKCrA4FSzawdM8liKr+jb6IrLKI88Zpraz13qyFQsD6qmWf5GAhXYjCLAqWbGB1vpoIqmpvIzFVV1e7+Y7r7OtW++zwWLMQsBR6peRggSqwRcGSDSz9BtmpUp7j2bTXM/GzWpYsJNfLFuWT8yc37IgK1ph1THKwYAdhDSlYEoDFX4w1elh2sIwaX0Bh7bsnr/evN7mHBj06zO16pvD5CFQtO/q8kMHHchtUWQm/WXh7GWr/KVgSgIVt+3i2GGWa5KWq83XfjVccC7pk6z+e5Lwb6U7YJu7KuCccWOltBwX6TEJeojEnk9CzHCyEbXj2g0QqpuTbclGluQCJec9JDEPMgptDr9qxwxf5RGiUl6p7MhdO2ieFDD4em5jeqDOXymZmOVhkgs3zB+2N8oB1uq9a0Om0vvAE/4XffKVTW0l68aF6bShYe5q/igkUCtYMgBW0VvOkdCyXBSx4byQWmrw50oU/MN89OeFNObMPZS8JpGpJ7hNWp6BtNaCrYnohBbwr+FgULJkXU5w4Kj1ViLsSmuu05luujHzttpXLfTNUk95P1YJD9zaONgkcedQwRlh9xDslJLtXULCkAQsai8d/d9m4ku9ISVX5Tzi3Z6Wo22hkFt0l5PLZLN+GCZnth0HVnRl3C6xoIBJuJ8ioNY/zwONXXoEGO378eH5+fllZGQUrVrAmvMLzh8ZFUoJl6fDl4974l9ARuP1aV0cbSfWsUL52qD0rpjoF3p1UhWjueaTIoaWlBavsya4NqampFKxYwQrrZo1mSkaVfr3P7zmYGtMIWJY+QFI9TrczpmHHzkQxvfw8cLXSPDKpwb4g0GAHDnjenzYLd5uZ/WBx07fECOi6gyv7sRSpm1unUjcdSN3EOgj29R/GOubQODGVupPQqH9RyTy4/Tt37iRvR8FuM/gughfGaNIrJfWdosG6eodxUgoxtulEg2V6YD6+wTTSIPqmusrP9XXDaIwwrFD8+Azfg78q7tTNt3ypG7vd8sRicePgLDkew5PsdmNvmJhWfQU5ZB4fq8orcLugurRaLeppyFcTvyFOsCakkEldazxg4RuMhvp4wPJ1YzJSRJHsz8M3ZTdwyrPjAgtVo8TWfPK+eM294Cb38KDwyWCsr4IGPIEOGZ0VSrmjH1az8L8+Tves+LN3+EIGrPKY6EEISvVEFuiayBs2hZsXB76Jg4IlJVioyuV/vYx9UGR6R/OHqdTNMHP3LXGC5Un17EqKfAnQu7H67CRRHaStKVgSb8cd9k2qYjytqdSN22V9eVn8VHnarVc562siuFaYgoR7PU4EQfkD8aAoWHKBFTb242I49W9jTN1k+vTdji+locqX6rnTbZwMZ85ida1IqCXUuaRgSf/Kk9Dd9KdqK4ui7+bgb02LhaduYm22Vct5qxj4Y7zRBLGr0LQPBUt6sDA/4t/VCILFzYJSN//lS92YjOi/tFT5Uj1HMgItIKYdkee8EbLUvHNhCpYsr5XDfQrdQdmXPay6SkDqps0XX3j7NTmo8qV6Otv9vY1pP5mg5CBvtVYiwBpmDHcfuk9ce6rgmbkIFmZJYcuYkO8r/WHE1M1nPmWQmSYXVb5UzyKS6ok1ECokOp8YsIYFLnMLbY/lPTEXweK820qFdVmMas86CN5zoTaQpG66O813XC8rWJ5Uz4Y1os0ICUyEg5KCJePLxqG0+A2ih7sDnOIbwSdSnOnfqtQ9YrC9uUJWqlAJiHp50WDBCEYo1aJgyQgW5uERnmnOkOohKfBEvauDv0Gjsix7WHqk7rzRvn2zO5YKvtDEKCr9I3yAgiUjWCQWH2nt1Eg6p/zmVP3Cn4M2W/PP2ZylxdaXnpYGqftu9yA1MR7PXAoR1KgrDSlY8oLFeXe4419qQQQ7SiK4gNIae5S1eyiYsSd9zvztbjHjcMd1iF2xxYUc6+DiE8xLUPgQNedDwZIdLJLxwMLzsH+2dnHjxTF4zb3djkOptg9Wekpobrsm7IUvvMO64u/2bZucVRVc7FmacMYdVAlZDUbBSgRYJFYkIlsiqHJqbMTV3ooAPQBCc7U2ufR9XLhJQ3ynQlJBYCaRgpUgsEh+l2dd6xwR2D5QJTyOSsFKEFiELWR5xWVOZlbI9DamdWAUrMSBRQSF4ZEn6rNN4FGBKnuMtpWClWiwyDwR7rzoREoiBX4hqAosDaVgzV6wSHxLhBpIsOABgO0W9wBQsGYGLOK4wB0WVwKVAPOHvsXjDlKwZgwsIvC3IqUUEy7QT1BU4YphKFhzBiwSy0aUK9a3t8khiIbAQEdePknBmjNg+W8qrA+0RawrZKQ6OxQnlhhJBTcFa7aA5Z+FAS/cYP71iXKeUVqgKVizCyz/nLHXK3CfZbKPyMwgogak4OTJcQoK1mwEy59FQdkT7j08MOSw49dhAAjIwpnDdyKQZpMoM03BmmNgBQYmEJVASAluEP4le+kKCVqCJKwjBZSAqccrKJiWlafoYJEtLiBV9c0blMPi2tbyIXyDrku3p+orcS1Nm+7pRGurYUeSuDa0bze+oK+76UTjanFttGkDGQoROxrIARnZSxfLAAFKb0SBqoPzBHuaGJhi01hUqFCwqFCwqFCwqFChYFGhYFGhYFGhQsGiMjvAOnToEIpvioqKNBoNXktRUlKCTZQLCwvp6FCJC6yamhqAVRkidHSoxAVWQ0NDoMYqLS2Fxjp27OQ+JPPmibSYMb2RjB74tTkQnxdEDAWLHkjBogdSsOiBFCw6dsIPPGF1J9U43BQsCpZUBwKm1GbHpcnmK3aYP6u0U7AoWBIc2DXhWnzIcstXTEqjI7+TvWy7ubjXScGKAla/KMGLW0+FA7v6Bt4uHL1gi/HFnLHDdUPZ3rZOOXJhkrGybeAUHBx8nmqseA88WDN8/W7mwUxLbierHnAGtteLrQvSLA7XDHcVHWgYccFGL88b/Vhtd7oTq7Gam5sRFFWpVPX19UEpHQoWr4xY3C/kW69INm7WOoKQIq1iwLnooOUNhS3BXcXsoaTPuaXG8eJR6217mV9/afrTHmbJYctL2WML0phHD1vNDnfiwEKJfrNXgBR55yoFK5zgthxoYeGkP5dnzdAO8VJFmqKXvWEXk9bCytrVIbP7aBe7Tm1/7LD1ul3M+VvM8/dbns61rlLadtY7lH2+zsBAq/qdz+Za4Qjqje4EgYWX9mJlDsBCbodqrAjSPOq6O83yl/0WsEXuVgSw0EDVRdvMrWMuabvaO+l69bjtrlTLb7eYr9nJPJRp+b/j1o3V9iPtbLie+Lv6Zon9iu1m7ZAzEWBFcLAoWEQYh/v9MvvF28wflNsqQu5WhLa2wn7jHsZkd0vV1TqD6/Lt5uXHbLsbHFCKUTsQ2tXN1Z4LyWpjZwwsqrGIHOtmr/UqBhidcHcrQluWa338iNUtRVcztMOwwl9U2wXyFK6rac3sVTuY1eV2NwVrRg4cZtwvFlhhbhBPj3q3wrVyvRPWc3O1I86u7m92XJRk3NXgiJUq3q4WdrPzUy3P5lmtLAVL/gMxIYdLhPn5vxW2v6Zafr/FvLzIVqoXerfC3tcOj78P91lcV6FXPlLZr9vN7FQPi6AqXFcxf3w4y3LPAcuYxU3BkvhAjGiFbjBTx75darsvw/K7LWaEppYesbxV4plVRXZihIOFBp0H3wgqMNauIhb1j6NW6DzomJjOKKSr8BdfOWa7fhejG3PFP6oW9tQGq9/kzu1gP1TZHzxkuWCr+fLkSfhPiDkl1zqO97Dx361w7dXjVrDLumLoqtHuRjxscaalpE/MGQV29RON/ZJtIWkot1PgqCI2dlDHPnbEetFWc3vP1xQsTN8MjLt7wlVvcJXpnQVd7CEdixTepmo7SMLFwyShIbX32nEbwpvwx2W6W6ENpnBhhgWqUeA1Dpjct37FIBKLA8WdUXhX9zQ4Lks2Q0n7zm3t4bQ3cCXfc6gu9bz8vPttzpDGMY2BbymDc5bTwS7Ls56f5BlP0AkFL6PGwh1F1E5usNZp7M/kWZFOgSd0Swpz9Q7z+VuMP99oQmgHDx8mcYg733OAwQUvOWJ5Jtf6r2O2fxfbPq+yQ1fFyUc8Bxb1sJgK7NYYol5g06jrqh3mt0rsCevq4TbHH1MYRFZdhnSu9Af8+zopznJrLhyuXJR7bOU/9+15KbNqTYWlKEDNywVWz6Tr0m0I/jJQifKBtVlrBzfrNfattQ48auktLGKDadXDFYniI9yBTZ1l5oqrWzqORjhqbxOLFHX7eKTdGRExx+OB8psEPwOK7snC/KU+hhoX4eVkI+3p3MAWru1Fd93t9pL/CUXNrfgmU37JWNWD+vq32nSpA/qOk8Tk5+fjjWEKhQLrKbCqIijyLjyz3dQ1cP3OyVfzx5ZkjC/cN9qrlyWdfkBruHTbZKqmNbtuMHuqoAAtTdUZ+L/Cm1QHKrQlDuX3yXAPlS8oqqkMd+Br2Z6B0nXzX+BW1QjCChuUI/J1lbcptUWmkt+h847jZ3+csYFUZzQ1t+TUDb2SO3bJVuP1OyZXZHflqHLqNJ92VjxvKLvNojyXKz4tgLPTdM3VJ8Gq8IomQERoLFhcTFxhcUjk5o6vJt4ptUmusVoM4ytTkwbL/+JWfINRXaZry5oRixZ6YE1vj730PAwuNJZL+R384FJ8q79+VWW/kffAJ3Osz+dbQy9wfaX92l3MQZ1DbuUa1LqbNroUZ6PblvIL67vq3i2zIfOzUmHDzAZ+Hn6AoeQ9sFI/0dip7mzZOVD36oj2iWmmUO0Vv8YSsfwL4R+MFGbpMEbavkGcDIbpht3MnkaHNGC5rNxIhq3uAfvxs4NUsaniupb2YzMLVrV+xKK6yNMZ9Y2V/SZtX99o9SPkUbaXntuu2xt6IGJjsObJdSfHx+niEDP7816moItNgNX2N3T+RNV9ZDANNcvQ/6n4CIv5zTaVIaYTSedj4bWzJwpzFZ/k5T05ofmzo/S/vXb3G6aS3/dUPr0ydWtpR2ccYLm4iVKu/SWu7Ec+i158uqnihp6mT2t6e7uav3SU/nQKrxtU1ZkzAhbuhFH9R/KsV+tP3obGzgqT+lrSPaP6Tw3d2qAD4RdifoofyGR2yRHr/6YzEkbOhBzY2Kmylf0KPXQqv9femhL/GUWB5WI4UzU3vJfretMzBa285OTrsgOas+Q/3YozAn9jL/8d1/I4N7idY1qEgeX2vC+57UXPK5OnvqTn+FUp+WugDKbr4cm+hvdYpW8KM155T0N3TSLB0vTbxyvvJZoJ1jDkM46uliRHyU/IwzZc+1x1nyHwjNtqHVduN2MePT+VgcpX6aOcsaC2FfpvRLtkuPaF1racyn6z6GtEz/UN75A7hQegtqdDksGJBaz2f3J187mKX05303zNUnJeRd5t3dX/6G7aBJOk7e0n97uiOh3ThEnN7fbi7047pOwnXMN9XN86zqjm3CGG0txgqn+Jq/jVyc9rLuC639mubkKZUbjUSpV+tL/+Dafi295DTh+tfqiupzkxYBlqnsZJWeUP67vrw30M3RuqfdmtOJN8srvp85y6fv9fVxTZzttkghMT4UT13Q29jWuMmpvdxdOeWJfy23iWoLn5mI50jdD3k5pbyHAN1L2m6bdJNTixgOVXP4qzOM2FXOMDXNcb3HAKZ6pS901gVkyKk8J1CE/GhuOqpCOfOBsXc+U/nQaZ8hyu9haP/hvL4XpXe1Sg/0+qc7mOFZxJiz6gwOPqnZE8D9KO1jbi/hEPFA+iQftkTW+XrGC1VbziddLPaeosjfphkIfHzGcZSy5sbi/yR02ht0I/r+lnWtvzhmpftJX9+uTcvvjMSc2tvY0f9ze8aVZdGfioYyqDp6upoxxqMvI1YsbDlnhcC/gtLe0F0g5OLGAhjDGayVnaOPe0PLjuhOuSZHNyLRu1Q/Do4dcjnulCoszSwQ3t5nTLOM0feFRg2Q/N2iXchNIf4YWZQGAsrYUVyAeeXXigRD0gxIIbQ5So5GBBQxOCA2em0VnUpftBGateFIq+tlcP6wlvmkwtSYMxHdE+1t66P79WF/LhrUEfZkt+PKp9tL11HzRl0DUCVjx7ZNgnKudjmiX5Uxev847Y+jW7zOs0DoEdKut3Ig6+pmL6gjvHKDea5dFMyB40P8KNZcM4BjrvOMvVO81faB2xKp7antbR6oeh54m9gLYPdKvjB6tNlwafCXeos2VbrMfCMWpVv4ZeEW2nr38TngM8/RAldBqjury/fiX8a78SCtdVENPSnu9Rb+W/CQhdngHrCRsKS4oD63paGNUV3t+f2dv4kXqAlUOdxwUWko537GdWKayxFgAhi54SLQDhBwuBsbvSmHdK7KJdJcRjvBPp08iUQt/wdpV+PP6xgxUjBrdV/bpo5wyadazqb6RvRL/63CbFOVNuU6+4rtZ1N/Y2roULFfi1jPIXTqXH2YW+9JIqlwMqHiwk51HEgwIxER1C3A8+WWlf9MWcMJpP5VgxUYp/CDBPhNHxzViV30McD5Eb0WMHWMkkFFO8+Cf/uMfmims8k8qyX8CCw9ghBCjVbYYi1LVl4msdU6mYE1X3B9rHWQQWfCTE1lHI4c+3x9oh5M+xULgjfKaMgPVBuR3Z5bJ+yYaguaMYwaQpvL6PmLjfOAofO8zJyU0aq14M8yRJVAmTG+FzWLFndJRpc9vaMhIQ5BMJFpyk2/cxQmr4I3To4wobiunCVS2iZ3DVkdY42sVKPgRNHcrxygVTc/XveF17vcCxq+4btpZfgANhZeDTzHgSaXYeOA2s8vJyZHLwLzb4i7D8a2+TQ/j9jtyhfxV6yjXtfCYRKc+Lk80ZLax8Q4AaBC9epxHXvrvi6aC4K5/HbUTuyBO3U13styYUrChg4U2bQKqpqQlZQoAVuAepv7oBNQUXbzXuqBiWJJ1+pG5o8YHxxw+dCCqAQEb9kq0TnypHE1CkoNQeGyy/x4eX4qxe1ZJjNTW8n8yp60Mmn7jAhTX1s6eeYhYeOG3vBuydjLKZCAtWEUyCFklpckhIOmptEUxfH7DjDyabt+5lXs8fTeRDWd9Vq1ct8sYOEH48CxGg2m7d9M+wI9ql3vjQj+p6mk4RxZMIHwvrbrHnE2rrJO8QAhCwrRmtLKmPWHrEirVWMzJ2CIsDKR9eijPxs9+hHqx7xevvf7exS3Pq8JEIsG5KYbD8V6YOZeg8m5VVDTqRLLs/g0EWdgbHDoFElLtM4XUGgt1IkhBNhvDjKcVHIsD6VG2XtUPbahwXJJn/lMIU97CzYexqe9r9SSESAUcV26nGRyLASkCHkBo60uaYVWNX09OJYAQi7Mj4noJ8fE3AmrUHInZ1avJBwaIHUrDogRQsChY9kIJFD5wLYGV7hYJFD5QYLIVXKFj0QOk1Vk5ODgWLHii7j0WFSmwifHWy6F2yRAs949w9I32LPZWZBitoqpgAwc5KgXuTyC2HDx+emJjAixRQl5aYM5aUlKBqFyWWgR6trILTodgO1xg4OZthsIKmigmQLK8k7HQocgRYBw8eLCgoSMwZ8ZoZ8uaiwE2jZBWDwaBUKlmWlfuMsWmshD1YRPK8krDT6XS6yclJjPvRo0cTc8Z9+/ahFrympmb//v2JOSO2QEPROZ4cAC3rif4fXnSErX5aQ1IAAAAASUVORK5CYII=",
- "description": "Display timeseries data using customizable line and bar charts. Use various pie charts to display latest values."
+ "description": "Display timeseries data using customizable line and bar charts. Use various pie charts to display latest values.",
+ "externalId": null,
+ "name": "Charts"
},
"widgetTypes": [
{
@@ -151,15 +153,15 @@
"sizeX": 8,
"sizeY": 5,
"resources": [],
- "templateHtml": "",
+ "templateHtml": "\n",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
- "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.flot.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n hasAdditionalLatestDataKeys: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
+ "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n hasAdditionalLatestDataKeys: true\n };\n}\n\n",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}",
"settingsDirective": "tb-flot-line-widget-settings",
"dataKeySettingsDirective": "tb-flot-line-key-settings",
"latestDataKeySettingsDirective": "tb-flot-latest-key-settings",
- "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}"
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":0,\"max\":1.2,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"timeForComparison\":\"previousInterval\",\"comparisonCustomIntervalValue\":7200000,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"dataKeysListForLabels\":[]},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}"
}
},
{
@@ -172,16 +174,16 @@
"sizeX": 8,
"sizeY": 5,
"resources": [],
- "templateHtml": "",
+ "templateHtml": "\n",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
- "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.flot.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\n}\n",
+ "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\n}\n",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}",
"latestDataKeySettingsSchema": "{}",
"settingsDirective": "tb-flot-line-widget-settings",
"dataKeySettingsDirective": "tb-flot-line-key-settings",
"latestDataKeySettingsDirective": "tb-flot-latest-key-settings",
- "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"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;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"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\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries Line Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"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;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"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\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Line Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"
}
},
{
@@ -194,16 +196,16 @@
"sizeX": 8,
"sizeY": 5,
"resources": [],
- "templateHtml": "",
+ "templateHtml": "\n",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
- "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'bar'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.flot.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\n}\n",
+ "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.flotWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.flotWidget.onLatestDataUpdated();\n}\n\nself.onResize = function() {\n self.ctx.$scope.flotWidget.onResize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.flotWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.flotWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\n}\n",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}",
"settingsDirective": "tb-flot-bar-widget-settings",
"dataKeySettingsDirective": "tb-flot-bar-key-settings",
"latestDataKeySettingsDirective": "tb-flot-latest-key-settings",
- "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":true,\"tooltipIndividual\":false,\"defaultBarWidth\":600},\"title\":\"Timeseries Bar Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}"
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":true,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"defaultBarWidth\":600,\"barAlignment\":\"left\",\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Bar Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}"
}
}
]
-}
\ No newline at end of file
+}
diff --git a/application/src/main/data/upgrade/3.5.1/schema_update.sql b/application/src/main/data/upgrade/3.5.1/schema_update.sql
index 3bc2c99168..1655ecb978 100644
--- a/application/src/main/data/upgrade/3.5.1/schema_update.sql
+++ b/application/src/main/data/upgrade/3.5.1/schema_update.sql
@@ -52,3 +52,73 @@ $$
$$;
-- NOTIFICATION CONFIGS VERSION CONTROL END
+
+-- EDGE EVENTS MIGRATION START
+DO
+$$
+ DECLARE table_partition RECORD;
+ BEGIN
+ -- in case of running the upgrade script a second time:
+ IF NOT (SELECT exists(SELECT FROM pg_tables WHERE tablename = 'old_edge_event')) THEN
+ ALTER TABLE edge_event RENAME TO old_edge_event;
+ CREATE INDEX IF NOT EXISTS idx_old_edge_event_created_time_tmp ON old_edge_event(created_time);
+ ALTER INDEX IF EXISTS idx_edge_event_tenant_id_and_created_time RENAME TO idx_old_edge_event_tenant_id_and_created_time;
+
+ FOR table_partition IN SELECT tablename AS name, split_part(tablename, '_', 3) AS partition_ts
+ FROM pg_tables WHERE tablename LIKE 'edge_event_%'
+ LOOP
+ EXECUTE format('ALTER TABLE %s RENAME TO old_edge_event_%s', table_partition.name, table_partition.partition_ts);
+ END LOOP;
+ ELSE
+ RAISE NOTICE 'Table old_edge_event already exists, leaving as is';
+ END IF;
+ END;
+$$;
+
+CREATE TABLE IF NOT EXISTS edge_event (
+ seq_id INT GENERATED ALWAYS AS IDENTITY,
+ id uuid NOT NULL,
+ 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
+) PARTITION BY RANGE (created_time);
+CREATE INDEX IF NOT EXISTS idx_edge_event_tenant_id_and_created_time ON edge_event(tenant_id, created_time DESC);
+CREATE INDEX IF NOT EXISTS idx_edge_event_id ON edge_event(id);
+ALTER TABLE IF EXISTS edge_event ALTER COLUMN seq_id SET CYCLE;
+
+CREATE OR REPLACE PROCEDURE migrate_edge_event(IN start_time_ms BIGINT, IN end_time_ms BIGINT, IN partition_size_ms BIGINT)
+ LANGUAGE plpgsql AS
+$$
+DECLARE
+ p RECORD;
+ partition_end_ts BIGINT;
+BEGIN
+ FOR p IN SELECT DISTINCT (created_time - created_time % partition_size_ms) AS partition_ts FROM old_edge_event
+ WHERE created_time >= start_time_ms AND created_time < end_time_ms
+ LOOP
+ partition_end_ts = p.partition_ts + partition_size_ms;
+ RAISE NOTICE '[edge_event] Partition to create : [%-%]', p.partition_ts, partition_end_ts;
+ EXECUTE format('CREATE TABLE IF NOT EXISTS edge_event_%s PARTITION OF edge_event ' ||
+ 'FOR VALUES FROM ( %s ) TO ( %s )', p.partition_ts, p.partition_ts, partition_end_ts);
+ END LOOP;
+
+ INSERT INTO edge_event (id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts)
+ SELECT id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts
+ FROM old_edge_event
+ WHERE created_time >= start_time_ms AND created_time < end_time_ms;
+END;
+$$;
+-- EDGE EVENTS MIGRATION END
+
+ALTER TABLE resource
+ ADD COLUMN IF NOT EXISTS etag varchar;
+
+UPDATE resource
+ SET etag = encode(sha256(decode(resource.data, 'base64')),'hex') WHERE resource.data is not null;
+
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 437a18bbff..a24a9a8ada 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -90,7 +90,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.queue.discovery.DiscoveryService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
-import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
+import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
@@ -536,6 +536,10 @@ public class ActorSystemContext {
@Getter
private int maxRpcRetries;
+ @Value("${actors.rule.external.force_ack:false}")
+ @Getter
+ private boolean externalNodeForceAck;
+
@Getter
@Setter
private TbActorSystem actorSystem;
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 fb6fbbdff2..1461654216 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
@@ -73,10 +73,14 @@ public class AppActor extends ContextAwareActor {
@Override
protected boolean doProcess(TbActorMsg msg) {
if (!ruleChainsInitialized) {
- initTenantActors();
- ruleChainsInitialized = true;
- if (msg.getMsgType() != MsgType.APP_INIT_MSG && msg.getMsgType() != MsgType.PARTITION_CHANGE_MSG) {
- log.warn("Rule Chains initialized by unexpected message: {}", msg);
+ if (MsgType.APP_INIT_MSG.equals(msg.getMsgType())) {
+ initTenantActors();
+ ruleChainsInitialized = true;
+ } else {
+ if (!msg.getMsgType().isIgnoreOnStart()) {
+ log.warn("Attempt to initialize Rule Chains by unexpected message: {}", msg);
+ }
+ return true;
}
}
switch (msg.getMsgType()) {
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 d5833e0c07..2779a83979 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
@@ -59,10 +59,10 @@ public class DeviceActor extends ContextAwareActor {
protected boolean doProcess(TbActorMsg msg) {
switch (msg.getMsgType()) {
case TRANSPORT_TO_DEVICE_ACTOR_MSG:
- processor.process(ctx, (TransportToDeviceActorMsgWrapper) msg);
+ processor.process((TransportToDeviceActorMsgWrapper) msg);
break;
case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG:
- processor.processAttributesUpdate(ctx, (DeviceAttributesEventNotificationMsg) msg);
+ processor.processAttributesUpdate((DeviceAttributesEventNotificationMsg) msg);
break;
case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG:
processor.processCredentialsUpdate(msg);
@@ -74,10 +74,10 @@ public class DeviceActor extends ContextAwareActor {
processor.processRpcRequest(ctx, (ToDeviceRpcRequestActorMsg) msg);
break;
case DEVICE_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG:
- processor.processRpcResponsesFromEdge(ctx, (FromDeviceRpcResponseActorMsg) msg);
+ processor.processRpcResponsesFromEdge((FromDeviceRpcResponseActorMsg) msg);
break;
case DEVICE_ACTOR_SERVER_SIDE_RPC_TIMEOUT_MSG:
- processor.processServerSideRpcTimeout(ctx, (DeviceActorServerSideRpcTimeoutMsg) msg);
+ processor.processServerSideRpcTimeout((DeviceActorServerSideRpcTimeoutMsg) msg);
break;
case SESSION_TIMEOUT_MSG:
processor.checkSessionsTimeout();
@@ -86,7 +86,7 @@ public class DeviceActor extends ContextAwareActor {
processor.processEdgeUpdate((DeviceEdgeUpdateMsg) msg);
break;
case REMOVE_RPC_TO_DEVICE_ACTOR_MSG:
- processor.processRemoveRpc(ctx, (RemoveRpcActorMsg) msg);
+ processor.processRemoveRpc((RemoveRpcActorMsg) 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 fe3a017fca..bb0e26ea32 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
@@ -84,7 +84,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionInfoProt
import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseStatusMsg;
-import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto;
import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg;
@@ -183,13 +182,15 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
void processRpcRequest(TbActorCtx context, ToDeviceRpcRequestActorMsg msg) {
ToDeviceRpcRequest request = msg.getMsg();
+ UUID rpcId = request.getId();
+ log.debug("[{}][{}] Received RPC request to process ...", deviceId, rpcId);
ToDeviceRpcRequestMsg rpcRequest = creteToDeviceRpcRequestMsg(request);
long timeout = request.getExpirationTime() - System.currentTimeMillis();
boolean persisted = request.isPersisted();
if (timeout <= 0) {
- log.debug("[{}][{}] Ignoring message due to exp time reached, {}", deviceId, request.getId(), request.getExpirationTime());
+ log.debug("[{}][{}] Ignoring message due to exp time reached, {}", deviceId, rpcId, request.getExpirationTime());
if (persisted) {
createRpc(request, RpcStatus.EXPIRED);
}
@@ -199,21 +200,23 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
boolean sent = false;
+ int requestId = rpcRequest.getRequestId();
if (systemContext.isEdgesEnabled() && edgeId != null) {
- log.debug("[{}][{}] device is related to edge [{}]. Saving RPC request to edge queue", tenantId, deviceId, edgeId.getId());
+ log.debug("[{}][{}] device is related to edge: [{}]. Saving RPC request: [{}][{}] to edge queue", tenantId, deviceId, edgeId.getId(), rpcId, requestId);
try {
- saveRpcRequestToEdgeQueue(request, rpcRequest.getRequestId()).get();
+ saveRpcRequestToEdgeQueue(request, requestId).get();
sent = true;
} catch (InterruptedException | ExecutionException e) {
- log.error("[{}][{}][{}] Failed to save rpc request to edge queue {}", tenantId, deviceId, edgeId.getId(), request, e);
+ log.error("[{}][{}][{}] Failed to save RPC request to edge queue {}", tenantId, deviceId, edgeId.getId(), request, e);
}
} else if (isSendNewRpcAvailable()) {
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);
+ rpcSubscriptions.forEach((sessionId, sessionInfo) -> {
+ log.debug("[{}][{}][{}][{}] send RPC request to transport ...", deviceId, sessionId, rpcId, requestId);
+ sendToTransport(rpcRequest, sessionId, sessionInfo.getNodeId());
+ if (SessionType.SYNC == sessionInfo.getType()) {
+ syncSessionSet.add(sessionId);
}
});
log.trace("Rpc syncSessionSet [{}] subscription after sent [{}]", syncSessionSet, rpcSubscriptions);
@@ -222,20 +225,20 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
if (persisted) {
ObjectNode response = JacksonUtil.newObjectNode();
- response.put("rpcId", request.getId().toString());
+ response.put("rpcId", rpcId.toString());
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(msg.getMsg().getId(), JacksonUtil.toString(response), null));
}
if (!persisted && request.isOneway() && sent) {
- log.debug("[{}] Rpc command response sent [{}]!", deviceId, request.getId());
+ log.debug("[{}] RPC command response sent [{}][{}]!", deviceId, rpcId, requestId);
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(msg.getMsg().getId(), null, null));
} else {
registerPendingRpcRequest(context, msg, sent, rpcRequest, timeout);
}
if (sent) {
- log.debug("[{}] RPC request {} is sent!", deviceId, request.getId());
+ log.debug("[{}][{}][{}] RPC request is sent!", deviceId, rpcId, requestId);
} else {
- log.debug("[{}] RPC request {} is NOT sent!", deviceId, request.getId());
+ log.debug("[{}][{}][{}] RPC request is NOT sent!", deviceId, rpcId, requestId);
}
}
@@ -243,7 +246,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
return !rpcSequential || toDeviceRpcPendingMap.values().stream().filter(md -> !md.isDelivered()).findAny().isEmpty();
}
- private Rpc createRpc(ToDeviceRpcRequest request, RpcStatus status) {
+ private void createRpc(ToDeviceRpcRequest request, RpcStatus status) {
Rpc rpc = new Rpc(new RpcId(request.getId()));
rpc.setCreatedTime(System.currentTimeMillis());
rpc.setTenantId(tenantId);
@@ -252,7 +255,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
rpc.setRequest(JacksonUtil.valueToTree(request));
rpc.setStatus(status);
rpc.setAdditionalInfo(JacksonUtil.toJsonNode(request.getAdditionalInfo()));
- return systemContext.getTbRpcService().save(tenantId, rpc);
+ systemContext.getTbRpcService().save(tenantId, rpc);
}
private ToDeviceRpcRequestMsg creteToDeviceRpcRequestMsg(ToDeviceRpcRequest request) {
@@ -269,82 +272,92 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
.build();
}
- void processRpcResponsesFromEdge(TbActorCtx context, FromDeviceRpcResponseActorMsg responseMsg) {
- log.debug("[{}] Processing rpc command response from edge session", deviceId);
+ void processRpcResponsesFromEdge(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());
+ log.debug("[{}] RPC command response [{}] is stale!", deviceId, responseMsg.getRequestId());
}
}
- void processRemoveRpc(TbActorCtx context, RemoveRpcActorMsg msg) {
- log.debug("[{}] Processing remove rpc command", msg.getRequestId());
+ void processRemoveRpc(RemoveRpcActorMsg msg) {
+ UUID requestId = msg.getRequestId();
+ log.debug("[{}][{}] Received remove RPC request ...", deviceId, requestId);
Map.Entry entry = null;
for (Map.Entry e : toDeviceRpcPendingMap.entrySet()) {
- if (e.getValue().getMsg().getMsg().getId().equals(msg.getRequestId())) {
+ if (e.getValue().getMsg().getMsg().getId().equals(requestId)) {
entry = e;
break;
}
}
if (entry != null) {
+ Integer key = entry.getKey();
if (entry.getValue().isDelivered()) {
- toDeviceRpcPendingMap.remove(entry.getKey());
+ toDeviceRpcPendingMap.remove(key);
} else {
Optional> firstRpc = getFirstRpc();
- if (firstRpc.isPresent() && entry.getKey().equals(firstRpc.get().getKey())) {
- toDeviceRpcPendingMap.remove(entry.getKey());
- sendNextPendingRequest(context);
+ if (firstRpc.isPresent() && key.equals(firstRpc.get().getKey())) {
+ toDeviceRpcPendingMap.remove(key);
+ log.debug("[{}][{}][{}] Removed pending RPC! Going to send next pending request ...", deviceId, requestId, key);
+ sendNextPendingRequest();
} else {
- toDeviceRpcPendingMap.remove(entry.getKey());
+ toDeviceRpcPendingMap.remove(key);
}
}
}
}
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);
+ int requestId = rpcRequest.getRequestId();
+ UUID rpcId = new UUID(rpcRequest.getRequestIdMSB(), rpcRequest.getRequestIdLSB());
+ log.debug("[{}][{}][{}] Registering pending RPC request...", deviceId, rpcId, requestId);
+ toDeviceRpcPendingMap.put(requestId, new ToDeviceRpcRequestMetadata(msg, sent));
+ DeviceActorServerSideRpcTimeoutMsg timeoutMsg = new DeviceActorServerSideRpcTimeoutMsg(requestId, timeout);
scheduleMsgWithDelay(context, timeoutMsg, timeoutMsg.getTimeout());
}
- void processServerSideRpcTimeout(TbActorCtx context, DeviceActorServerSideRpcTimeoutMsg msg) {
- ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(msg.getId());
+ void processServerSideRpcTimeout(DeviceActorServerSideRpcTimeoutMsg msg) {
+ Integer requestId = msg.getId();
+ ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(requestId);
if (requestMd != null) {
- log.debug("[{}] RPC request [{}] timeout detected!", deviceId, msg.getId());
- if (requestMd.getMsg().getMsg().isPersisted()) {
- systemContext.getTbRpcService().save(tenantId, new RpcId(requestMd.getMsg().getMsg().getId()), RpcStatus.EXPIRED, null);
+ ToDeviceRpcRequest toDeviceRpcRequest = requestMd.getMsg().getMsg();
+ UUID rpcId = toDeviceRpcRequest.getId();
+ log.debug("[{}][{}][{}] RPC request timeout detected!", deviceId, rpcId, requestId);
+ if (toDeviceRpcRequest.isPersisted()) {
+ systemContext.getTbRpcService().save(tenantId, new RpcId(rpcId), RpcStatus.EXPIRED, null);
}
- systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(),
+ systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(rpcId,
null, requestMd.isSent() ? RpcError.TIMEOUT : RpcError.NO_ACTIVE_CONNECTION));
if (!requestMd.isDelivered()) {
- sendNextPendingRequest(context);
+ log.debug("[{}][{}][{}] Pending RPC timeout detected! Going to send next pending request ...", deviceId, rpcId, requestId);
+ sendNextPendingRequest();
}
}
}
- private void sendPendingRequests(TbActorCtx context, UUID sessionId, String nodeId) {
+ private void sendPendingRequests(UUID sessionId, String nodeId) {
SessionType sessionType = getSessionType(sessionId);
if (!toDeviceRpcPendingMap.isEmpty()) {
- log.debug("[{}] Pushing {} pending RPC messages to new async session [{}]", deviceId, toDeviceRpcPendingMap.size(), sessionId);
+ log.debug("[{}] Pushing {} pending RPC messages to session: [{}]", deviceId, sessionId, toDeviceRpcPendingMap.size());
if (sessionType == SessionType.SYNC) {
- log.debug("[{}] Cleanup sync rpc session [{}]", deviceId, sessionId);
+ log.debug("[{}] Cleanup sync RPC session [{}]", deviceId, sessionId);
rpcSubscriptions.remove(sessionId);
}
} else {
- log.debug("[{}] No pending RPC messages for new async session [{}]", deviceId, sessionId);
+ log.debug("[{}] No pending RPC messages for session: [{}]", deviceId, sessionId);
}
Set sentOneWayIds = new HashSet<>();
if (rpcSequential) {
- getFirstRpc().ifPresent(processPendingRpc(context, sessionId, nodeId, sentOneWayIds));
+ getFirstRpc().ifPresent(processPendingRpc(sessionId, nodeId, sentOneWayIds));
} else if (sessionType == SessionType.ASYNC) {
- toDeviceRpcPendingMap.entrySet().forEach(processPendingRpc(context, sessionId, nodeId, sentOneWayIds));
+ toDeviceRpcPendingMap.entrySet().forEach(processPendingRpc(sessionId, nodeId, sentOneWayIds));
} else {
- toDeviceRpcPendingMap.entrySet().stream().findFirst().ifPresent(processPendingRpc(context, sessionId, nodeId, sentOneWayIds));
+ toDeviceRpcPendingMap.entrySet().stream().findFirst().ifPresent(processPendingRpc(sessionId, nodeId, sentOneWayIds));
}
sentOneWayIds.stream().filter(id -> !toDeviceRpcPendingMap.get(id).getMsg().getMsg().isPersisted()).forEach(toDeviceRpcPendingMap::remove);
@@ -354,35 +367,38 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
return toDeviceRpcPendingMap.entrySet().stream().filter(e -> !e.getValue().isDelivered()).findFirst();
}
- private void sendNextPendingRequest(TbActorCtx context) {
+ private void sendNextPendingRequest() {
if (rpcSequential) {
- rpcSubscriptions.forEach((id, s) -> sendPendingRequests(context, id, s.getNodeId()));
+ rpcSubscriptions.forEach((id, s) -> sendPendingRequests(id, s.getNodeId()));
}
}
- private Consumer> processPendingRpc(TbActorCtx context, UUID sessionId, String nodeId, Set sentOneWayIds) {
+ private Consumer> processPendingRpc(UUID sessionId, String nodeId, Set sentOneWayIds) {
return entry -> {
ToDeviceRpcRequest request = entry.getValue().getMsg().getMsg();
ToDeviceRpcRequestBody body = request.getBody();
+ Integer requestId = entry.getKey();
+ UUID rpcId = request.getId();
if (request.isOneway() && !rpcSequential) {
- sentOneWayIds.add(entry.getKey());
- systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(request.getId(), null, null));
+ sentOneWayIds.add(requestId);
+ systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(rpcId, null, null));
}
ToDeviceRpcRequestMsg rpcRequest = ToDeviceRpcRequestMsg.newBuilder()
- .setRequestId(entry.getKey())
+ .setRequestId(requestId)
.setMethodName(body.getMethod())
.setParams(body.getParams())
.setExpirationTime(request.getExpirationTime())
- .setRequestIdMSB(request.getId().getMostSignificantBits())
- .setRequestIdLSB(request.getId().getLeastSignificantBits())
+ .setRequestIdMSB(rpcId.getMostSignificantBits())
+ .setRequestIdLSB(rpcId.getLeastSignificantBits())
.setOneway(request.isOneway())
.setPersisted(request.isPersisted())
.build();
+ log.debug("[{}][{}][{}][{}] Send pending RPC request to transport ...", deviceId, sessionId, rpcId, requestId);
sendToTransport(rpcRequest, sessionId, nodeId);
};
}
- void process(TbActorCtx context, TransportToDeviceActorMsgWrapper wrapper) {
+ void process(TransportToDeviceActorMsgWrapper wrapper) {
TransportToDeviceActorMsg msg = wrapper.getMsg();
TbCallback callback = wrapper.getCallback();
var sessionInfo = msg.getSessionInfo();
@@ -391,36 +407,36 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
processSessionStateMsgs(sessionInfo, msg.getSessionEvent());
}
if (msg.hasSubscribeToAttributes()) {
- processSubscriptionCommands(context, sessionInfo, msg.getSubscribeToAttributes());
+ processSubscriptionCommands(sessionInfo, msg.getSubscribeToAttributes());
}
if (msg.hasSubscribeToRPC()) {
- processSubscriptionCommands(context, sessionInfo, msg.getSubscribeToRPC());
+ processSubscriptionCommands(sessionInfo, msg.getSubscribeToRPC());
}
if (msg.hasSendPendingRPC()) {
- sendPendingRequests(context, getSessionId(sessionInfo), sessionInfo.getNodeId());
+ sendPendingRequests(getSessionId(sessionInfo), sessionInfo.getNodeId());
}
if (msg.hasGetAttributes()) {
- handleGetAttributesRequest(context, sessionInfo, msg.getGetAttributes());
+ handleGetAttributesRequest(sessionInfo, msg.getGetAttributes());
}
if (msg.hasToDeviceRPCCallResponse()) {
- processRpcResponses(context, sessionInfo, msg.getToDeviceRPCCallResponse());
+ processRpcResponses(sessionInfo, msg.getToDeviceRPCCallResponse());
}
if (msg.hasSubscriptionInfo()) {
- handleSessionActivity(context, sessionInfo, msg.getSubscriptionInfo());
+ handleSessionActivity(sessionInfo, msg.getSubscriptionInfo());
}
if (msg.hasClaimDevice()) {
- handleClaimDeviceMsg(context, sessionInfo, msg.getClaimDevice());
+ handleClaimDeviceMsg(msg.getClaimDevice());
}
if (msg.hasRpcResponseStatusMsg()) {
- processRpcResponseStatus(context, sessionInfo, msg.getRpcResponseStatusMsg());
+ processRpcResponseStatus(sessionInfo, msg.getRpcResponseStatusMsg());
}
if (msg.hasUplinkNotificationMsg()) {
- processUplinkNotificationMsg(context, sessionInfo, msg.getUplinkNotificationMsg());
+ processUplinkNotificationMsg(sessionInfo, msg.getUplinkNotificationMsg());
}
callback.onSuccess();
}
- private void processUplinkNotificationMsg(TbActorCtx context, SessionInfoProto sessionInfo, TransportProtos.UplinkNotificationMsg uplinkNotificationMsg) {
+ private void processUplinkNotificationMsg(SessionInfoProto sessionInfo, TransportProtos.UplinkNotificationMsg uplinkNotificationMsg) {
String nodeId = sessionInfo.getNodeId();
sessions.entrySet().stream()
.filter(kv -> kv.getValue().getSessionInfo().getNodeId().equals(nodeId) && (kv.getValue().isSubscribedToAttributes() || kv.getValue().isSubscribedToRPC()))
@@ -434,7 +450,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
});
}
- private void handleClaimDeviceMsg(TbActorCtx context, SessionInfoProto sessionInfo, ClaimDeviceMsg msg) {
+ private void handleClaimDeviceMsg(ClaimDeviceMsg msg) {
DeviceId deviceId = new DeviceId(new UUID(msg.getDeviceIdMSB(), msg.getDeviceIdLSB()));
systemContext.getClaimDevicesService().registerClaimingInfo(tenantId, deviceId, msg.getSecretKey(), msg.getDurationMs());
}
@@ -447,7 +463,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
systemContext.getDeviceStateService().onDeviceDisconnect(tenantId, deviceId);
}
- private void handleGetAttributesRequest(TbActorCtx context, SessionInfoProto sessionInfo, GetAttributeRequestMsg request) {
+ private void handleGetAttributesRequest(SessionInfoProto sessionInfo, GetAttributeRequestMsg request) {
int requestId = request.getRequestId();
if (request.getOnlyShared()) {
Futures.addCallback(findAllAttributesByScope(DataConstants.SHARED_SCOPE), new FutureCallback<>() {
@@ -531,7 +547,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
return sessions.containsKey(sessionId) ? SessionType.ASYNC : SessionType.SYNC;
}
- void processAttributesUpdate(TbActorCtx context, DeviceAttributesEventNotificationMsg msg) {
+ void processAttributesUpdate(DeviceAttributesEventNotificationMsg msg) {
if (attributeSubscriptions.size() > 0) {
boolean hasNotificationData = false;
AttributeUpdateNotificationMsg.Builder notification = AttributeUpdateNotificationMsg.newBuilder();
@@ -568,19 +584,21 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
}
- private void processRpcResponses(TbActorCtx context, SessionInfoProto sessionInfo, ToDeviceRpcResponseMsg responseMsg) {
+ private void processRpcResponses(SessionInfoProto sessionInfo, ToDeviceRpcResponseMsg responseMsg) {
UUID sessionId = getSessionId(sessionInfo);
- log.debug("[{}] Processing rpc command response [{}]", deviceId, sessionId);
- ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(responseMsg.getRequestId());
+ log.debug("[{}][{}] Processing RPC command response: {}", deviceId, sessionId, responseMsg);
+ int requestId = responseMsg.getRequestId();
+ ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(requestId);
boolean success = requestMd != null;
if (success) {
+ ToDeviceRpcRequest toDeviceRequestMsg = requestMd.getMsg().getMsg();
+ boolean delivered = requestMd.isDelivered();
boolean hasError = StringUtils.isNotEmpty(responseMsg.getError());
try {
String payload = hasError ? responseMsg.getError() : responseMsg.getPayload();
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(
- new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(),
- payload, null));
- if (requestMd.getMsg().getMsg().isPersisted()) {
+ new FromDeviceRpcResponse(toDeviceRequestMsg.getId(), payload, null));
+ if (toDeviceRequestMsg.isPersisted()) {
RpcStatus status = hasError ? RpcStatus.FAILED : RpcStatus.SUCCESSFUL;
JsonNode response;
try {
@@ -588,28 +606,33 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
} catch (IllegalArgumentException e) {
response = JacksonUtil.newObjectNode().put("error", payload);
}
- systemContext.getTbRpcService().save(tenantId, new RpcId(requestMd.getMsg().getMsg().getId()), status, response);
+ systemContext.getTbRpcService().save(tenantId, new RpcId(toDeviceRequestMsg.getId()), status, response);
}
} finally {
- if (hasError && !requestMd.isDelivered()) {
- sendNextPendingRequest(context);
+ if (!delivered) {
+ String errorResponse = hasError ? "error" : "";
+ log.debug("[{}][{}][{}] Received {} response for undelivered RPC! Going to send next pending request ...", deviceId, sessionId, requestId, errorResponse);
+ sendNextPendingRequest();
}
}
} else {
- log.debug("[{}] Rpc command response [{}] is stale!", deviceId, responseMsg.getRequestId());
+ log.debug("[{}][{}][{}] RPC command response is stale!", deviceId, sessionId, requestId);
}
}
- private void processRpcResponseStatus(TbActorCtx context, SessionInfoProto sessionInfo, ToDeviceRpcResponseStatusMsg responseMsg) {
+ private void processRpcResponseStatus(SessionInfoProto sessionInfo, ToDeviceRpcResponseStatusMsg responseMsg) {
UUID rpcId = new UUID(responseMsg.getRequestIdMSB(), responseMsg.getRequestIdLSB());
RpcStatus status = RpcStatus.valueOf(responseMsg.getStatus());
- ToDeviceRpcRequestMetadata md = toDeviceRpcPendingMap.get(responseMsg.getRequestId());
+ UUID sessionId = getSessionId(sessionInfo);
+ int requestId = responseMsg.getRequestId();
+ log.debug("[{}][{}][{}][{}] Processing RPC command response status: [{}]", deviceId, sessionId, rpcId, requestId, status);
+ ToDeviceRpcRequestMetadata md = toDeviceRpcPendingMap.get(requestId);
if (md != null) {
JsonNode response = null;
if (status.equals(RpcStatus.DELIVERED)) {
if (md.getMsg().getMsg().isOneway()) {
- toDeviceRpcPendingMap.remove(responseMsg.getRequestId());
+ toDeviceRpcPendingMap.remove(requestId);
if (rpcSequential) {
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(rpcId, null, null));
}
@@ -620,7 +643,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
Integer maxRpcRetries = md.getMsg().getMsg().getRetries();
maxRpcRetries = maxRpcRetries == null ? systemContext.getMaxRpcRetries() : Math.min(maxRpcRetries, systemContext.getMaxRpcRetries());
if (maxRpcRetries <= md.getRetries()) {
- toDeviceRpcPendingMap.remove(responseMsg.getRequestId());
+ toDeviceRpcPendingMap.remove(requestId);
status = RpcStatus.FAILED;
response = JacksonUtil.newObjectNode().put("error", "There was a Timeout and all retry attempts have been exhausted. Retry attempts set: " + maxRpcRetries);
} else {
@@ -632,17 +655,18 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
systemContext.getTbRpcService().save(tenantId, new RpcId(rpcId), status, response);
}
if (status != RpcStatus.SENT) {
- sendNextPendingRequest(context);
+ log.debug("[{}][{}][{}][{}] RPC was {}! Going to send next pending request ...", deviceId, sessionId, rpcId, requestId, status.name().toLowerCase());
+ sendNextPendingRequest();
}
} else {
- log.info("[{}][{}] Rpc has already removed from pending map.", deviceId, rpcId);
+ log.warn("[{}][{}][{}][{}] RPC has already been removed from pending map.", deviceId, sessionId, rpcId, requestId);
}
}
- private void processSubscriptionCommands(TbActorCtx context, SessionInfoProto sessionInfo, SubscribeToAttributeUpdatesMsg subscribeCmd) {
+ private void processSubscriptionCommands(SessionInfoProto sessionInfo, SubscribeToAttributeUpdatesMsg subscribeCmd) {
UUID sessionId = getSessionId(sessionInfo);
if (subscribeCmd.getUnsubscribe()) {
- log.debug("[{}] Canceling attributes subscription for session [{}]", deviceId, sessionId);
+ log.debug("[{}] Canceling attributes subscription for session: [{}]", deviceId, sessionId);
attributeSubscriptions.remove(sessionId);
} else {
SessionInfoMetaData sessionMD = sessions.get(sessionId);
@@ -650,7 +674,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
sessionMD = new SessionInfoMetaData(new SessionInfo(subscribeCmd.getSessionType(), sessionInfo.getNodeId()));
}
sessionMD.setSubscribedToAttributes(true);
- log.debug("[{}] Registering attributes subscription for session [{}]", deviceId, sessionId);
+ log.debug("[{}] Registering attributes subscription for session: [{}]", deviceId, sessionId);
attributeSubscriptions.put(sessionId, sessionMD.getSessionInfo());
dumpSessions();
}
@@ -660,10 +684,10 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
return new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB());
}
- private void processSubscriptionCommands(TbActorCtx context, SessionInfoProto sessionInfo, SubscribeToRPCMsg subscribeCmd) {
+ private void processSubscriptionCommands(SessionInfoProto sessionInfo, SubscribeToRPCMsg subscribeCmd) {
UUID sessionId = getSessionId(sessionInfo);
if (subscribeCmd.getUnsubscribe()) {
- log.debug("[{}] Canceling rpc subscription for session [{}]", deviceId, sessionId);
+ log.debug("[{}] Canceling RPC subscription for session: [{}]", deviceId, sessionId);
rpcSubscriptions.remove(sessionId);
} else {
SessionInfoMetaData sessionMD = sessions.get(sessionId);
@@ -671,9 +695,9 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
sessionMD = new SessionInfoMetaData(new SessionInfo(subscribeCmd.getSessionType(), sessionInfo.getNodeId()));
}
sessionMD.setSubscribedToRPC(true);
- log.debug("[{}] Registering rpc subscription for session [{}]", deviceId, sessionId);
rpcSubscriptions.put(sessionId, sessionMD.getSessionInfo());
- sendPendingRequests(context, sessionId, sessionInfo.getNodeId());
+ log.debug("[{}] Registered RPC subscription for session: [{}] Going to check for pending requests ...", deviceId, sessionId);
+ sendPendingRequests(sessionId, sessionInfo.getNodeId());
dumpSessions();
}
}
@@ -683,10 +707,10 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
Objects.requireNonNull(sessionId);
if (msg.getEvent() == SessionEvent.OPEN) {
if (sessions.containsKey(sessionId)) {
- log.debug("[{}] Received duplicate session open event [{}]", deviceId, sessionId);
+ log.debug("[{}][{}] Received duplicate session open event.", deviceId, sessionId);
return;
}
- log.debug("[{}] Processing new session [{}]. Current sessions size {}", deviceId, sessionId, sessions.size());
+ log.debug("[{}] Processing new session: [{}] Current sessions size: {}", deviceId, sessionId, sessions.size());
sessions.put(sessionId, new SessionInfoMetaData(new SessionInfo(SessionType.ASYNC, sessionInfo.getNodeId())));
if (sessions.size() == 1) {
@@ -695,7 +719,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
systemContext.getDeviceStateService().onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
dumpSessions();
} else if (msg.getEvent() == SessionEvent.CLOSED) {
- log.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId);
+ log.debug("[{}][{}] Canceling subscriptions for closed session.", deviceId, sessionId);
sessions.remove(sessionId);
attributeSubscriptions.remove(sessionId);
rpcSubscriptions.remove(sessionId);
@@ -706,7 +730,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
}
- private void handleSessionActivity(TbActorCtx context, SessionInfoProto sessionInfoProto, SubscriptionInfoProto subscriptionInfo) {
+ private void handleSessionActivity(SessionInfoProto sessionInfoProto, SubscriptionInfoProto subscriptionInfo) {
UUID sessionId = getSessionId(sessionInfoProto);
Objects.requireNonNull(sessionId);
@@ -743,7 +767,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
private void notifyTransportAboutClosedSessionMaxSessionsLimit(UUID sessionId, SessionInfoMetaData sessionMd) {
- log.debug("remove eldest session (max concurrent sessions limit reached per device) sessionId [{}] sessionMd [{}]", sessionId, sessionMd);
+ log.debug("remove eldest session (max concurrent sessions limit reached per device) sessionId: [{}] sessionMd: [{}]", sessionId, sessionMd);
notifyTransportAboutClosedSession(sessionId, sessionMd, "max concurrent sessions limit reached per device!");
}
@@ -807,14 +831,6 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
systemContext.getTbCoreToTransportService().process(nodeId, msg);
}
- private void sendToTransport(ToServerRpcResponseMsg rpcMsg, UUID sessionId, String nodeId) {
- ToTransportMsg msg = ToTransportMsg.newBuilder()
- .setSessionIdMSB(sessionId.getMostSignificantBits())
- .setSessionIdLSB(sessionId.getLeastSignificantBits())
- .setToServerResponse(rpcMsg).build();
- systemContext.getTbCoreToTransportService().process(nodeId, msg);
- }
-
private ListenableFuture saveRpcRequestToEdgeQueue(ToDeviceRpcRequest msg, Integer requestId) {
ObjectNode body = JacksonUtil.newObjectNode();
body.put("requestId", requestId);
@@ -897,14 +913,14 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
log.debug("[{}] Restored session: {}", deviceId, sessionMD);
}
- log.debug("[{}] Restored sessions: {}, rpc subscriptions: {}, attribute subscriptions: {}", deviceId, sessions.size(), rpcSubscriptions.size(), attributeSubscriptions.size());
+ log.debug("[{}] Restored sessions: {}, RPC subscriptions: {}, attribute subscriptions: {}", deviceId, sessions.size(), rpcSubscriptions.size(), attributeSubscriptions.size());
}
private void dumpSessions() {
if (systemContext.isLocalCacheType()) {
return;
}
- log.debug("[{}] Dumping sessions: {}, rpc subscriptions: {}, attribute subscriptions: {} to cache", deviceId, sessions.size(), rpcSubscriptions.size(), attributeSubscriptions.size());
+ log.debug("[{}] Dumping sessions: {}, RPC subscriptions: {}, attribute subscriptions: {} to cache", deviceId, sessions.size(), rpcSubscriptions.size(), attributeSubscriptions.size());
List sessionsList = new ArrayList<>(sessions.size());
sessions.forEach((uuid, sessionMD) -> {
if (sessionMD.getSessionInfo().getType() == SessionType.SYNC) {
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 8b76924d96..30d909cd5a 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
@@ -34,6 +34,7 @@ import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.TbRelationTypes;
import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
@@ -214,6 +215,12 @@ class DefaultTbContext implements TbContext {
enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbRelationTypes.FAILURE), failureMessage, null, null);
}
+ @Override
+ public void enqueueForTellFailure(TbMsg tbMsg, Throwable th) {
+ TopicPartitionInfo tpi = resolvePartition(tbMsg);
+ enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbRelationTypes.FAILURE), getFailureMessage(th), null, null);
+ }
+
@Override
public void enqueueForTellNext(TbMsg tbMsg, String relationType) {
TopicPartitionInfo tpi = resolvePartition(tbMsg);
@@ -311,16 +318,7 @@ class DefaultTbContext implements TbContext {
if (nodeCtx.getSelf().isDebugMode()) {
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbRelationTypes.FAILURE, th);
}
- String failureMessage;
- if (th != null) {
- if (!StringUtils.isEmpty(th.getMessage())) {
- failureMessage = th.getMessage();
- } else {
- failureMessage = th.getClass().getSimpleName();
- }
- } else {
- failureMessage = null;
- }
+ String failureMessage = getFailureMessage(th);
nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getRuleChainId(),
nodeCtx.getSelf().getId(), Collections.singleton(TbRelationTypes.FAILURE),
msg, failureMessage));
@@ -724,6 +722,11 @@ class DefaultTbContext implements TbContext {
return mainCtx.getSlackService();
}
+ @Override
+ public boolean isExternalNodeForceAck() {
+ return mainCtx.isExternalNodeForceAck();
+ }
+
@Override
public RuleEngineRpcService getRpcService() {
return mainCtx.getTbRuleEngineDeviceRpcService();
@@ -840,10 +843,24 @@ class DefaultTbContext implements TbContext {
}
@Override
- public void checkTenantEntity(EntityId entityId) {
+ public void checkTenantEntity(EntityId entityId) throws TbNodeException {
if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) {
- throw new RuntimeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.");
+ throw new TbNodeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.", true);
+ }
+ }
+
+ private static String getFailureMessage(Throwable th) {
+ String failureMessage;
+ if (th != null) {
+ if (!StringUtils.isEmpty(th.getMessage())) {
+ failureMessage = th.getMessage();
+ } else {
+ failureMessage = th.getClass().getSimpleName();
+ }
+ } else {
+ failureMessage = null;
}
+ return failureMessage;
}
private class SimpleTbQueueCallback implements TbQueueCallback {
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 9534104ec1..7f919754fc 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,6 +23,7 @@ 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.shared.RuleChainErrorActor;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
@@ -31,6 +32,7 @@ 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.common.msg.queue.RuleEngineException;
import org.thingsboard.server.dao.rule.RuleChainService;
import java.util.function.Function;
@@ -86,7 +88,12 @@ public abstract class RuleChainManagerActor extends ContextAwareActor {
() -> DefaultActorService.RULE_DISPATCHER_NAME,
() -> {
RuleChain ruleChain = provider.apply(ruleChainId);
- return new RuleChainActor.ActorCreator(systemContext, tenantId, ruleChain);
+ if (ruleChain == null) {
+ return new RuleChainErrorActor.ActorCreator(systemContext, tenantId,
+ new RuleEngineException("Rule Chain with id: " + ruleChainId + " not found!"));
+ } else {
+ return new RuleChainActor.ActorCreator(systemContext, tenantId, ruleChain);
+ }
});
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleEngineComponentActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleEngineComponentActor.java
index e1918466a8..42a78b7965 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleEngineComponentActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleEngineComponentActor.java
@@ -24,7 +24,7 @@ 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.TbActorStopReason;
-import org.thingsboard.server.common.msg.notification.trigger.RuleEngineComponentLifecycleEventTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventTrigger;
public abstract class RuleEngineComponentActor> extends ComponentActor {
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/RuleChainErrorActor.java b/application/src/main/java/org/thingsboard/server/actors/shared/RuleChainErrorActor.java
new file mode 100644
index 0000000000..a204c7b286
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/RuleChainErrorActor.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.shared;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.TbActor;
+import org.thingsboard.server.actors.TbActorId;
+import org.thingsboard.server.actors.TbStringActorId;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg;
+import org.thingsboard.server.common.msg.queue.RuleEngineException;
+
+import java.util.UUID;
+
+@Slf4j
+public class RuleChainErrorActor extends ContextAwareActor {
+
+ private final TenantId tenantId;
+ private final RuleEngineException error;
+
+ private RuleChainErrorActor(ActorSystemContext systemContext, TenantId tenantId, RuleEngineException error) {
+ super(systemContext);
+ this.tenantId = tenantId;
+ this.error = error;
+ }
+
+ @Override
+ protected boolean doProcess(TbActorMsg msg) {
+ if (msg instanceof RuleChainAwareMsg) {
+ log.debug("[{}] Reply with {} for message {}", tenantId, error.getMessage(), msg);
+ var rcMsg = (RuleChainAwareMsg) msg;
+ rcMsg.getMsg().getCallback().onFailure(error);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public static class ActorCreator extends ContextBasedCreator {
+
+ private final TenantId tenantId;
+ private final RuleEngineException error;
+
+ public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleEngineException error) {
+ super(context);
+ this.tenantId = tenantId;
+ this.error = error;
+ }
+
+ @Override
+ public TbActorId createActorId() {
+ return new TbStringActorId(UUID.randomUUID().toString());
+ }
+
+ @Override
+ public TbActor createActor() {
+ return new RuleChainErrorActor(context, tenantId, error);
+ }
+ }
+
+}
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 84b7757846..11a5895fef 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
@@ -49,7 +49,6 @@ 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.EdgeSessionMsg;
-import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
@@ -211,10 +210,6 @@ public class TenantActor extends RuleChainManagerActor {
log.trace("[{}] Ack message because Rule Engine is disabled", tenantId);
tbMsg.getCallback().onSuccess();
}
- systemContext.getNotificationRuleProcessor().process(RuleEngineMsgTrigger.builder()
- .tenantId(tenantId)
- .msg(tbMsg)
- .build());
}
private void onRuleChainMsg(RuleChainAwareMsg msg) {
diff --git a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
index 981bc6ecbe..e62bdf84ef 100644
--- a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
+++ b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
@@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.exception.TenantProfileNotFoundException;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
-import org.thingsboard.server.dao.util.limits.LimitedApi;
+import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.service.security.model.SecurityUser;
@@ -49,7 +49,7 @@ public class RateLimitProcessingFilter extends OncePerRequestFilter {
SecurityUser user = getCurrentUser();
if (user != null && !user.isSystemAdmin()) {
try {
- if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS, user.getTenantId())) {
+ if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS_PER_TENANT, user.getTenantId())) {
rateLimitExceeded(EntityType.TENANT, response);
return;
}
@@ -60,7 +60,7 @@ public class RateLimitProcessingFilter extends OncePerRequestFilter {
}
if (user.isCustomerUser()) {
- if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS, user.getTenantId(), user.getCustomerId())) {
+ if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS_PER_CUSTOMER, user.getTenantId(), user.getCustomerId())) {
rateLimitExceeded(EntityType.CUSTOMER, response);
return;
}
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 052e1bb6ba..d6766e73a5 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
@@ -78,6 +78,7 @@ public class ThingsboardSecurityConfiguration {
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/**";
+ public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code";
@Autowired
private ThingsboardErrorResponseHandler restAccessDeniedHandler;
@@ -142,7 +143,7 @@ public class ThingsboardSecurityConfiguration {
protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
List pathsToSkip = new ArrayList<>(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS));
pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT,
- PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT));
+ PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT, MAIL_OAUTH2_PROCESSING_ENTRY_POINT));
SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
JwtTokenAuthenticationProcessingFilter filter
= new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher);
@@ -195,7 +196,7 @@ public class ThingsboardSecurityConfiguration {
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers
.cacheControl(config -> {})
- .frameOptions(config -> {}))
+ .frameOptions(config -> {}).disable())
.cors(cors -> {})
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(config -> {})
@@ -206,7 +207,9 @@ public class ThingsboardSecurityConfiguration {
.requestMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
.requestMatchers(PUBLIC_LOGIN_ENTRY_POINT).permitAll() // Public login end-point
.requestMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
- .requestMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points
+ .requestMatchers(MAIL_OAUTH2_PROCESSING_ENTRY_POINT).permitAll() // Mail oauth2 code processing url
+ .requestMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll()) // static resources, user activation and password reset end-points
+ .authorizeRequests(config -> config
.requestMatchers(WS_TOKEN_BASED_AUTH_ENTRY_POINT).authenticated() // Protected WebSocket API End-points
.requestMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated()) // Protected API End-points
.exceptionHandling(config -> config.accessDeniedHandler(restAccessDeniedHandler))
diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
index 12c5abf47d..9fc928e19a 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
@@ -15,14 +15,27 @@
*/
package org.thingsboard.server.controller;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl;
+import com.google.api.client.auth.oauth2.AuthorizationCodeTokenRequest;
+import com.google.api.client.auth.oauth2.ClientParametersAuthentication;
+import com.google.api.client.auth.oauth2.TokenResponse;
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.gson.GsonFactory;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -33,18 +46,24 @@ import org.springframework.web.bind.annotation.PostMapping;
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.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.FeaturesInfo;
+import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.SystemInfo;
import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.audit.ActionType;
+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.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.common.data.security.model.JwtSettings;
@@ -59,6 +78,7 @@ import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
+import org.thingsboard.server.service.security.auth.oauth2.CookieUtils;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.permission.Operation;
@@ -69,15 +89,25 @@ import org.thingsboard.server.service.sync.vc.autocommit.TbAutoCommitSettingsSer
import org.thingsboard.server.service.system.SystemInfoService;
import org.thingsboard.server.service.update.UpdateService;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
@RestController
@TbCoreComponent
+@Slf4j
@RequestMapping("/api/admin")
@RequiredArgsConstructor
public class AdminController extends BaseController {
+ private static final String PREV_URI_PATH_PARAMETER = "prevUri";
+ private static final String PREV_URI_COOKIE_NAME = "prev_uri";
+ private static final String STATE_COOKIE_NAME = "state";
+ private static final String MAIL_SETTINGS_KEY = "mail";
+
private final MailService mailService;
private final SmsService smsService;
private final AdminSettingsService adminSettingsService;
@@ -104,6 +134,7 @@ public class AdminController extends BaseController {
AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key), "No Administration settings found for key: " + key);
if (adminSettings.getKey().equals("mail")) {
((ObjectNode) adminSettings.getJsonValue()).remove("password");
+ ((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken");
}
return adminSettings;
}
@@ -124,6 +155,7 @@ public class AdminController extends BaseController {
if (adminSettings.getKey().equals("mail")) {
mailService.updateMailConfiguration();
((ObjectNode) adminSettings.getJsonValue()).remove("password");
+ ((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken");
} else if (adminSettings.getKey().equals("sms")) {
smsService.updateSmsConfiguration();
}
@@ -190,9 +222,19 @@ public class AdminController extends BaseController {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
adminSettings = checkNotNull(adminSettings);
if (adminSettings.getKey().equals("mail")) {
- if (!adminSettings.getJsonValue().has("password")) {
+ if (adminSettings.getJsonValue().has("enableOauth2") && adminSettings.getJsonValue().get("enableOauth2").asBoolean()) {
AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"));
- ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText());
+ JsonNode refreshToken = mailSettings.getJsonValue().get("refreshToken");
+ if (refreshToken == null) {
+ throw new ThingsboardException("Refresh token was not generated. Please, generate refresh token.", ThingsboardErrorCode.GENERAL);
+ }
+ ObjectNode settings = (ObjectNode) adminSettings.getJsonValue();
+ settings.put("refreshToken", refreshToken.asText());
+ } else {
+ if (!adminSettings.getJsonValue().has("password")) {
+ AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"));
+ ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText());
+ }
}
String email = getCurrentUser().getEmail();
mailService.sendTestMail(adminSettings.getJsonValue(), email);
@@ -364,4 +406,84 @@ public class AdminController extends BaseController {
return systemInfoService.getFeaturesInfo();
}
+ @ApiOperation(value = "Get OAuth2 log in processing URL (getMailProcessingUrl)", notes = "Returns the URL enclosed in " +
+ "double quotes. After successful authentication with OAuth2 provider and user consent for requested scope, it makes a redirect to this path so that the platform can do " +
+ "further log in processing and generating access tokens. " + SYSTEM_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/mail/oauth2/loginProcessingUrl", method = RequestMethod.GET)
+ @ResponseBody
+ public String getMailProcessingUrl() throws ThingsboardException {
+ accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
+ return "\"/api/admin/mail/oauth2/code\"";
+ }
+
+ @ApiOperation(value = "Redirect user to mail provider login page. ", notes = "After user logged in and provided access" +
+ "provider sends authorization code to specified redirect uri.)")
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/mail/oauth2/authorize", method = RequestMethod.GET, produces = "application/text")
+ public String getAuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException {
+ String state = StringUtils.generateSafeToken();
+ if (request.getParameter(PREV_URI_PATH_PARAMETER) != null) {
+ CookieUtils.addCookie(response, PREV_URI_COOKIE_NAME, request.getParameter(PREV_URI_PATH_PARAMETER), 180);
+ }
+ CookieUtils.addCookie(response, STATE_COOKIE_NAME, state, 180);
+
+ accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
+ AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, MAIL_SETTINGS_KEY), "No Administration mail settings found");
+ JsonNode jsonValue = adminSettings.getJsonValue();
+
+ String clientId = checkNotNull(jsonValue.get("clientId"), "No clientId was configured").asText();
+ String authUri = checkNotNull(jsonValue.get("authUri"), "No authorization uri was configured").asText();
+ String redirectUri = checkNotNull(jsonValue.get("redirectUri"), "No Redirect uri was configured").asText();
+ List scope = JacksonUtil.convertValue(checkNotNull(jsonValue.get("scope"), "No scope was configured"), new TypeReference<>() {
+ });
+
+ return "\"" + new AuthorizationCodeRequestUrl(authUri, clientId)
+ .setScopes(scope)
+ .setState(state)
+ .setRedirectUri(redirectUri)
+ .build() + "\"";
+ }
+
+ @RequestMapping(value = "/mail/oauth2/code", params = {"code", "state"}, method = RequestMethod.GET)
+ public void codeProcessingUrl(
+ @RequestParam(value = "code") String code, @RequestParam(value = "state") String state,
+ HttpServletRequest request, HttpServletResponse response) throws ThingsboardException, IOException {
+ Optional prevUrlOpt = CookieUtils.getCookie(request, PREV_URI_COOKIE_NAME);
+ Optional cookieState = CookieUtils.getCookie(request, STATE_COOKIE_NAME);
+
+ String baseUrl = this.systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request);
+ String prevUri = baseUrl + (prevUrlOpt.isPresent() ? prevUrlOpt.get().getValue() : "/settings/outgoing-mail");
+
+ if (cookieState.isEmpty() || !cookieState.get().getValue().equals(state)) {
+ CookieUtils.deleteCookie(request, response, STATE_COOKIE_NAME);
+ throw new ThingsboardException("Refresh token was not generated, invalid state param", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
+ }
+ CookieUtils.deleteCookie(request, response, STATE_COOKIE_NAME);
+ CookieUtils.deleteCookie(request, response, PREV_URI_COOKIE_NAME);
+
+ AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, MAIL_SETTINGS_KEY), "No Administration mail settings found");
+ JsonNode jsonValue = adminSettings.getJsonValue();
+
+ String clientId = checkNotNull(jsonValue.get("clientId"), "No clientId was configured").asText();
+ String clientSecret = checkNotNull(jsonValue.get("clientSecret"), "No client secret was configured").asText();
+ String clientRedirectUri = checkNotNull(jsonValue.get("redirectUri"), "No Redirect uri was configured").asText();
+ String tokenUri = checkNotNull(jsonValue.get("tokenUri"), "No authorization uri was configured").asText();
+
+ TokenResponse tokenResponse;
+ try {
+ tokenResponse = new AuthorizationCodeTokenRequest(new NetHttpTransport(), new GsonFactory(), new GenericUrl(tokenUri), code)
+ .setRedirectUri(clientRedirectUri)
+ .setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret))
+ .execute();
+ } catch (IOException e) {
+ log.warn("Unable to retrieve refresh token: {}", e.getMessage());
+ throw new ThingsboardException("Error while requesting access token: " + e.getMessage(), ThingsboardErrorCode.GENERAL);
+ }
+ ((ObjectNode) jsonValue).put("refreshToken", tokenResponse.getRefreshToken());
+ ((ObjectNode) jsonValue).put("tokenGenerated", true);
+
+ adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings);
+ response.sendRedirect(prevUri);
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java
index 529b1fe48a..3b709666ff 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java
@@ -78,7 +78,7 @@ public class AlarmCommentController extends BaseController {
@PathVariable(ALARM_ID) String strAlarmId, @Parameter(description = "A JSON value representing the comment.") @RequestBody AlarmComment alarmComment) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId);
AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
- Alarm alarm = checkAlarmId(alarmId, Operation.WRITE);
+ Alarm alarm = checkAlarmInfoId(alarmId, Operation.WRITE);
alarmComment.setAlarmId(alarmId);
return tbAlarmCommentService.saveAlarmComment(alarm, alarmComment, getCurrentUser());
}
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 86941e2b07..8a061f9f8e 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
@@ -42,6 +42,7 @@ 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;
+import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent;
@@ -49,7 +50,6 @@ import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.config.annotations.ApiOperation;
-import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
index 8f95dbff35..b7d014e57d 100644
--- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
+++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
@@ -110,6 +110,8 @@ public class ControllerConstants {
protected static final String RESOURCE_DESCRIPTION = "Resource is a heavyweight object that includes main information about the Resource and also data. ";
protected static final String RESOURCE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the resource title.";
+ protected static final String RESOURCE_TYPE = "A string value representing the resource type.";
+
protected static final String LWM2M_OBJECT_DESCRIPTION = "LwM2M Object is a object that includes information about the LwM2M model which can be used in transport configuration for the LwM2M device profile. ";
protected static final String DEVICE_NAME_DESCRIPTION = "A string value representing the Device name.";
@@ -173,45 +175,130 @@ public class ControllerConstants {
protected static final String IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION = "A Boolean value representing the Server SecurityInfo for future Bootstrap client mode settings. Values: 'true' for Bootstrap Server; 'false' for Lwm2m Server. ";
- protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION =
+ protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION =
"{\n" +
" \"device\": {\n" +
- " \"name\": \"LwRpk00000000\",\n" +
- " \"type\": \"lwm2mProfileRpk\"\n" +
- " },\n" +
+ " \"name\":\"Name_DeviceWithCredantial_AccessToken\",\n" +
+ " \"label\":\"Label_DeviceWithCredantial_AccessToken\",\n" +
+ " \"deviceProfileId\":{\n" +
+ " \"id\":\"9d9588c0-06c9-11ee-b618-19be30fdeb60\",\n" +
+ " \"entityType\":\"DEVICE_PROFILE\"\n" +
+ " }\n" +
+ " },\n" +
" \"credentials\": {\n" +
- " \"id\": \"null\",\n" +
- " \"createdTime\": 0,\n" +
- " \"deviceId\": \"null\",\n" +
- " \"credentialsType\": \"LWM2M_CREDENTIALS\",\n" +
- " \"credentialsId\": \"LwRpk00000000\",\n" +
- " \"credentialsValue\": {\n" +
- " \"client\": {\n" +
- " \"endpoint\": \"LwRpk00000000\",\n" +
- " \"securityConfigClientMode\": \"RPK\",\n" +
- " \"key\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\"\n" +
- " },\n" +
- " \"bootstrap\": {\n" +
- " \"bootstrapServer\": {\n" +
- " \"securityMode\": \"RPK\",\n" +
- " \"clientPublicKeyOrId\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\",\n" +
- " \"clientSecretKey\": \"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\"\n" +
- " },\n" +
- " \"lwm2mServer\": {\n" +
- " \"securityMode\": \"RPK\",\n" +
- " \"clientPublicKeyOrId\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\",\n" +
- " \"clientSecretKey\": \"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\"\n" +
- " }\n" +
- " }\n" +
- " }\n" +
- " }\n" +
+ " \"credentialsType\": \"ACCESS_TOKEN\",\n" +
+ " \"credentialsId\": \"6hmxew8pmmzng4e3une2\"\n" +
+ " }\n" +
+ "}";
+
+ protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_DEFAULT_PARAM_DESCRIPTION =
+ "{\n" +
+ " \"device\": {\n" +
+ " \"name\":\"Name_DeviceWithCredantial_AccessToken_Default\",\n" +
+ " \"label\":\"Label_DeviceWithCredantial_AccessToken_Default\",\n" +
+ " \"type\": \"default\"\n" +
+ " },\n" +
+ " \"credentials\": {\n" +
+ " \"credentialsType\": \"ACCESS_TOKEN\",\n" +
+ " \"credentialsId\": \"6hmxew8pmmzng4e3une3\"\n" +
+ " }\n" +
"}";
- protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN =
- MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
+ protected static final String certificateValue = "\"-----BEGIN CERTIFICATE----- " +
+ "MIICMTCCAdegAwIBAgIUI9dBuwN6pTtK6uZ03rkiCwV4wEYwCgYIKoZIzj0EAwIwbjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGluZ3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVByb3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTYxN1oXDTI0MDMyODE0NTYxN1owbjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGluZ3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVByb3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9Zo791qKQiGNBm11r4ZGxh+w+ossZL3xc46ufq5QckQHP7zkD2XDAcmP5GvdkM1sBFN9AWaCkQfNnWmfERsOOKNTMFEwHQYDVR0OBBYEFFFc5uyCyglQoZiKhzXzMcQ3BKORMB8GA1UdIwQYMBaAFFFc5uyCyglQoZiKhzXzMcQ3BKORMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhANbA9CuhoOifZMMmqkpuld+65CR+ItKdXeRAhLMZuccuAiB0FSQB34zMutXrZj1g8Gl5OkE7YryFHbei1z0SveHR8g== " +
+ "-----END CERTIFICATE-----\"";
+ protected static final String certificateId = "\"84f5911765abba1f96bf4165604e9e90338fc6214081a8e623b6ff9669aedb27\"";
- protected static final String FILTER_VALUE_TYPE = NEW_LINE + "## Value Type and Operations" + NEW_LINE +
+ protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_X509_CERTIFICATE_PARAM_DESCRIPTION =
+ "{\n" +
+ " \"device\": {\n" +
+ " \"name\":\"Name_DeviceWithCredantial_X509_Certificate\",\n" +
+ " \"label\":\"Label_DeviceWithCredantial_X509_Certificate\",\n" +
+ " \"deviceProfileId\":{\n" +
+ " \"id\":\"9d9588c0-06c9-11ee-b618-19be30fdeb60\",\n" +
+ " \"entityType\":\"DEVICE_PROFILE\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"credentials\": {\n" +
+ " \"credentialsType\": \"X509_CERTIFICATE\",\n" +
+ " \"credentialsId\": " + certificateId + ",\n" +
+ " \"credentialsValue\": " + certificateValue + "\n" +
+ " }\n" +
+ "}";
+
+ protected static final String MQTT_BASIC_VALUE = "\"{\\\"clientId\\\":\\\"5euh5nzm34bjjh1efmlt\\\",\\\"userName\\\":\\\"onasd1lgwasmjl7v2v7h\\\",\\\"password\\\":\\\"b9xtm4ny8kt9zewaga5o\\\"}\"";
+
+ protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_MQTT_BASIC_PARAM_DESCRIPTION =
+ "{\n" +
+ " \"device\": {\n" +
+ " \"name\":\"Name_DeviceWithCredantial_MQTT_Basic\",\n" +
+ " \"label\":\"Label_DeviceWithCredantial_MQTT_Basic\",\n" +
+ " \"deviceProfileId\":{\n" +
+ " \"id\":\"9d9588c0-06c9-11ee-b618-19be30fdeb60\",\n" +
+ " \"entityType\":\"DEVICE_PROFILE\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"credentials\": {\n" +
+ " \"credentialsType\": \"MQTT_BASIC\",\n" +
+ " \"credentialsValue\": " + MQTT_BASIC_VALUE + "\n" +
+ " }\n" +
+ "}";
+
+ protected static final String CREDENTIALS_VALUE_LVM2M_RPK_DESCRIPTION =
+ " \"{" +
+ "\\\"client\\\":{ " +
+ "\\\"endpoint\\\":\\\"LwRpk00000000\\\", " +
+ "\\\"securityConfigClientMode\\\":\\\"RPK\\\", " +
+ "\\\"key\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\"" +
+ " }, " +
+ "\\\"bootstrap\\\":{ " +
+ "\\\"bootstrapServer\\\":{ " +
+ "\\\"securityMode\\\":\\\"RPK\\\", " +
+ "\\\"clientPublicKeyOrId\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\", " +
+ "\\\"clientSecretKey\\\":\\\"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\\\"" +
+ "}, " +
+ "\\\"lwm2mServer\\\":{ \\\"securityMode\\\":\\\"RPK\\\", " +
+ "\\\"clientPublicKeyOrId\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\", " +
+ "\\\"clientSecretKey\\\":\\\"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\\\"" +
+ "}" +
+ "} " +
+ "}\"";
+
+ protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION =
+ "{\n" +
+ " \"device\": {\n" +
+ " \"name\":\"Name_LwRpk00000000\",\n" +
+ " \"label\":\"Label_LwRpk00000000\",\n" +
+ " \"deviceProfileId\":{\n" +
+ " \"id\":\"a660bd50-10ef-11ee-8737-b5634e73c779\",\n" +
+ " \"entityType\":\"DEVICE_PROFILE\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"credentials\": {\n" +
+ " \"credentialsType\": \"LWM2M_CREDENTIALS\",\n" +
+ " \"credentialsId\": \"LwRpk00000000\",\n" +
+ " \"credentialsValue\":\n" + CREDENTIALS_VALUE_LVM2M_RPK_DESCRIPTION + "\n" +
+ " }\n" +
+ "}";
+
+ protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN =
+ MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
+
+ protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN =
+ MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_DEFAULT_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
+
+ protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN =
+ MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_X509_CERTIFICATE_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
+
+ protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN =
+ MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_MQTT_BASIC_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
+
+ protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN =
+ MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
+
+
+ protected static final String FILTER_VALUE_TYPE = NEW_LINE + "## Value Type and Operations" + NEW_LINE +
"Provides a hint about the data type of the entity field that is defined in the filter key. " +
"The value type impacts the list of possible operations that you may use in the corresponding predicate. For example, you may use 'STARTS_WITH' or 'END_WITH', but you can't use 'GREATER_OR_EQUAL' for string values." +
"The following filter value types and corresponding predicate operations are supported: " + NEW_LINE +
@@ -220,7 +307,7 @@ public class ControllerConstants {
" * 'BOOLEAN' - used for boolean values. Operations: EQUAL, NOT_EQUAL;\n" +
" * 'DATE_TIME' - similar to numeric, transforms value to milliseconds since epoch. Operations: EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL; \n";
- protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
+ protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"schedule\":{\n" +
" \"type\":\"SPECIFIC_TIME\",\n" +
@@ -235,7 +322,7 @@ public class ControllerConstants {
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
- protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
+ protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"schedule\":{\n" +
" \"type\":\"CUSTOM\",\n" +
@@ -287,9 +374,9 @@ public class ControllerConstants {
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
- protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "\"schedule\": null" + MARKDOWN_CODE_BLOCK_END;
+ protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "\"schedule\": null" + MARKDOWN_CODE_BLOCK_END;
- protected static final String DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
+ protected static final String DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"spec\":{\n" +
" \"type\":\"REPEATING\",\n" +
@@ -305,7 +392,8 @@ public class ControllerConstants {
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
- protected static final String DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
+
+ protected static final String DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"spec\":{\n" +
" \"type\":\"DURATION\",\n" +
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 603409bba0..a0f5a96aed 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -95,7 +95,11 @@ import static org.thingsboard.server.controller.ControllerConstants.DEVICE_NAME_
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TYPE_DESCRIPTION;
-import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN;
+import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN;
+import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN;
+import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN;
+import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN;
+import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
@@ -183,9 +187,20 @@ public class DeviceController extends BaseController {
@ApiOperation(value = "Create Device (saveDevice) with credentials ",
notes = "Create or update the Device. When creating device, platform generates Device Id as " + UUID_WIKI_LINK +
- "Requires to provide the Device Credentials object as well. Useful to create device and credentials in one request. " +
- "You may find the example of LwM2M device and RPK credentials below: \n\n" +
- DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN +
+ "Requires to provide the Device Credentials object as well as an existing device profile ID or use \"default\".\n" +
+ "You may find the example of device with different type of credentials below: \n\n" +
+ "- Credentials type: \"Access token\" with device profile ID below: \n\n" +
+ DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" +
+ "- Credentials type: \"Access token\" with device profile default below: \n\n" +
+ DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN + "\n\n" +
+ "- Credentials type: \"X509\" with device profile ID below: \n\n" +
+ "Note: credentialsId - format Sha3Hash, certificateValue - format PEM (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" +
+ DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" +
+ "- Credentials type: \"MQTT_BASIC\" with device profile ID below: \n\n" +
+ DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" +
+ "- You may find the example of LwM2M device and RPK credentials below: \n\n" +
+ "Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" +
+ DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" +
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device entity. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java
index fc95c02217..0e64d4cd63 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java
@@ -86,6 +86,6 @@ public class EdgeEventController extends BaseController {
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));
+ return checkNotNull(edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, pageLink));
}
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java b/application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java
new file mode 100644
index 0000000000..ede3d825e6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.config.annotations.ApiOperation;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.mail.TbMailConfigTemplateService;
+import org.thingsboard.server.service.security.permission.Operation;
+import org.thingsboard.server.service.security.permission.Resource;
+
+import java.io.IOException;
+
+import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
+
+@RestController
+@TbCoreComponent
+@RequiredArgsConstructor
+@RequestMapping("/api/mail/config/template")
+@Slf4j
+public class MailConfigTemplateController extends BaseController {
+ private static final String MAIL_CONFIG_TEMPLATE_DEFINITION = "Mail configuration template is set of default smtp settings for mail server that specific provider supports";
+ private final TbMailConfigTemplateService mailConfigTemplateService;
+
+ @ApiOperation(value = "Get the list of all OAuth2 client registration templates (getClientRegistrationTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH,
+ notes = MAIL_CONFIG_TEMPLATE_DEFINITION)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(method = RequestMethod.GET, produces = "application/json")
+ @ResponseBody
+ public JsonNode getClientRegistrationTemplates() throws ThingsboardException, IOException {
+ accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
+ return mailConfigTemplateService.findAllMailConfigTemplates();
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationRuleController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationRuleController.java
index 457fac9304..cdb6850003 100644
--- a/application/src/main/java/org/thingsboard/server/controller/NotificationRuleController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/NotificationRuleController.java
@@ -34,7 +34,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
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 64606968b5..9e28dbad5e 100644
--- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
@@ -460,7 +460,7 @@ public class RuleChainController extends BaseController {
@Parameter(description = "Enables overwrite for existing rule chains with the same name.")
@RequestParam(required = false, defaultValue = "false") boolean overwrite) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
- List importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite);
+ List importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite, tbRuleChainService::updateRuleNodeConfiguration);
for (RuleChainImportResult importResult : importResults) {
if (importResult.getError() == null) {
tbClusterService.broadcastEntityStateChangeEvent(importResult.getTenantId(), importResult.getRuleChainId(),
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 387d31d40d..e1ba39638e 100644
--- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
@@ -22,19 +22,25 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ByteArrayResource;
+import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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.RequestHeader;
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.ResourceType;
+import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
+import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
@@ -50,6 +56,7 @@ import org.thingsboard.server.service.security.permission.Resource;
import java.util.Base64;
import java.util.List;
+import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
@@ -58,6 +65,7 @@ import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_DES
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_INFO_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TEXT_SEARCH_DESCRIPTION;
+import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TYPE;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
@@ -71,6 +79,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI
@RequiredArgsConstructor
public class TbResourceController extends BaseController {
+ private static final String DOWNLOAD_RESOURCE_IF_NOT_CHANGED = "Download Resource based on the provided Resource Id or return 304 status code if resource was not changed.";
private final TbResourceService tbResourceService;
public static final String RESOURCE_ID = "resourceId";
@@ -94,6 +103,47 @@ public class TbResourceController extends BaseController {
.body(resource);
}
+ @ApiOperation(value = "Download LWM2M Resource (downloadLwm2mResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/resource/lwm2m/{resourceId}/download", method = RequestMethod.GET, produces = "application/xml")
+ @ResponseBody
+ public ResponseEntity downloadLwm2mResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
+ @PathVariable(RESOURCE_ID) String strResourceId,
+ @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
+ return downloadResourceIfChanged(ResourceType.LWM2M_MODEL, strResourceId, etag);
+ }
+
+ @ApiOperation(value = "Download PKCS_12 Resource (downloadPkcs12ResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/resource/pkcs12/{resourceId}/download", method = RequestMethod.GET, produces = "application/x-pkcs12")
+ @ResponseBody
+ public ResponseEntity downloadPkcs12ResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
+ @PathVariable(RESOURCE_ID) String strResourceId,
+ @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
+ return downloadResourceIfChanged(ResourceType.PKCS_12, strResourceId, etag);
+ }
+
+ @ApiOperation(value = "Download JKS Resource (downloadJksResourceIfChanged)",
+ notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/resource/jks/{resourceId}/download", method = RequestMethod.GET, produces = "application/x-java-keystore")
+ @ResponseBody
+ public ResponseEntity downloadJksResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
+ @PathVariable(RESOURCE_ID) String strResourceId,
+ @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
+ return downloadResourceIfChanged(ResourceType.JKS, strResourceId, etag);
+ }
+
+ @ApiOperation(value = "Download JS Resource (downloadJsResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/resource/js/{resourceId}/download", method = RequestMethod.GET, produces = "application/javascript")
+ @ResponseBody
+ public ResponseEntity downloadJsResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
+ @PathVariable(RESOURCE_ID) String strResourceId,
+ @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
+ return downloadResourceIfChanged(ResourceType.JS_MODULE, strResourceId, etag);
+ }
+
@ApiOperation(value = "Get Resource Info (getResourceInfoById)",
notes = "Fetch the Resource Info object based on the provided Resource Id. " +
RESOURCE_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH,
@@ -152,6 +202,8 @@ public class TbResourceController extends BaseController {
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
+ @Parameter(description = RESOURCE_TYPE, schema = @Schema(allowableValues = {"LWM2M_MODEL", "JKS", "PKCS_12", "JS_MODULE"}))
+ @RequestParam(required = false) String resourceType,
@Parameter(description = RESOURCE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "title", "resourceType", "tenantId"}))
@@ -159,10 +211,15 @@ public class TbResourceController extends BaseController {
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
+ TbResourceInfoFilter.TbResourceInfoFilterBuilder filter = TbResourceInfoFilter.builder();
+ filter.tenantId(getTenantId());
+ if (StringUtils.isNotEmpty(resourceType)) {
+ filter.resourceType(ResourceType.valueOf(resourceType));
+ }
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
- return checkNotNull(resourceService.findTenantResourcesByTenantId(getTenantId(), pageLink));
+ return checkNotNull(resourceService.findTenantResourcesByTenantId(filter.build(), pageLink));
} else {
- return checkNotNull(resourceService.findAllTenantResourcesByTenantId(getTenantId(), pageLink));
+ return checkNotNull(resourceService.findAllTenantResourcesByTenantId(filter.build(), pageLink));
}
}
@@ -215,4 +272,30 @@ public class TbResourceController extends BaseController {
TbResource tbResource = checkResourceId(resourceId, Operation.DELETE);
tbResourceService.delete(tbResource, getCurrentUser());
}
+
+ private ResponseEntity downloadResourceIfChanged(ResourceType type, String strResourceId, String etag) throws ThingsboardException {
+ checkParameter(RESOURCE_ID, strResourceId);
+ TbResourceId resourceId = new TbResourceId(toUUID(strResourceId));
+
+ if (etag != null) {
+ TbResourceInfo tbResourceInfo = checkResourceInfoId(resourceId, Operation.READ);
+ if (etag.equals(tbResourceInfo.getEtag())) {
+ return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
+ .eTag(tbResourceInfo.getEtag())
+ .build();
+ }
+ }
+
+ TbResource tbResource = checkResourceId(resourceId, Operation.READ);
+ ByteArrayResource resource = new ByteArrayResource(Base64.getDecoder().decode(tbResource.getData().getBytes()));
+
+ return ResponseEntity.ok()
+ .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + tbResource.getFileName())
+ .header("x-filename", tbResource.getFileName())
+ .contentLength(resource.contentLength())
+ .header("Content-Type", type.getMediaType())
+ .cacheControl(CacheControl.noCache())
+ .eTag(tbResource.getEtag())
+ .body(resource);
+ }
}
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
index c436461c65..90c7a761d8 100644
--- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
@@ -40,7 +40,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.config.WebSocketConfiguration;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
-import org.thingsboard.server.dao.util.limits.LimitedApi;
+import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
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 51d7435f0e..7f4133e223 100644
--- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
+++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
@@ -260,6 +260,8 @@ public class ThingsboardInstallService {
case "3.5.1":
log.info("Upgrading ThingsBoard from version 3.5.1 to 3.5.2 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.5.1");
+ dataUpdateService.updateData("3.5.1");
+ systemDataLoaderService.updateDefaultNotificationConfigs();
//TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache
break;
default:
@@ -268,6 +270,7 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createOrUpdateViewsAndFunctions();
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Updating system data...");
+ dataUpdateService.upgradeRuleNodes();
systemDataLoaderService.updateSystemWidgets();
installScripts.loadSystemLwm2mResources();
}
diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
index 623b95b40e..e068cd4d23 100644
--- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
@@ -28,7 +28,9 @@ import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
+import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.CustomerId;
@@ -40,10 +42,12 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
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.notification.trigger.EntitiesLimitTrigger;
-import org.thingsboard.server.common.msg.notification.trigger.EntityActionTrigger;
+import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
+import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionTrigger;
import org.thingsboard.server.dao.audit.AuditLogService;
-import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import java.util.List;
import java.util.Map;
@@ -241,19 +245,7 @@ public class EntityActionService {
}
}
if (tenantId != null && !tenantId.isSysTenantId()) {
- if (actionType == ActionType.ADDED) {
- notificationRuleProcessor.process(EntitiesLimitTrigger.builder()
- .tenantId(tenantId)
- .entityType(entityId.getEntityType())
- .build());
- }
- notificationRuleProcessor.process(EntityActionTrigger.builder()
- .tenantId(tenantId)
- .entityId(entityId)
- .entity(entity)
- .actionType(actionType)
- .user(user)
- .build());
+ processNotificationRules(tenantId, entityId, entity, actionType, user, additionalInfo);
}
TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode));
tbClusterService.pushMsgToRuleEngine(tenantId, entityId, tbMsg, null);
@@ -263,6 +255,53 @@ public class EntityActionService {
}
}
+ private void processNotificationRules(TenantId tenantId, EntityId entityId, HasName entity, ActionType actionType, User user, Object... additionalInfo) {
+ switch (actionType) {
+ case ADDED:
+ notificationRuleProcessor.process(EntitiesLimitTrigger.builder()
+ .tenantId(tenantId)
+ .entityType(entityId.getEntityType())
+ .build());
+ case UPDATED:
+ case DELETED:
+ notificationRuleProcessor.process(EntityActionTrigger.builder()
+ .tenantId(tenantId)
+ .entityId(entityId)
+ .entity(entity)
+ .actionType(actionType)
+ .user(user)
+ .build());
+ break;
+ case ALARM_ASSIGNED:
+ case ALARM_UNASSIGNED:
+ if (!(entity instanceof AlarmInfo)) { // should not normally happen
+ log.warn("Invalid alarm assignment event: entity is not instance of AlarmInfo");
+ break;
+ }
+ notificationRuleProcessor.process(AlarmAssignmentTrigger.builder()
+ .tenantId(tenantId)
+ .alarmInfo((AlarmInfo) entity)
+ .actionType(actionType)
+ .user(user)
+ .build());
+ break;
+ case ADDED_COMMENT:
+ case UPDATED_COMMENT:
+ if (!(entity instanceof Alarm)) { // should not normally happen
+ log.warn("Invalid alarm comment event: entity is not instance of Alarm");
+ break;
+ }
+ notificationRuleProcessor.process(AlarmCommentTrigger.builder()
+ .tenantId(tenantId)
+ .comment(extractParameter(AlarmComment.class, 0, additionalInfo))
+ .alarm((Alarm) entity)
+ .actionType(actionType)
+ .user(user)
+ .build());
+ break;
+ }
+ }
+
public void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
ActionType actionType, Exception e, Object... additionalInfo) {
if (customerId == null || customerId.isNullUid()) {
diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
index f0ee13ae69..26233fa94c 100644
--- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
@@ -48,7 +48,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
-import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.ApiUsageLimitTrigger;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
@@ -61,7 +61,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceM
import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionService;
-import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
+import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.service.apiusage.BaseApiUsageState.StatsCalculationResult;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.mail.MailExecutorService;
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 ab43208b85..14dac3f3ff 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
@@ -31,6 +31,7 @@ import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.rule.engine.api.NodeDefinition;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbRelationTypes;
+import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentType;
@@ -48,12 +49,14 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
@Service
@Slf4j
public class AnnotationComponentDiscoveryService implements ComponentDiscoveryService {
public static final int MAX_OPTIMISITC_RETRIES = 3;
+
@Value("${plugins.scan_packages}")
private String[] scanPackages;
@@ -63,11 +66,13 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
@Autowired
private ComponentDescriptorService componentDescriptorService;
- private Map components = new HashMap<>();
+ private final Map ruleNodeClasses = new HashMap<>();
+
+ private final Map components = new HashMap<>();
- private Map> coreComponentsMap = new HashMap<>();
+ private final Map> coreComponentsMap = new HashMap<>();
- private Map> edgeComponentsMap = new HashMap<>();
+ private final Map> edgeComponentsMap = new HashMap<>();
private boolean isInstall() {
return environment.acceptsProfiles(Profiles.of("install"));
@@ -75,28 +80,62 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
@PostConstruct
public void init() {
+ for (var def : discoverBeansByAnnotationType(RuleNode.class)) {
+ String clazzName = def.getBeanClassName();
+ try {
+ var clazz = Class.forName(clazzName);
+ RuleNode annotation = clazz.getAnnotation(RuleNode.class);
+ boolean versioned = false;
+ if (annotation.version() > 0) { // No need to process nodes that has version = 0;
+ if (TbVersionedNode.class.isAssignableFrom(clazz)) {
+ versioned = true;
+ } else {
+ log.error("RuleNode [{}] has version {} but does not implement TbVersionedNode interface! Any update procedures for this rule node will be skipped!", clazzName, annotation.version());
+ }
+ }
+ ruleNodeClasses.put(clazzName, new RuleNodeClassInfo(clazz, annotation, versioned));
+ } catch (Exception e) {
+ log.warn("Failed to create instance of rule node type: {} due to: ", clazzName, e);
+ }
+ }
if (!isInstall()) {
discoverComponents();
}
}
+ private Set discoverBeansByAnnotationType(Class extends Annotation> annotationType) {
+ ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
+ scanner.addIncludeFilter(new AnnotationTypeFilter(annotationType));
+ Set defs = new HashSet<>();
+ for (String scanPackage : scanPackages) {
+ defs.addAll(scanner.findCandidateComponents(scanPackage));
+ }
+ return defs;
+ }
+
+ @Override
+ public Optional getRuleNodeInfo(String clazz) {
+ return Optional.ofNullable(ruleNodeClasses.get(clazz));
+ }
+
+ @Override
+ public List getVersionedNodes() {
+ return ruleNodeClasses.values().stream().filter(RuleNodeClassInfo::isVersioned).collect(Collectors.toList());
+ }
+
private void registerRuleNodeComponents() {
- Set ruleNodeBeanDefinitions = getBeanDefinitions(RuleNode.class);
- for (BeanDefinition def : ruleNodeBeanDefinitions) {
+ for (RuleNodeClassInfo def : ruleNodeClasses.values()) {
int retryCount = 0;
Exception cause = null;
while (retryCount < MAX_OPTIMISITC_RETRIES) {
try {
- String clazzName = def.getBeanClassName();
- Class> clazz = Class.forName(clazzName);
- RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
- ComponentType type = ruleNodeAnnotation.type();
+ ComponentType type = def.getAnnotation().type();
ComponentDescriptor component = scanAndPersistComponent(def, type);
components.put(component.getClazz(), component);
- putComponentIntoMaps(type, ruleNodeAnnotation, component);
+ putComponentIntoMaps(type, def.getAnnotation(), component);
break;
} catch (Exception e) {
- log.trace("Can't initialize component {}, due to {}", def.getBeanClassName(), e.getMessage(), e);
+ log.trace("Can't initialize component {}, due to {}", def.getClassName(), e.getMessage(), e);
cause = e;
retryCount++;
try {
@@ -107,7 +146,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
}
}
if (cause != null && retryCount == MAX_OPTIMISITC_RETRIES) {
- log.error("Can't initialize component {}, due to {}", def.getBeanClassName(), cause.getMessage(), cause);
+ log.error("Can't initialize component {}, due to {}", def.getClassName(), cause.getMessage(), cause);
throw new RuntimeException(cause);
}
}
@@ -144,13 +183,14 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
return false;
}
- private ComponentDescriptor scanAndPersistComponent(BeanDefinition def, ComponentType type) {
+ private ComponentDescriptor scanAndPersistComponent(RuleNodeClassInfo def, ComponentType type) {
ComponentDescriptor scannedComponent = new ComponentDescriptor();
- String clazzName = def.getBeanClassName();
+ String clazzName = def.getClassName();
try {
scannedComponent.setType(type);
- Class> clazz = Class.forName(clazzName);
+ Class> clazz = def.getClazz();
RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
+ scannedComponent.setConfigurationVersion(def.isVersioned() ? def.getCurrentVersion() : 0);
scannedComponent.setName(ruleNodeAnnotation.name());
scannedComponent.setScope(ruleNodeAnnotation.scope());
scannedComponent.setClusteringMode(ruleNodeAnnotation.clusteringMode());
@@ -162,7 +202,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
scannedComponent.setClazz(clazzName);
log.debug("Processing scanned component: {}", scannedComponent);
} catch (Exception e) {
- log.error("Can't initialize component {}, due to {}", def.getBeanClassName(), e.getMessage(), e);
+ log.error("Can't initialize component {}, due to {}", clazzName, e.getMessage(), e);
throw new RuntimeException(e);
}
ComponentDescriptor persistedComponent = componentDescriptorService.findByClazz(TenantId.SYS_TENANT_ID, clazzName);
@@ -210,16 +250,6 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
return relationTypes.toArray(new String[relationTypes.size()]);
}
- private Set getBeanDefinitions(Class extends Annotation> componentType) {
- ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
- scanner.addIncludeFilter(new AnnotationTypeFilter(componentType));
- Set defs = new HashSet<>();
- for (String scanPackage : scanPackages) {
- defs.addAll(scanner.findCandidateComponents(scanPackage));
- }
- return defs;
- }
-
@Override
public void discoverComponents() {
registerRuleNodeComponents();
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 731009575d..d0b0804f9b 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
@@ -19,7 +19,9 @@ 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.Collection;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -30,6 +32,10 @@ public interface ComponentDiscoveryService {
void discoverComponents();
+ Optional getRuleNodeInfo(String clazz);
+
+ List getVersionedNodes();
+
List getComponents(ComponentType type, RuleChainType ruleChainType);
List getComponents(Set types, RuleChainType ruleChainType);
diff --git a/application/src/main/java/org/thingsboard/server/service/component/RuleNodeClassInfo.java b/application/src/main/java/org/thingsboard/server/service/component/RuleNodeClassInfo.java
new file mode 100644
index 0000000000..56fd367820
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/component/RuleNodeClassInfo.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.component;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.RuleNode;
+
+@Data
+public class RuleNodeClassInfo {
+
+ private final Class> clazz;
+ private final RuleNode annotation;
+ private final boolean versioned;
+
+ public String getClassName(){
+ return clazz.getName();
+ }
+
+ public String getSimpleName() {
+ return clazz.getSimpleName();
+ }
+
+ public int getCurrentVersion() {
+ return annotation.version();
+ }
+
+}
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
index e0f6b3f6fc..91efd0edf7 100644
--- 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
@@ -341,7 +341,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
sessionNewEvents.put(edgeId, false);
Futures.addCallback(session.processEdgeEvents(), new FutureCallback<>() {
@Override
- public void onSuccess(Void result) {
+ public void onSuccess(Boolean newEventsAdded) {
+ if (Boolean.TRUE.equals(newEventsAdded)) {
+ sessionNewEvents.put(edgeId, true);
+ }
scheduleEdgeEventsCheck(session);
}
@@ -380,21 +383,26 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
}
}
- private void onEdgeDisconnect(EdgeId edgeId) {
- log.info("[{}] edge disconnected!", edgeId);
- EdgeGrpcSession removed = sessions.remove(edgeId);
- final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
- newEventLock.lock();
- try {
- sessionNewEvents.remove(edgeId);
- } finally {
- newEventLock.unlock();
+ private void onEdgeDisconnect(EdgeId edgeId, UUID sessionId) {
+ log.info("[{}][{}] edge disconnected!", edgeId, sessionId);
+ EdgeGrpcSession toRemove = sessions.get(edgeId);
+ if (toRemove.getSessionId().equals(sessionId)) {
+ toRemove = sessions.remove(edgeId);
+ final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
+ newEventLock.lock();
+ try {
+ sessionNewEvents.remove(edgeId);
+ } finally {
+ newEventLock.unlock();
+ }
+ save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, false);
+ long lastDisconnectTs = System.currentTimeMillis();
+ save(edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, lastDisconnectTs);
+ pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edgeId, lastDisconnectTs, DISCONNECT_EVENT);
+ cancelScheduleEdgeEventsCheck(edgeId);
+ } else {
+ log.debug("[{}] edge session [{}] is not available anymore, nothing to remove. most probably this session is already outdated!", edgeId, sessionId);
}
- save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, false);
- long lastDisconnectTs = System.currentTimeMillis();
- save(edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, lastDisconnectTs);
- pushRuleEngineMessage(removed.getEdge().getTenantId(), edgeId, lastDisconnectTs, DISCONNECT_EVENT);
- cancelScheduleEdgeEventsCheck(edgeId);
}
private void save(EdgeId edgeId, String key, long value) {
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
index 33aed92682..4f0f5d277a 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
@@ -24,6 +24,7 @@ import io.grpc.stub.StreamObserver;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
+import org.springframework.data.util.Pair;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.edge.Edge;
@@ -35,6 +36,8 @@ 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.PageLink;
+import org.thingsboard.server.common.data.page.SortOrder;
+import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg;
@@ -68,17 +71,15 @@ import org.thingsboard.server.service.edge.rpc.fetch.GeneralEdgeEventFetcher;
import java.io.Closeable;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
@Slf4j
@Data
@@ -89,10 +90,11 @@ public final class EdgeGrpcSession implements Closeable {
private static final int MAX_DOWNLINK_ATTEMPTS = 10; // max number of attemps to send downlink message if edge connected
private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs";
+ private static final String QUEUE_START_SEQ_ID_ATTR_KEY = "queueStartSeqId";
private final UUID sessionId;
private final BiConsumer sessionOpenListener;
- private final Consumer sessionCloseListener;
+ private final BiConsumer sessionCloseListener;
private final EdgeSessionState sessionState = new EdgeSessionState();
@@ -103,6 +105,12 @@ public final class EdgeGrpcSession implements Closeable {
private boolean connected;
private boolean syncCompleted;
+ private Long newStartTs;
+ private Long previousStartTs;
+ private Long newStartSeqId;
+ private Long previousStartSeqId;
+ private Long seqIdEnd;
+
private EdgeVersion edgeVersion;
private int maxInboundMessageSize;
@@ -111,7 +119,7 @@ public final class EdgeGrpcSession implements Closeable {
private ScheduledExecutorService sendDownlinkExecutorService;
EdgeGrpcSession(EdgeContextComponent ctx, StreamObserver outputStream, BiConsumer sessionOpenListener,
- Consumer sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize) {
+ BiConsumer sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize) {
this.sessionId = UUID.randomUUID();
this.ctx = ctx;
this.outputStream = outputStream;
@@ -180,7 +188,7 @@ public final class EdgeGrpcSession implements Closeable {
connected = false;
if (edge != null) {
try {
- sessionCloseListener.accept(edge.getId());
+ sessionCloseListener.accept(edge.getId(), sessionId);
} catch (Exception ignored) {
}
}
@@ -204,10 +212,10 @@ public final class EdgeGrpcSession implements Closeable {
EdgeEventFetcher next = cursor.getNext();
log.info("[{}][{}] starting sync process, cursor current idx = {}, class = {}",
edge.getTenantId(), edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName());
- ListenableFuture uuidListenableFuture = startProcessingEdgeEvents(next);
- Futures.addCallback(uuidListenableFuture, new FutureCallback<>() {
+ ListenableFuture> future = startProcessingEdgeEvents(next);
+ Futures.addCallback(future, new FutureCallback<>() {
@Override
- public void onSuccess(@Nullable UUID result) {
+ public void onSuccess(@Nullable Pair result) {
doSync(cursor);
}
@@ -288,7 +296,7 @@ public final class EdgeGrpcSession implements Closeable {
} catch (Exception e) {
log.error("[{}] Failed to send downlink message [{}]", this.sessionId, downlinkMsg, e);
connected = false;
- sessionCloseListener.accept(edge.getId());
+ sessionCloseListener.accept(edge.getId(), sessionId);
} finally {
downlinkMsgLock.unlock();
}
@@ -307,36 +315,51 @@ public final class EdgeGrpcSession implements Closeable {
sendDownlinkMsg(edgeConfigMsg);
}
- ListenableFuture processEdgeEvents() throws Exception {
- SettableFuture result = SettableFuture.create();
+ ListenableFuture processEdgeEvents() throws Exception {
+ SettableFuture result = SettableFuture.create();
log.trace("[{}] starting processing edge events", this.sessionId);
if (isConnected() && isSyncCompleted()) {
- Long queueStartTs = getQueueStartTs().get();
+ Pair startTsAndSeqId = getQueueStartTsAndSeqId().get();
+ this.previousStartTs = startTsAndSeqId.getFirst();
+ this.previousStartSeqId = startTsAndSeqId.getSecond();
GeneralEdgeEventFetcher fetcher = new GeneralEdgeEventFetcher(
- queueStartTs,
+ this.previousStartTs,
+ this.previousStartSeqId,
+ this.seqIdEnd,
+ false,
+ Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()),
ctx.getEdgeEventService());
- ListenableFuture ifOffsetFuture = startProcessingEdgeEvents(fetcher);
- Futures.addCallback(ifOffsetFuture, new FutureCallback<>() {
+ Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() {
@Override
- public void onSuccess(@Nullable UUID ifOffset) {
- if (ifOffset != null) {
- Long newStartTs = Uuids.unixTimestamp(ifOffset);
- ListenableFuture> updateFuture = updateQueueStartTs(newStartTs);
+ public void onSuccess(@Nullable Pair newStartTsAndSeqId) {
+ if (newStartTsAndSeqId != null) {
+ ListenableFuture> updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId);
Futures.addCallback(updateFuture, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable List list) {
- log.debug("[{}] queue offset was updated [{}][{}]", sessionId, ifOffset, newStartTs);
- result.set(null);
+ log.debug("[{}] queue offset was updated [{}]", sessionId, newStartTsAndSeqId);
+ if (fetcher.isSeqIdNewCycleStarted()) {
+ seqIdEnd = fetcher.getSeqIdEnd();
+ boolean newEventsAvailable = isNewEdgeEventsAvailable();
+ result.set(newEventsAvailable);
+ } else {
+ seqIdEnd = null;
+ boolean newEventsAvailable = isSeqIdStartedNewCycle();
+ if (!newEventsAvailable) {
+ newEventsAvailable = isNewEdgeEventsAvailable();
+ }
+ result.set(newEventsAvailable);
+ }
}
@Override
public void onFailure(Throwable t) {
- log.error("[{}] Failed to update queue offset [{}]", sessionId, ifOffset, t);
+ log.error("[{}] Failed to update queue offset [{}]", sessionId, newStartTsAndSeqId, t);
result.setException(t);
}
}, ctx.getGrpcCallbackExecutorService());
} else {
- log.trace("[{}] ifOffset is null. Skipping iteration without db update", sessionId);
+ log.trace("[{}] newStartTsAndSeqId is null. Skipping iteration without db update", sessionId);
result.set(null);
}
}
@@ -354,14 +377,14 @@ public final class EdgeGrpcSession implements Closeable {
return result;
}
- private ListenableFuture startProcessingEdgeEvents(EdgeEventFetcher fetcher) {
- SettableFuture result = SettableFuture.create();
+ private ListenableFuture> startProcessingEdgeEvents(EdgeEventFetcher fetcher) {
+ SettableFuture> result = SettableFuture.create();
PageLink pageLink = fetcher.getPageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount());
processEdgeEvents(fetcher, pageLink, result);
return result;
}
- private void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture result) {
+ private void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture> result) {
try {
PageData pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink);
if (isConnected() && !pageData.getData().isEmpty()) {
@@ -377,8 +400,15 @@ public final class EdgeGrpcSession implements Closeable {
if (isConnected() && pageData.hasNext()) {
processEdgeEvents(fetcher, pageLink.nextPageLink(), result);
} else {
- UUID ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId();
- result.set(ifOffset);
+ EdgeEvent latestEdgeEvent = pageData.getData().get(pageData.getData().size() - 1);
+ UUID idOffset = latestEdgeEvent.getUuidId();
+ if (idOffset != null) {
+ Long newStartTs = Uuids.unixTimestamp(idOffset);
+ long newStartSeqId = latestEdgeEvent.getSeqId();
+ result.set(Pair.of(newStartTs, newStartSeqId));
+ } else {
+ result.set(null);
+ }
}
}
}
@@ -461,69 +491,113 @@ public final class EdgeGrpcSession implements Closeable {
}
}
- private DownlinkMsg convertToDownlinkMsg(EdgeEvent edgeEvent) {
- log.trace("[{}][{}] converting edge event to downlink msg [{}]", edge.getTenantId(), this.sessionId, edgeEvent);
- DownlinkMsg downlinkMsg = null;
- try {
- 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:
- case CREDENTIALS_REQUEST:
- case RPC_CALL:
- downlinkMsg = convertEntityEventToDownlink(edgeEvent);
- log.trace("[{}][{}] entity message processed [{}]", edgeEvent.getTenantId(), this.sessionId, downlinkMsg);
- break;
- case ATTRIBUTES_UPDATED:
- case POST_ATTRIBUTES:
- case ATTRIBUTES_DELETED:
- case TIMESERIES_UPDATED:
- downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edgeEvent);
- break;
- default:
- log.warn("[{}][{}] Unsupported action type [{}]", edge.getTenantId(), this.sessionId, edgeEvent.getAction());
+ private List convertToDownlinkMsgsPack(List edgeEvents) {
+ List result = new ArrayList<>();
+ for (EdgeEvent edgeEvent : edgeEvents) {
+ log.trace("[{}][{}] converting edge event to downlink msg [{}]", edge.getTenantId(), this.sessionId, edgeEvent);
+ DownlinkMsg downlinkMsg = null;
+ try {
+ 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 CREDENTIALS_REQUEST:
+ case RPC_CALL:
+ case ASSIGNED_TO_CUSTOMER:
+ case UNASSIGNED_FROM_CUSTOMER:
+ downlinkMsg = convertEntityEventToDownlink(edgeEvent);
+ log.trace("[{}][{}] entity message processed [{}]", edgeEvent.getTenantId(), this.sessionId, downlinkMsg);
+ break;
+ case ATTRIBUTES_UPDATED:
+ case POST_ATTRIBUTES:
+ case ATTRIBUTES_DELETED:
+ case TIMESERIES_UPDATED:
+ downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edgeEvent);
+ break;
+ default:
+ log.warn("[{}][{}] Unsupported action type [{}]", edge.getTenantId(), this.sessionId, edgeEvent.getAction());
+ }
+ } catch (Exception e) {
+ log.error("[{}][{}] Exception during converting edge event to downlink msg", edge.getTenantId(), this.sessionId, e);
+ }
+ if (downlinkMsg != null) {
+ result.add(downlinkMsg);
+ }
+ }
+ return result;
+ }
+
+ private ListenableFuture> getQueueStartTsAndSeqId() {
+ ListenableFuture> future =
+ ctx.getAttributesService().find(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, Arrays.asList(QUEUE_START_TS_ATTR_KEY, QUEUE_START_SEQ_ID_ATTR_KEY));
+ return Futures.transform(future, attributeKvEntries -> {
+ long startTs = 0L;
+ long startSeqId = 0L;
+ for (AttributeKvEntry attributeKvEntry : attributeKvEntries) {
+ if (QUEUE_START_TS_ATTR_KEY.equals(attributeKvEntry.getKey())) {
+ startTs = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L;
+ }
+ if (QUEUE_START_SEQ_ID_ATTR_KEY.equals(attributeKvEntry.getKey())) {
+ startSeqId = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L;
+ }
+ }
+ if (startSeqId == 0L) {
+ startSeqId = findStartSeqIdFromOldestEventIfAny();
}
+ return Pair.of(startTs, startSeqId);
+ }, ctx.getGrpcCallbackExecutorService());
+ }
+
+ private boolean isSeqIdStartedNewCycle() {
+ try {
+ TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, this.newStartTs, System.currentTimeMillis());
+ PageData edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), 0L, this.previousStartSeqId == 0 ? null : this.previousStartSeqId - 1, pageLink);
+ return !edgeEvents.getData().isEmpty();
} catch (Exception e) {
- log.error("[{}][{}] Exception during converting edge event to downlink msg", edge.getTenantId(), this.sessionId, e);
+ log.error("[{}][{}][{}] Failed to execute isSeqIdStartedNewCycle", edge.getTenantId(), edge.getId(), sessionId, e);
}
- return downlinkMsg;
+ return false;
}
- private List convertToDownlinkMsgsPack(List edgeEvents) {
- return edgeEvents
- .stream()
- .map(this::convertToDownlinkMsg)
- .filter(Objects::nonNull)
- .collect(Collectors.toList());
+ private boolean isNewEdgeEventsAvailable() {
+ try {
+ TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, this.newStartTs, System.currentTimeMillis());
+ PageData edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), this.newStartSeqId, null, pageLink);
+ return !edgeEvents.getData().isEmpty();
+ } catch (Exception e) {
+ log.error("[{}][{}][{}] Failed to execute isNewEdgeEventsAvailable", edge.getTenantId(), edge.getId(), sessionId, e);
+ }
+ return false;
}
- 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;
+ private long findStartSeqIdFromOldestEventIfAny() {
+ long startSeqId = 0L;
+ try {
+ TimePageLink pageLink = new TimePageLink(1, 0, null, new SortOrder("createdTime"), null, null);
+ PageData edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), null, null, pageLink);
+ if (!edgeEvents.getData().isEmpty()) {
+ startSeqId = edgeEvents.getData().get(0).getSeqId() - 1;
}
- }, ctx.getGrpcCallbackExecutorService());
+ } catch (Exception e) {
+ log.error("[{}][{}][{}] Failed to execute findStartSeqIdFromOldestEventIfAny", edge.getTenantId(), edge.getId(), sessionId, e);
+ }
+ return startSeqId;
}
- private ListenableFuture> updateQueueStartTs(Long newStartTs) {
- log.trace("[{}] updating QueueStartTs [{}][{}]", this.sessionId, edge.getId(), newStartTs);
- List attributes = Collections.singletonList(
- new BaseAttributeKvEntry(
- new LongDataEntry(QUEUE_START_TS_ATTR_KEY, newStartTs), System.currentTimeMillis()));
+ private ListenableFuture> updateQueueStartTsAndSeqId(Pair pair) {
+ this.newStartTs = pair.getFirst();
+ this.newStartSeqId = pair.getSecond();
+ log.trace("[{}] updateQueueStartTsAndSeqId [{}][{}][{}]", this.sessionId, edge.getId(), this.newStartTs, this.newStartSeqId);
+ List attributes = Arrays.asList(
+ new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_TS_ATTR_KEY, this.newStartTs), System.currentTimeMillis()),
+ new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_SEQ_ID_ATTR_KEY, this.newStartSeqId), System.currentTimeMillis()));
return ctx.getAttributesService().save(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, attributes);
}
@@ -693,8 +767,11 @@ public final class EdgeGrpcSession implements Closeable {
}
private void interruptPreviousSendDownlinkMsgsTask() {
- log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId);
- stopCurrentSendDownlinkMsgsTask(true);
+ if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone()
+ || sessionState.getScheduledSendDownlinkTask() != null && !sessionState.getScheduledSendDownlinkTask().isCancelled()) {
+ log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId);
+ stopCurrentSendDownlinkMsgsTask(true);
+ }
}
private void interruptGeneralProcessingOnSync() {
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
index 69a83da0a0..447a73e5cf 100644
--- 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
@@ -18,7 +18,10 @@ package org.thingsboard.server.service.edge.rpc.constructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityViewId;
@@ -47,13 +50,22 @@ public class AlarmMsgConstructor {
String entityName = null;
switch (alarm.getOriginator().getEntityType()) {
case DEVICE:
- entityName = deviceService.findDeviceById(tenantId, new DeviceId(alarm.getOriginator().getId())).getName();
+ Device deviceById = deviceService.findDeviceById(tenantId, new DeviceId(alarm.getOriginator().getId()));
+ if (deviceById != null) {
+ entityName = deviceById.getName();
+ }
break;
case ASSET:
- entityName = assetService.findAssetById(tenantId, new AssetId(alarm.getOriginator().getId())).getName();
+ Asset assetById = assetService.findAssetById(tenantId, new AssetId(alarm.getOriginator().getId()));
+ if (assetById != null) {
+ entityName = assetById.getName();
+ }
break;
case ENTITY_VIEW:
- entityName = entityViewService.findEntityViewById(tenantId, new EntityViewId(alarm.getOriginator().getId())).getName();
+ EntityView entityViewById = entityViewService.findEntityViewById(tenantId, new EntityViewId(alarm.getOriginator().getId()));
+ if (entityViewById != null) {
+ entityName = entityViewById.getName();
+ }
break;
}
AlarmUpdateMsg.Builder builder = AlarmUpdateMsg.newBuilder()
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java
index 327184e6a9..24008ece09 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java
@@ -16,19 +16,27 @@
package org.thingsboard.server.service.edge.rpc.fetch;
import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
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.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.edge.EdgeEventService;
@AllArgsConstructor
+@Slf4j
public class GeneralEdgeEventFetcher implements EdgeEventFetcher {
private final Long queueStartTs;
+ private Long seqIdStart;
+ @Getter
+ private Long seqIdEnd;
+ @Getter
+ private boolean seqIdNewCycleStarted;
+ private Long maxReadRecordsCount;
private final EdgeEventService edgeEventService;
@Override
@@ -37,13 +45,32 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher {
pageSize,
0,
null,
- new SortOrder("createdTime", SortOrder.Direction.ASC),
+ null,
queueStartTs,
- null);
+ System.currentTimeMillis());
}
@Override
public PageData fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) {
- return edgeEventService.findEdgeEvents(tenantId, edge.getId(), (TimePageLink) pageLink, true);
+ try {
+ PageData edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), seqIdStart, seqIdEnd, (TimePageLink) pageLink);
+ if (edgeEvents.getData().isEmpty()) {
+ this.seqIdEnd = Math.max(this.maxReadRecordsCount, seqIdStart - this.maxReadRecordsCount);
+ edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), 0L, seqIdEnd, (TimePageLink) pageLink);
+ if (edgeEvents.getData().stream().anyMatch(ee -> ee.getSeqId() < seqIdStart)) {
+ log.info("[{}] seqId column of edge_event table started new cycle [{}]", tenantId, edge.getId());
+ this.seqIdNewCycleStarted = true;
+ this.seqIdStart = 0L;
+ } else {
+ edgeEvents = new PageData<>();
+ log.warn("[{}] unexpected edge notification message received. " +
+ "no new events found and seqId column of edge_event table doesn't started new cycle [{}]", tenantId, edge.getId());
+ }
+ }
+ return edgeEvents;
+ } catch (Exception e) {
+ log.error("[{}] failed to find edge events [{}]", tenantId, edge.getId());
+ }
+ return new PageData<>();
}
}
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 69cdebd55e..c0396e2d9e 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
@@ -709,4 +709,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
executor.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
}
+ @Override
+ public void updateDefaultNotificationConfigs() {
+ notificationSettingsService.updateDefaultNotificationConfigs(TenantId.SYS_TENANT_ID);
+ }
+
}
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 9a810aa446..36ec2b9b78 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
@@ -48,6 +48,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Optional;
+import java.util.function.Function;
import static org.thingsboard.server.utils.LwM2mObjectModelUtils.toLwm2mResource;
@@ -173,7 +174,7 @@ public class InstallScripts {
ruleChain = ruleChainService.saveRuleChain(ruleChain);
ruleChainMetaData.setRuleChainId(ruleChain.getId());
- ruleChainService.saveRuleChainMetaData(TenantId.SYS_TENANT_ID, ruleChainMetaData);
+ ruleChainService.saveRuleChainMetaData(TenantId.SYS_TENANT_ID, ruleChainMetaData, Function.identity());
return ruleChain;
}
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 e1a34e616b..34515f827d 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
@@ -744,6 +744,18 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
} catch (Exception e) {
}
}
+ try {
+ conn.createStatement().execute("ALTER TABLE component_descriptor ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;");
+ } catch (Exception e) {
+ }
+ try {
+ conn.createStatement().execute("ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;");
+ } catch (Exception e) {
+ }
+ try {
+ conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);");
+ } catch (Exception e) {
+ }
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005002;");
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
index fb6b28592c..08de12da78 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
@@ -41,4 +41,6 @@ public interface SystemDataLoaderService {
void createDefaultNotificationConfigs();
+ void updateDefaultNotificationConfigs();
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DataUpdateService.java
index ae573b829a..289e3b1c94 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/update/DataUpdateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/update/DataUpdateService.java
@@ -19,4 +19,5 @@ public interface DataUpdateService {
void updateData(String fromVersion) throws Exception;
+ void upgradeRuleNodes();
}
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 13943d5698..dafccfdad6 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
@@ -17,6 +17,7 @@ package org.thingsboard.server.service.install.update;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@@ -26,6 +27,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration;
import org.thingsboard.rule.engine.profile.TbDeviceProfileNode;
@@ -46,6 +48,7 @@ import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
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.PageDataIterable;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.query.DynamicValue;
@@ -62,6 +65,7 @@ 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.TenantProfileQueueConfiguration;
+import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.alarm.AlarmDao;
import org.thingsboard.server.dao.audit.AuditLogDao;
@@ -77,15 +81,18 @@ import org.thingsboard.server.dao.sql.device.DeviceProfileRepository;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.install.SystemDataLoaderService;
-import org.thingsboard.server.service.install.TbRuleEngineQueueConfigService;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Function;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.StringUtils.isBlank;
@@ -133,7 +140,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
private QueueService queueService;
@Autowired
- private TbRuleEngineQueueConfigService queueConfig;
+ private ComponentDiscoveryService componentDiscoveryService;
@Autowired
private SystemDataLoaderService systemDataLoaderService;
@@ -195,19 +202,73 @@ public class DefaultDataUpdateService implements DataUpdateService {
} else {
log.info("Skipping audit logs migration");
}
- boolean skipEdgeEventsMigration = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false);
- if (!skipEdgeEventsMigration) {
- log.info("Starting edge events migration. Can be skipped with TB_SKIP_EDGE_EVENTS_MIGRATION env variable set to true");
- edgeEventDao.migrateEdgeEvents();
- } else {
- log.info("Skipping edge events migration");
- }
+ migrateEdgeEvents("Starting edge events migration. ");
+ break;
+ case "3.5.1":
+ log.info("Updating data from version 3.5.1 to 3.5.2 ...");
+ migrateEdgeEvents("Starting edge events migration - adding seq_id column. ");
break;
default:
throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion);
}
}
+ private void migrateEdgeEvents(String logPrefix) {
+ boolean skipEdgeEventsMigration = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false);
+ if (!skipEdgeEventsMigration) {
+ log.info(logPrefix + "Can be skipped with TB_SKIP_EDGE_EVENTS_MIGRATION env variable set to true");
+ edgeEventDao.migrateEdgeEvents();
+ } else {
+ log.info("Skipping edge events migration");
+ }
+ }
+
+ @Override
+ public void upgradeRuleNodes() {
+ try {
+ log.info("Lookup rule nodes to upgrade ...");
+ var nodeClassToVersionMap = componentDiscoveryService.getVersionedNodes();
+ log.info("Found {} versioned nodes to check for upgrade!", nodeClassToVersionMap.size());
+ nodeClassToVersionMap.forEach(clazz -> {
+ var ruleNodeType = clazz.getClassName();
+ var ruleNodeTypeForLogs = clazz.getSimpleName();
+ var toVersion = clazz.getCurrentVersion();
+ log.info("Going to check for nodes with type: {} to upgrade to version: {}.", ruleNodeTypeForLogs, toVersion);
+ var ruleNodesToUpdate = new PageDataIterable<>(
+ pageLink -> ruleChainService.findAllRuleNodesByTypeAndVersionLessThan(ruleNodeType, toVersion, pageLink), 1024
+ );
+ if (Iterables.isEmpty(ruleNodesToUpdate)) {
+ log.info("There are no active nodes with type: {}, or all nodes with this type already set to latest version!", ruleNodeTypeForLogs);
+ } else {
+ for (var ruleNode : ruleNodesToUpdate) {
+ var ruleNodeId = ruleNode.getId();
+ var oldConfiguration = ruleNode.getConfiguration();
+ int fromVersion = ruleNode.getConfigurationVersion();
+ log.info("Going to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",
+ ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion);
+ try {
+ var tbVersionedNode = (TbVersionedNode) clazz.getClazz().getDeclaredConstructor().newInstance();
+ TbPair upgradeRuleNodeConfigurationResult = tbVersionedNode.upgrade(fromVersion, oldConfiguration);
+ if (upgradeRuleNodeConfigurationResult.getFirst()) {
+ ruleNode.setConfiguration(upgradeRuleNodeConfigurationResult.getSecond());
+ }
+ ruleNode.setConfigurationVersion(toVersion);
+ ruleChainService.saveRuleNode(TenantId.SYS_TENANT_ID, ruleNode);
+ log.info("Successfully upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",
+ ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion);
+ } catch (Exception e) {
+ log.warn("Failed to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {} due to: ",
+ ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion, e);
+ }
+ }
+ }
+ });
+ log.info("Finished rule nodes upgrade!");
+ } catch (Exception e) {
+ log.error("Unexpected error during rule nodes upgrade: ", e);
+ }
+ }
+
private final PaginatedUpdater deviceProfileEntityDynamicConditionsUpdater =
new PaginatedUpdater<>() {
@@ -442,7 +503,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
md.getNodes().add(ruleNode);
md.setFirstNodeIndex(newIdx);
md.addConnectionInfo(newIdx, oldIdx, "Success");
- ruleChainService.saveRuleChainMetaData(tenant.getId(), md);
+ ruleChainService.saveRuleChainMetaData(tenant.getId(), md, Function.identity());
}
} catch (Exception e) {
log.error("[{}] Unable to update Tenant: {}", tenant.getId(), tenant.getName(), e);
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 5fe876bb75..053a766b7b 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
@@ -16,6 +16,7 @@
package org.thingsboard.server.service.mail;
import com.fasterxml.jackson.databind.JsonNode;
+
import freemarker.template.Configuration;
import freemarker.template.Template;
import jakarta.xml.bind.DatatypeConverter;
@@ -54,7 +55,6 @@ import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
-import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -62,7 +62,6 @@ import java.util.concurrent.TimeoutException;
@Slf4j
public class DefaultMailService implements MailService {
- public static final String MAIL_PROP = "mail.";
public static final String TARGET_EMAIL = "targetEmail";
public static final String UTF_8 = "UTF-8";
@@ -83,7 +82,10 @@ public class DefaultMailService implements MailService {
@Autowired
private PasswordResetExecutorService passwordResetExecutorService;
- private JavaMailSenderImpl mailSender;
+ @Autowired
+ private TbMailContextComponent tbMailContextComponent;
+
+ private TbMailSender mailSender;
private String mailFrom;
@@ -106,7 +108,7 @@ public class DefaultMailService implements MailService {
AdminSettings settings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
if (settings != null) {
JsonNode jsonConfig = settings.getJsonValue();
- mailSender = createMailSender(jsonConfig);
+ mailSender = new TbMailSender(tbMailContextComponent, jsonConfig);
mailFrom = jsonConfig.get("mailFrom").asText();
timeout = jsonConfig.get("timeout").asLong(DEFAULT_TIMEOUT);
} else {
@@ -114,65 +116,6 @@ public class DefaultMailService implements MailService {
}
}
- private JavaMailSenderImpl createMailSender(JsonNode jsonConfig) {
- JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
- mailSender.setHost(jsonConfig.get("smtpHost").asText());
- mailSender.setPort(parsePort(jsonConfig.get("smtpPort").asText()));
- mailSender.setUsername(jsonConfig.get("username").asText());
- mailSender.setPassword(jsonConfig.get("password").asText());
- mailSender.setJavaMailProperties(createJavaMailProperties(jsonConfig));
- return mailSender;
- }
-
- private Properties createJavaMailProperties(JsonNode jsonConfig) {
- Properties javaMailProperties = new Properties();
- String protocol = jsonConfig.get("smtpProtocol").asText();
- javaMailProperties.put("mail.transport.protocol", protocol);
- javaMailProperties.put(MAIL_PROP + protocol + ".host", jsonConfig.get("smtpHost").asText());
- javaMailProperties.put(MAIL_PROP + protocol + ".port", jsonConfig.get("smtpPort").asText());
- javaMailProperties.put(MAIL_PROP + protocol + ".timeout", jsonConfig.get("timeout").asText());
- javaMailProperties.put(MAIL_PROP + protocol + ".auth", String.valueOf(StringUtils.isNotEmpty(jsonConfig.get("username").asText())));
- boolean enableTls = false;
- if (jsonConfig.has("enableTls")) {
- if (jsonConfig.get("enableTls").isBoolean() && jsonConfig.get("enableTls").booleanValue()) {
- enableTls = true;
- } else if (jsonConfig.get("enableTls").isTextual()) {
- enableTls = "true".equalsIgnoreCase(jsonConfig.get("enableTls").asText());
- }
- }
- javaMailProperties.put(MAIL_PROP + protocol + ".starttls.enable", enableTls);
- if (enableTls && jsonConfig.has("tlsVersion") && !jsonConfig.get("tlsVersion").isNull()) {
- String tlsVersion = jsonConfig.get("tlsVersion").asText();
- if (StringUtils.isNoneEmpty(tlsVersion)) {
- javaMailProperties.put(MAIL_PROP + protocol + ".ssl.protocols", tlsVersion);
- }
- }
-
- boolean enableProxy = jsonConfig.has("enableProxy") && jsonConfig.get("enableProxy").asBoolean();
-
- if (enableProxy) {
- javaMailProperties.put(MAIL_PROP + protocol + ".proxy.host", jsonConfig.get("proxyHost").asText());
- javaMailProperties.put(MAIL_PROP + protocol + ".proxy.port", jsonConfig.get("proxyPort").asText());
- String proxyUser = jsonConfig.get("proxyUser").asText();
- if (StringUtils.isNoneEmpty(proxyUser)) {
- javaMailProperties.put(MAIL_PROP + protocol + ".proxy.user", proxyUser);
- }
- String proxyPassword = jsonConfig.get("proxyPassword").asText();
- if (StringUtils.isNoneEmpty(proxyPassword)) {
- javaMailProperties.put(MAIL_PROP + protocol + ".proxy.password", proxyPassword);
- }
- }
- return javaMailProperties;
- }
-
- private int parsePort(String strPort) {
- try {
- return Integer.valueOf(strPort);
- } catch (NumberFormatException e) {
- throw new IncorrectParameterException(String.format("Invalid smtp port value: %s", strPort));
- }
- }
-
@Override
public void sendEmail(TenantId tenantId, String email, String subject, String message) throws ThingsboardException {
sendMail(mailSender, mailFrom, email, subject, message, timeout);
@@ -180,7 +123,7 @@ public class DefaultMailService implements MailService {
@Override
public void sendTestMail(JsonNode jsonConfig, String email) throws ThingsboardException {
- JavaMailSenderImpl testMailSender = createMailSender(jsonConfig);
+ TbMailSender testMailSender = new TbMailSender(tbMailContextComponent, jsonConfig);
String mailFrom = jsonConfig.get("mailFrom").asText();
String subject = messages.getMessage("test.message.subject", null, Locale.US);
long timeout = jsonConfig.get("timeout").asLong(DEFAULT_TIMEOUT);
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultTbMailConfigTemplateService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultTbMailConfigTemplateService.java
new file mode 100644
index 0000000000..cb502e7e51
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultTbMailConfigTemplateService.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.mail;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.stereotype.Service;
+import org.thingsboard.common.util.JacksonUtil;
+
+import javax.annotation.PostConstruct;
+import java.io.IOException;
+
+@Service
+@Slf4j
+public class DefaultTbMailConfigTemplateService implements TbMailConfigTemplateService {
+
+ private JsonNode mailConfigTemplates;
+
+ @PostConstruct
+ private void postConstruct() throws IOException {
+ mailConfigTemplates = JacksonUtil.toJsonNode(new ClassPathResource("/templates/mail_config_templates.json").getFile());
+ }
+
+ @Override
+ public JsonNode findAllMailConfigTemplates() {
+ return mailConfigTemplates;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/RefreshTokenExpCheckService.java b/application/src/main/java/org/thingsboard/server/service/mail/RefreshTokenExpCheckService.java
new file mode 100644
index 0000000000..56f3c4c4c5
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/mail/RefreshTokenExpCheckService.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.mail;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.api.client.auth.oauth2.ClientParametersAuthentication;
+import com.google.api.client.auth.oauth2.RefreshTokenRequest;
+import com.google.api.client.auth.oauth2.TokenResponse;
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.gson.GsonFactory;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.settings.AdminSettingsService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+
+import static org.thingsboard.server.common.data.mail.MailOauth2Provider.OFFICE_365;
+
+@TbCoreComponent
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class RefreshTokenExpCheckService {
+ public static final int AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS = 90;
+ private final AdminSettingsService adminSettingsService;
+
+ @Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${mail.oauth2.refreshTokenCheckingInterval})}", fixedDelayString = "${mail.oauth2.refreshTokenCheckingInterval}")
+ public void check() throws IOException {
+ AdminSettings settings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
+ if (settings != null && settings.getJsonValue().has("enableOauth2") && settings.getJsonValue().get("enableOauth2").asBoolean()) {
+ JsonNode jsonValue = settings.getJsonValue();
+ if (OFFICE_365.name().equals(jsonValue.get("providerId").asText()) && jsonValue.has("refreshTokenExpires")) {
+ long expiresIn = jsonValue.get("refreshTokenExpires").longValue();
+ if ((expiresIn - System.currentTimeMillis()) < 604800000L) { //less than 7 days
+ log.info("Trying to refresh refresh token.");
+
+ String clientId = jsonValue.get("clientId").asText();
+ String clientSecret = jsonValue.get("clientSecret").asText();
+ String refreshToken = jsonValue.get("refreshToken").asText();
+ String tokenUri = jsonValue.get("tokenUri").asText();
+
+ TokenResponse tokenResponse = new RefreshTokenRequest(new NetHttpTransport(), new GsonFactory(),
+ new GenericUrl(tokenUri), refreshToken)
+ .setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret))
+ .execute();
+ ((ObjectNode)jsonValue).put("refreshToken", tokenResponse.getRefreshToken());
+ ((ObjectNode)jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli());
+ adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, settings);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/TbMailConfigTemplateService.java b/application/src/main/java/org/thingsboard/server/service/mail/TbMailConfigTemplateService.java
new file mode 100644
index 0000000000..b69ae0977c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/mail/TbMailConfigTemplateService.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.mail;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import java.io.IOException;
+
+public interface TbMailConfigTemplateService {
+ JsonNode findAllMailConfigTemplates() throws IOException;
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java b/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java
new file mode 100644
index 0000000000..dce6c10b7e
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.mail;
+
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.dao.settings.AdminSettingsService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+
+@Component
+@Data
+@Lazy
+public class TbMailContextComponent {
+
+ @Autowired
+ private AdminSettingsService adminSettingsService;
+}
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java b/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java
new file mode 100644
index 0000000000..e8e9271998
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java
@@ -0,0 +1,168 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.mail;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.api.client.auth.oauth2.ClientParametersAuthentication;
+import com.google.api.client.auth.oauth2.RefreshTokenRequest;
+import com.google.api.client.auth.oauth2.TokenResponse;
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.gson.GsonFactory;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.lang.Nullable;
+import org.springframework.mail.javamail.JavaMailSenderImpl;
+import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.common.data.StringUtils;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.mail.MailOauth2Provider;
+import org.thingsboard.server.dao.exception.IncorrectParameterException;
+
+import jakarta.mail.internet.MimeMessage;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Properties;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import static org.thingsboard.server.service.mail.RefreshTokenExpCheckService.AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS;
+
+@Slf4j
+public class TbMailSender extends JavaMailSenderImpl {
+
+ private static final String MAIL_PROP = "mail.";
+ private final TbMailContextComponent ctx;
+ private final Lock lock;
+ private final Boolean oauth2Enabled;
+ private volatile String accessToken;
+ private volatile long tokenExpires;
+
+ public TbMailSender(TbMailContextComponent ctx, JsonNode jsonConfig) {
+ super();
+ this.lock = new ReentrantLock();
+ this.tokenExpires = 0L;
+ this.ctx = ctx;
+ this.oauth2Enabled = jsonConfig.has("enableOauth2") && jsonConfig.get("enableOauth2").asBoolean();
+
+ setHost(jsonConfig.get("smtpHost").asText());
+ setPort(parsePort(jsonConfig.get("smtpPort").asText()));
+ setUsername(jsonConfig.get("username").asText());
+ if (jsonConfig.has("password")) {
+ setPassword(jsonConfig.get("password").asText());
+ }
+ setJavaMailProperties(createJavaMailProperties(jsonConfig));
+ }
+
+ @SneakyThrows
+ @Override
+ public void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) {
+ if (oauth2Enabled && (System.currentTimeMillis() > tokenExpires)){
+ refreshAccessToken();
+ setPassword(accessToken);
+ }
+ super.doSend(mimeMessages, originalMessages);
+ }
+
+ private Properties createJavaMailProperties(JsonNode jsonConfig) {
+ Properties javaMailProperties = new Properties();
+ String protocol = jsonConfig.get("smtpProtocol").asText();
+ javaMailProperties.put("mail.transport.protocol", protocol);
+ javaMailProperties.put(MAIL_PROP + protocol + ".host", jsonConfig.get("smtpHost").asText());
+ javaMailProperties.put(MAIL_PROP + protocol + ".port", jsonConfig.get("smtpPort").asText());
+ javaMailProperties.put(MAIL_PROP + protocol + ".timeout", jsonConfig.get("timeout").asText());
+ javaMailProperties.put(MAIL_PROP + protocol + ".auth", String.valueOf(StringUtils.isNotEmpty(jsonConfig.get("username").asText())));
+ boolean enableTls = false;
+ if (jsonConfig.has("enableTls")) {
+ if (jsonConfig.get("enableTls").isBoolean() && jsonConfig.get("enableTls").booleanValue()) {
+ enableTls = true;
+ } else if (jsonConfig.get("enableTls").isTextual()) {
+ enableTls = "true".equalsIgnoreCase(jsonConfig.get("enableTls").asText());
+ }
+ }
+ javaMailProperties.put(MAIL_PROP + protocol + ".starttls.enable", enableTls);
+ if (enableTls && jsonConfig.has("tlsVersion") && !jsonConfig.get("tlsVersion").isNull()) {
+ String tlsVersion = jsonConfig.get("tlsVersion").asText();
+ if (StringUtils.isNoneEmpty(tlsVersion)) {
+ javaMailProperties.put(MAIL_PROP + protocol + ".ssl.protocols", tlsVersion);
+ }
+ }
+
+ boolean enableProxy = jsonConfig.has("enableProxy") && jsonConfig.get("enableProxy").asBoolean();
+
+ if (enableProxy) {
+ javaMailProperties.put(MAIL_PROP + protocol + ".proxy.host", jsonConfig.get("proxyHost").asText());
+ javaMailProperties.put(MAIL_PROP + protocol + ".proxy.port", jsonConfig.get("proxyPort").asText());
+ String proxyUser = jsonConfig.get("proxyUser").asText();
+ if (StringUtils.isNoneEmpty(proxyUser)) {
+ javaMailProperties.put(MAIL_PROP + protocol + ".proxy.user", proxyUser);
+ }
+ String proxyPassword = jsonConfig.get("proxyPassword").asText();
+ if (StringUtils.isNoneEmpty(proxyPassword)) {
+ javaMailProperties.put(MAIL_PROP + protocol + ".proxy.password", proxyPassword);
+ }
+ }
+
+ if (oauth2Enabled) {
+ javaMailProperties.put(MAIL_PROP + protocol + ".auth.mechanisms", "XOAUTH2");
+ }
+ return javaMailProperties;
+ }
+
+ public void refreshAccessToken() throws ThingsboardException {
+ lock.lock();
+ try {
+ if (System.currentTimeMillis() > tokenExpires) {
+ AdminSettings settings = ctx.getAdminSettingsService().findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
+ JsonNode jsonValue = settings.getJsonValue();
+
+ String clientId = jsonValue.get("clientId").asText();
+ String clientSecret = jsonValue.get("clientSecret").asText();
+ String refreshToken = jsonValue.get("refreshToken").asText();
+ String tokenUri = jsonValue.get("tokenUri").asText();
+ String providerId = jsonValue.get("providerId").asText();
+
+ TokenResponse tokenResponse = new RefreshTokenRequest(new NetHttpTransport(), new GsonFactory(),
+ new GenericUrl(tokenUri), refreshToken)
+ .setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret))
+ .execute();
+ if (MailOauth2Provider.OFFICE_365.name().equals(providerId)) {
+ ((ObjectNode)jsonValue).put("refreshToken", tokenResponse.getRefreshToken());
+ ((ObjectNode)jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli());
+ ctx.getAdminSettingsService().saveAdminSettings(TenantId.SYS_TENANT_ID, settings);
+ }
+ accessToken = tokenResponse.getAccessToken();
+ tokenExpires = System.currentTimeMillis() + (tokenResponse.getExpiresInSeconds().intValue() * 1000);
+ }
+ } catch (Exception e) {
+ log.warn("Unable to retrieve access token: {}", e.getMessage());
+ throw new ThingsboardException("Error while retrieving access token: " + e.getMessage(), ThingsboardErrorCode.GENERAL);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private int parsePort(String strPort) {
+ try {
+ return Integer.parseInt(strPort);
+ } catch (NumberFormatException e) {
+ throw new IncorrectParameterException(String.format("Invalid smtp port value: %s", strPort));
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
index 0cb9dc780b..cb77cc0948 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
@@ -15,13 +15,10 @@
*/
package org.thingsboard.server.service.notification;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
-import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
@@ -31,6 +28,7 @@ import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.AlreadySentException;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
@@ -59,14 +57,11 @@ import org.thingsboard.server.dao.notification.NotificationService;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
-import org.thingsboard.server.dao.user.UserService;
+import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.NotificationsTopicService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
-import org.thingsboard.server.dao.util.limits.LimitedApi;
-import org.thingsboard.server.dao.util.limits.RateLimitService;
-import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.executors.NotificationExecutorService;
import org.thingsboard.server.service.notification.channels.NotificationChannel;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils;
@@ -74,7 +69,6 @@ import org.thingsboard.server.service.telemetry.AbstractSubscriptionService;
import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -87,7 +81,7 @@ import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
-@SuppressWarnings({"UnstableApiUsage", "rawtypes"})
+@SuppressWarnings({"rawtypes"})
public class DefaultNotificationCenter extends AbstractSubscriptionService implements NotificationCenter, NotificationChannel {
private final NotificationTargetService notificationTargetService;
@@ -95,9 +89,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
private final NotificationService notificationService;
private final NotificationTemplateService notificationTemplateService;
private final NotificationSettingsService notificationSettingsService;
- private final UserService userService;
private final NotificationExecutorService notificationExecutor;
- private final DbCallbackExecutorService dbCallbackExecutorService;
private final NotificationsTopicService notificationsTopicService;
private final TbQueueProducerProvider producerProvider;
private final RateLimitService rateLimitService;
@@ -172,37 +164,32 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
.build();
notificationExecutor.submit(() -> {
- List> results = new ArrayList<>();
-
for (NotificationTarget target : targets) {
- List> result = processForTarget(target, ctx);
- results.addAll(result);
+ processForTarget(target, ctx);
}
- Futures.whenAllComplete(results).run(() -> {
- NotificationRequestId requestId = ctx.getRequest().getId();
- log.debug("[{}] Notification request processing is finished", requestId);
- NotificationRequestStats stats = ctx.getStats();
+ NotificationRequestId requestId = ctx.getRequest().getId();
+ log.debug("[{}] Notification request processing is finished", requestId);
+ NotificationRequestStats stats = ctx.getStats();
+ try {
+ notificationRequestService.updateNotificationRequest(tenantId, requestId, NotificationRequestStatus.SENT, stats);
+ } catch (Exception e) {
+ log.error("[{}] Failed to update stats for notification request", requestId, e);
+ }
+
+ if (callback != null) {
try {
- notificationRequestService.updateNotificationRequest(tenantId, requestId, NotificationRequestStatus.SENT, stats);
+ callback.accept(stats);
} catch (Exception e) {
- log.error("[{}] Failed to update stats for notification request", requestId, e);
- }
-
- if (callback != null) {
- try {
- callback.accept(stats);
- } catch (Exception e) {
- log.error("Failed to process callback for notification request {}", requestId, e);
- }
+ log.error("Failed to process callback for notification request {}", requestId, e);
}
- }, dbCallbackExecutorService);
+ }
});
return request;
}
- private List> processForTarget(NotificationTarget target, NotificationProcessingContext ctx) {
+ private void processForTarget(NotificationTarget target, NotificationProcessingContext ctx) {
Iterable extends NotificationRecipient> recipients;
switch (target.getConfiguration().getType()) {
case PLATFORM_USERS: {
@@ -231,43 +218,35 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
Set deliveryMethods = new HashSet<>(ctx.getDeliveryMethods());
deliveryMethods.removeIf(deliveryMethod -> !target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod));
log.debug("[{}] Processing notification request for {} target ({}) for delivery methods {}", ctx.getRequest().getId(), target.getConfiguration().getType(), target.getId(), deliveryMethods);
+ if (deliveryMethods.isEmpty()) {
+ return;
+ }
- List> results = new ArrayList<>();
- if (!deliveryMethods.isEmpty()) {
- for (NotificationRecipient recipient : recipients) {
- for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) {
- ListenableFuture resultFuture = processForRecipient(deliveryMethod, recipient, ctx);
- DonAsynchron.withCallback(resultFuture, result -> {
- ctx.getStats().reportSent(deliveryMethod, recipient);
- }, error -> {
- ctx.getStats().reportError(deliveryMethod, error, recipient);
- });
- results.add(resultFuture);
+ for (NotificationRecipient recipient : recipients) {
+ for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) {
+ try {
+ processForRecipient(deliveryMethod, recipient, ctx);
+ ctx.getStats().reportSent(deliveryMethod, recipient);
+ } catch (Exception error) {
+ ctx.getStats().reportError(deliveryMethod, error, recipient);
}
}
}
- return results;
}
- private ListenableFuture processForRecipient(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient, NotificationProcessingContext ctx) {
+ private void processForRecipient(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient, NotificationProcessingContext ctx) throws Exception {
if (ctx.getStats().contains(deliveryMethod, recipient.getId())) {
- return Futures.immediateFailedFuture(new AlreadySentException());
+ throw new AlreadySentException();
}
-
- DeliveryMethodNotificationTemplate processedTemplate;
- try {
- processedTemplate = ctx.getProcessedTemplate(deliveryMethod, recipient);
- } catch (Exception e) {
- return Futures.immediateFailedFuture(e);
- }
-
NotificationChannel notificationChannel = channels.get(deliveryMethod);
+ DeliveryMethodNotificationTemplate processedTemplate = ctx.getProcessedTemplate(deliveryMethod, recipient);
+
log.trace("[{}] Sending {} notification for recipient {}", ctx.getRequest().getId(), deliveryMethod, recipient);
- return notificationChannel.sendNotification(recipient, processedTemplate, ctx);
+ notificationChannel.sendNotification(recipient, processedTemplate, ctx);
}
@Override
- public ListenableFuture sendNotification(User recipient, WebDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) {
+ public void sendNotification(User recipient, WebDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
NotificationRequest request = ctx.getRequest();
Notification notification = Notification.builder()
.requestId(request.getId())
@@ -283,14 +262,14 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
notification = notificationService.saveNotification(recipient.getTenantId(), notification);
} catch (Exception e) {
log.error("Failed to create notification for recipient {}", recipient.getId(), e);
- return Futures.immediateFailedFuture(e);
+ throw e;
}
NotificationUpdate update = NotificationUpdate.builder()
.created(true)
.notification(notification)
.build();
- return onNotificationUpdate(recipient.getTenantId(), recipient.getId(), update);
+ onNotificationUpdate(recipient.getTenantId(), recipient.getId(), update);
}
@Override
@@ -384,13 +363,11 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
clusterService.pushMsgToCore(tenantId, notificationRequestId, toCoreMsg, null);
}
- private ListenableFuture onNotificationUpdate(TenantId tenantId, UserId recipientId, NotificationUpdate update) {
+ private void onNotificationUpdate(TenantId tenantId, UserId recipientId, NotificationUpdate update) {
log.trace("Submitting notification update for recipient {}: {}", recipientId, update);
- return Futures.submit(() -> {
- forwardToSubscriptionManagerService(tenantId, recipientId, subscriptionManagerService -> {
- subscriptionManagerService.onNotificationUpdate(tenantId, recipientId, update, TbCallback.EMPTY);
- }, () -> TbSubscriptionUtils.notificationUpdateToProto(tenantId, recipientId, update));
- }, wsCallBackExecutor);
+ forwardToSubscriptionManagerService(tenantId, recipientId, subscriptionManagerService -> {
+ subscriptionManagerService.onNotificationUpdate(tenantId, recipientId, update, TbCallback.EMPTY);
+ }, () -> TbSubscriptionUtils.notificationUpdateToProto(tenantId, recipientId, update));
}
private void onNotificationRequestUpdate(TenantId tenantId, NotificationRequestUpdate update) {
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java
index 8b3c9551c2..8e9e8525e3 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java
@@ -15,7 +15,6 @@
*/
package org.thingsboard.server.service.notification.channels;
-import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.MailService;
@@ -24,7 +23,6 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate;
-import org.thingsboard.server.service.mail.MailExecutorService;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
@Component
@@ -32,19 +30,15 @@ import org.thingsboard.server.service.notification.NotificationProcessingContext
public class EmailNotificationChannel implements NotificationChannel {
private final MailService mailService;
- private final MailExecutorService executor;
@Override
- public ListenableFuture sendNotification(User recipient, EmailDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) {
- return executor.submit(() -> {
- mailService.send(recipient.getTenantId(), null, TbEmail.builder()
- .to(recipient.getEmail())
- .subject(processedTemplate.getSubject())
- .body(processedTemplate.getBody())
- .html(true)
- .build());
- return null;
- });
+ public void sendNotification(User recipient, EmailDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
+ mailService.send(recipient.getTenantId(), null, TbEmail.builder()
+ .to(recipient.getEmail())
+ .subject(processedTemplate.getSubject())
+ .body(processedTemplate.getBody())
+ .html(true)
+ .build());
}
@Override
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java
index 02fe6264d2..0275644765 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java
@@ -15,7 +15,6 @@
*/
package org.thingsboard.server.service.notification.channels;
-import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient;
@@ -24,7 +23,7 @@ import org.thingsboard.server.service.notification.NotificationProcessingContext
public interface NotificationChannel {
- ListenableFuture sendNotification(R recipient, T processedTemplate, NotificationProcessingContext ctx);
+ void sendNotification(R recipient, T processedTemplate, NotificationProcessingContext ctx) throws Exception;
void check(TenantId tenantId) throws Exception;
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java
index 46afbd7270..25c6b9dcab 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java
@@ -15,7 +15,6 @@
*/
package org.thingsboard.server.service.notification.channels;
-import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.slack.SlackService;
@@ -26,7 +25,6 @@ import org.thingsboard.server.common.data.notification.settings.SlackNotificatio
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
-import org.thingsboard.server.service.executors.ExternalCallExecutorService;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
@Component
@@ -35,15 +33,11 @@ public class SlackNotificationChannel implements NotificationChannel sendNotification(SlackConversation conversation, SlackDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) {
+ public void sendNotification(SlackConversation conversation, SlackDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
SlackNotificationDeliveryMethodConfig config = ctx.getDeliveryMethodConfig(NotificationDeliveryMethod.SLACK);
- return executor.submit(() -> {
- slackService.sendMessage(ctx.getTenantId(), config.getBotToken(), conversation.getId(), processedTemplate.getBody());
- return null;
- });
+ slackService.sendMessage(ctx.getTenantId(), config.getBotToken(), conversation.getId(), processedTemplate.getBody());
}
@Override
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java
index 3c44dabd4b..44b61d3bb3 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java
@@ -15,8 +15,6 @@
*/
package org.thingsboard.server.service.notification.channels;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
@@ -26,26 +24,21 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
-import org.thingsboard.server.service.sms.SmsExecutorService;
@Component
@RequiredArgsConstructor
public class SmsNotificationChannel implements NotificationChannel {
private final SmsService smsService;
- private final SmsExecutorService executor;
@Override
- public ListenableFuture sendNotification(User recipient, SmsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) {
+ public void sendNotification(User recipient, SmsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
String phone = recipient.getPhone();
if (StringUtils.isBlank(phone)) {
- return Futures.immediateFailedFuture(new RuntimeException("User does not have phone number"));
+ throw new RuntimeException("User does not have phone number");
}
- return executor.submit(() -> {
- smsService.sendSms(recipient.getTenantId(), recipient.getCustomerId(), new String[]{phone}, processedTemplate.getBody());
- return null;
- });
+ smsService.sendSms(recipient.getTenantId(), recipient.getCustomerId(), new String[]{phone}, processedTemplate.getBody());
}
@Override
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java
index c7465b0ac8..e9656f2ea6 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java
@@ -15,52 +15,44 @@
*/
package org.thingsboard.server.service.notification.rule;
-import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.cache.Cache;
-import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.NotificationCenter;
-import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
-import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger;
-import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger;
+import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.dao.notification.NotificationRequestService;
-import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.queue.discovery.PartitionService;
-import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
+import org.thingsboard.server.queue.notification.NotificationDeduplicationService;
import org.thingsboard.server.service.executors.NotificationExecutorService;
import org.thingsboard.server.service.notification.rule.cache.NotificationRulesCache;
import org.thingsboard.server.service.notification.rule.trigger.NotificationRuleTriggerProcessor;
-import org.thingsboard.server.service.notification.rule.trigger.RuleEngineMsgNotificationRuleTriggerProcessor;
-import jakarta.annotation.PostConstruct;
-import java.io.Serializable;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@@ -72,41 +64,39 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
private final NotificationRulesCache notificationRulesCache;
private final NotificationRequestService notificationRequestService;
+ private final NotificationDeduplicationService deduplicationService;
private final PartitionService partitionService;
private final RateLimitService rateLimitService;
@Autowired @Lazy
private NotificationCenter notificationCenter;
private final NotificationExecutorService notificationExecutor;
- private final CacheManager cacheManager;
- private Cache sentNotifications;
private final Map triggerProcessors = new EnumMap<>(NotificationRuleTriggerType.class);
- @PostConstruct
- private void init() {
- sentNotifications = cacheManager.getCache(CacheConstants.SENT_NOTIFICATIONS_CACHE);
- if (sentNotifications == null) {
- throw new IllegalStateException("Sent notifications cache is not set up");
- }
- }
-
@Override
public void process(NotificationRuleTrigger trigger) {
NotificationRuleTriggerType triggerType = trigger.getType();
- if (triggerType == null) return;
TenantId tenantId = triggerType.isTenantLevel() ? trigger.getTenantId() : TenantId.SYS_TENANT_ID;
try {
- List rules = notificationRulesCache.getEnabled(tenantId, triggerType);
- for (NotificationRule rule : rules) {
- notificationExecutor.submit(() -> {
+ List enabledRules = notificationRulesCache.getEnabled(tenantId, triggerType);
+ if (enabledRules.isEmpty()) {
+ return;
+ }
+ if (trigger.deduplicate()) {
+ enabledRules = new ArrayList<>(enabledRules);
+ enabledRules.removeIf(rule -> deduplicationService.alreadyProcessed(trigger, rule));
+ }
+ final List rules = enabledRules;
+ notificationExecutor.submit(() -> {
+ for (NotificationRule rule : rules) {
try {
processNotificationRule(rule, trigger);
} catch (Throwable e) {
log.error("Failed to process notification rule {} for trigger type {} with trigger object {}", rule.getId(), rule.getTriggerType(), trigger, e);
}
- });
- }
+ }
+ });
} catch (Throwable e) {
log.error("Failed to process notification rules for trigger: {}", trigger, e);
}
@@ -142,9 +132,6 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
log.debug("[{}] Rate limit for notification requests per rule was exceeded (rule '{}')", rule.getTenantId(), rule.getName());
return;
}
- if (trigger.getType().isDeduplicate() && alreadySent(rule.getId(), trigger)) {
- return;
- }
NotificationInfo notificationInfo = constructNotificationInfo(trigger, triggerConfig);
rule.getRecipientsConfig().getTargetsTable().forEach((delay, targets) -> {
@@ -172,14 +159,13 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
.ruleId(rule.getId())
.originatorEntityId(originatorEntityId)
.build();
- notificationExecutor.submit(() -> {
- try {
- log.debug("Submitting notification request for rule '{}' with delay of {} sec to targets {}", rule.getName(), delayInSec, targets);
- notificationCenter.processNotificationRequest(rule.getTenantId(), notificationRequest, null);
- } catch (Exception e) {
- log.error("Failed to process notification request for tenant {} for rule {}", rule.getTenantId(), rule.getId(), e);
- }
- });
+
+ try {
+ log.debug("Submitting notification request for rule '{}' with delay of {} sec to targets {}", rule.getName(), delayInSec, targets);
+ notificationCenter.processNotificationRequest(rule.getTenantId(), notificationRequest, null);
+ } catch (Exception e) {
+ log.error("Failed to process notification request for tenant {} for rule {}", rule.getTenantId(), rule.getId(), e);
+ }
}
private boolean matchesFilter(NotificationRuleTrigger trigger, NotificationRuleTriggerConfig triggerConfig) {
@@ -194,23 +180,6 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
return triggerProcessors.get(triggerConfig.getTriggerType()).constructNotificationInfo(trigger);
}
- private boolean alreadySent(NotificationRuleId ruleId, NotificationRuleTrigger trigger) {
- String key = ruleId + "_" + trigger.getOriginatorEntityId();
- SentNotification sent = sentNotifications.get(key, SentNotification.class);
- boolean alreadySent;
- if (sent != null && sent.getTrigger().equals(trigger)) {
- alreadySent = true;
- log.debug("Notification for {} trigger was already sent, ignoring", trigger.getType());
- // updating cache anyway so that the value is not removed by ttl
- } else {
- alreadySent = false;
- sent = new SentNotification(trigger);
- }
- log.trace("[{}] Putting to sentNotifications cache: {}", ruleId, trigger);
- sentNotifications.put(key, sent);
- return alreadySent;
- }
-
@EventListener(ComponentLifecycleMsg.class)
public void onNotificationRuleDeleted(ComponentLifecycleMsg componentLifecycleMsg) {
if (componentLifecycleMsg.getEvent() != ComponentLifecycleEvent.DELETED ||
@@ -232,24 +201,9 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
@Autowired
public void setTriggerProcessors(Collection processors) {
- Map ruleEngineMsgTypeToTriggerType = new HashMap<>();
processors.forEach(processor -> {
triggerProcessors.put(processor.getTriggerType(), processor);
- if (processor instanceof RuleEngineMsgNotificationRuleTriggerProcessor) {
- Set supportedMsgTypes = ((RuleEngineMsgNotificationRuleTriggerProcessor>) processor).getSupportedMsgTypes();
- supportedMsgTypes.forEach(supportedMsgType -> {
- ruleEngineMsgTypeToTriggerType.put(supportedMsgType, processor.getTriggerType());
- });
- }
});
- RuleEngineMsgTrigger.msgTypeToTriggerType = ruleEngineMsgTypeToTriggerType;
- }
-
- @Data
- private static class SentNotification implements Serializable {
- private static final long serialVersionUID = 38973480405095422L;
-
- private final NotificationRuleTrigger trigger;
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/DefaultNotificationRulesCache.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/DefaultNotificationRulesCache.java
index bac51d3585..28563834f0 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/DefaultNotificationRulesCache.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/DefaultNotificationRulesCache.java
@@ -17,7 +17,6 @@ package org.thingsboard.server.service.notification.rule.cache;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
-import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@@ -25,7 +24,7 @@ import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.notification.NotificationRuleService;
@@ -50,7 +49,7 @@ public class DefaultNotificationRulesCache implements NotificationRulesCache {
private int cacheMaxSize;
@Value("${cache.notificationRules.timeToLiveInMinutes:30}")
private int cacheValueTtl;
- private Cache> cache;
+ private Cache> cache;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
@@ -96,21 +95,15 @@ public class DefaultNotificationRulesCache implements NotificationRulesCache {
}
}
- private void evict(TenantId tenantId) {
+ public void evict(TenantId tenantId) {
cache.invalidateAll(Arrays.stream(NotificationRuleTriggerType.values())
.map(triggerType -> key(tenantId, triggerType))
.collect(Collectors.toList()));
log.trace("Evicted all notification rules for tenant {} from cache", tenantId);
}
- private static CacheKey key(TenantId tenantId, NotificationRuleTriggerType triggerType) {
- return new CacheKey(tenantId, triggerType);
- }
-
- @Data
- private static class CacheKey {
- private final TenantId tenantId;
- private final NotificationRuleTriggerType triggerType;
+ private static String key(TenantId tenantId, NotificationRuleTriggerType triggerType) {
+ return tenantId + "_" + triggerType;
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/NotificationRulesCache.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/NotificationRulesCache.java
index 2a1054b352..bcfa7d0120 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/NotificationRulesCache.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/cache/NotificationRulesCache.java
@@ -17,7 +17,7 @@ package org.thingsboard.server.service.notification.rule.cache;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.util.List;
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java
index ed72f6be58..dba5fd8de4 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java
@@ -16,52 +16,48 @@
package org.thingsboard.server.service.notification.rule.trigger;
import org.springframework.stereotype.Service;
-import org.thingsboard.common.util.JacksonUtil;
-import org.thingsboard.server.common.data.DataConstants;
-import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmAssignee;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
+import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.notification.info.AlarmAssignmentNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
-import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentNotificationRuleTriggerConfig;
-import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentNotificationRuleTriggerConfig.Action;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
-import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger;
-
-import java.util.Set;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig.Action;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentTrigger;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service
-public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor {
+public class AlarmAssignmentTriggerProcessor implements NotificationRuleTriggerProcessor {
@Override
- public boolean matchesFilter(RuleEngineMsgTrigger trigger, AlarmAssignmentNotificationRuleTriggerConfig triggerConfig) {
- Action action = trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGNED) ? Action.ASSIGNED : Action.UNASSIGNED;
+ public boolean matchesFilter(AlarmAssignmentTrigger trigger, AlarmAssignmentNotificationRuleTriggerConfig triggerConfig) {
+ Action action = trigger.getActionType() == ActionType.ALARM_ASSIGNED ? Action.ASSIGNED : Action.UNASSIGNED;
if (!triggerConfig.getNotifyOn().contains(action)) {
return false;
}
- Alarm alarm = JacksonUtil.fromString(trigger.getMsg().getData(), Alarm.class);
- return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType()) &&
- emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity()) &&
- (isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm));
+ AlarmInfo alarmInfo = trigger.getAlarmInfo();
+ return emptyOrContains(triggerConfig.getAlarmTypes(), alarmInfo.getType()) &&
+ emptyOrContains(triggerConfig.getAlarmSeverities(), alarmInfo.getSeverity()) &&
+ (isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarmInfo));
}
@Override
- public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger) {
- AlarmInfo alarmInfo = JacksonUtil.fromString(trigger.getMsg().getData(), AlarmInfo.class);
+ public RuleOriginatedNotificationInfo constructNotificationInfo(AlarmAssignmentTrigger trigger) {
+ AlarmInfo alarmInfo = trigger.getAlarmInfo();
AlarmAssignee assignee = alarmInfo.getAssignee();
return AlarmAssignmentNotificationInfo.builder()
- .action(trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGNED) ? "assigned" : "unassigned")
+ .action(trigger.getActionType() == ActionType.ALARM_ASSIGNED ? "assigned" : "unassigned")
.assigneeFirstName(assignee != null ? assignee.getFirstName() : null)
.assigneeLastName(assignee != null ? assignee.getLastName() : null)
.assigneeEmail(assignee != null ? assignee.getEmail() : null)
.assigneeId(assignee != null ? assignee.getId() : null)
- .userEmail(trigger.getMsg().getMetaData().getValue("userEmail"))
- .userFirstName(trigger.getMsg().getMetaData().getValue("userFirstName"))
- .userLastName(trigger.getMsg().getMetaData().getValue("userLastName"))
+ .userEmail(trigger.getUser().getEmail())
+ .userFirstName(trigger.getUser().getFirstName())
+ .userLastName(trigger.getUser().getLastName())
.alarmId(alarmInfo.getUuidId())
.alarmType(alarmInfo.getType())
.alarmOriginator(alarmInfo.getOriginator())
@@ -77,9 +73,4 @@ public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificatio
return NotificationRuleTriggerType.ALARM_ASSIGNMENT;
}
- @Override
- public Set getSupportedMsgTypes() {
- return Set.of(DataConstants.ALARM_ASSIGNED, DataConstants.ALARM_UNASSIGNED);
- }
-
}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java
index cf71718f83..ef742922b3 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java
@@ -15,68 +15,67 @@
*/
package org.thingsboard.server.service.notification.rule.trigger;
+import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
-import org.thingsboard.common.util.JacksonUtil;
-import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.alarm.Alarm;
-import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
+import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.notification.info.AlarmCommentNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
-import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentNotificationRuleTriggerConfig;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
-import org.thingsboard.server.common.msg.TbMsg;
-import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger;
-
-import java.util.Set;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentTrigger;
+import org.thingsboard.server.dao.entity.EntityService;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service
-public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor {
+@RequiredArgsConstructor
+public class AlarmCommentTriggerProcessor implements NotificationRuleTriggerProcessor {
+
+ private final EntityService entityService;
@Override
- public boolean matchesFilter(RuleEngineMsgTrigger trigger, AlarmCommentNotificationRuleTriggerConfig triggerConfig) {
- TbMsg msg = trigger.getMsg();
- if (msg.getMetaData().getValue("comment") == null) {
- return false;
- }
- if (msg.getType().equals(DataConstants.COMMENT_UPDATED) && !triggerConfig.isNotifyOnCommentUpdate()) {
+ public boolean matchesFilter(AlarmCommentTrigger trigger, AlarmCommentNotificationRuleTriggerConfig triggerConfig) {
+ if (trigger.getActionType() == ActionType.UPDATED_COMMENT && !triggerConfig.isNotifyOnCommentUpdate()) {
return false;
}
if (triggerConfig.isOnlyUserComments()) {
- AlarmComment comment = JacksonUtil.fromString(msg.getMetaData().getValue("comment"), AlarmComment.class);
- if (comment.getType() == AlarmCommentType.SYSTEM) {
+ if (trigger.getComment().getType() == AlarmCommentType.SYSTEM) {
return false;
}
}
- Alarm alarm = JacksonUtil.fromString(msg.getData(), Alarm.class);
+ Alarm alarm = trigger.getAlarm();
return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType()) &&
emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity()) &&
(isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm));
}
@Override
- public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger) {
- TbMsg msg = trigger.getMsg();
- AlarmComment comment = JacksonUtil.fromString(msg.getMetaData().getValue("comment"), AlarmComment.class);
- AlarmInfo alarmInfo = JacksonUtil.fromString(msg.getData(), AlarmInfo.class);
+ public RuleOriginatedNotificationInfo constructNotificationInfo(AlarmCommentTrigger trigger) {
+ Alarm alarm = trigger.getAlarm();
+ String originatorName;
+ if (alarm instanceof AlarmInfo) {
+ originatorName = ((AlarmInfo) alarm).getOriginatorName();
+ } else {
+ originatorName = entityService.fetchEntityName(trigger.getTenantId(), alarm.getOriginator()).orElse("");
+ }
return AlarmCommentNotificationInfo.builder()
- .comment(comment.getComment().get("text").asText())
- .action(msg.getType().equals(DataConstants.COMMENT_CREATED) ? "added" : "updated")
- .userEmail(msg.getMetaData().getValue("userEmail"))
- .userFirstName(msg.getMetaData().getValue("userFirstName"))
- .userLastName(msg.getMetaData().getValue("userLastName"))
- .alarmId(alarmInfo.getUuidId())
- .alarmType(alarmInfo.getType())
- .alarmOriginator(alarmInfo.getOriginator())
- .alarmOriginatorName(alarmInfo.getOriginatorName())
- .alarmSeverity(alarmInfo.getSeverity())
- .alarmStatus(alarmInfo.getStatus())
- .alarmCustomerId(alarmInfo.getCustomerId())
+ .comment(trigger.getComment().getComment().get("text").asText())
+ .action(trigger.getActionType() == ActionType.ADDED_COMMENT ? "added" : "updated")
+ .userEmail(trigger.getUser().getEmail())
+ .userFirstName(trigger.getUser().getFirstName())
+ .userLastName(trigger.getUser().getLastName())
+ .alarmId(alarm.getUuidId())
+ .alarmType(alarm.getType())
+ .alarmOriginator(alarm.getOriginator())
+ .alarmOriginatorName(originatorName)
+ .alarmSeverity(alarm.getSeverity())
+ .alarmStatus(alarm.getStatus())
+ .alarmCustomerId(alarm.getCustomerId())
.build();
}
@@ -85,9 +84,4 @@ public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRu
return NotificationRuleTriggerType.ALARM_COMMENT;
}
- @Override
- public Set getSupportedMsgTypes() {
- return Set.of(DataConstants.COMMENT_CREATED, DataConstants.COMMENT_UPDATED);
- }
-
}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java
index c32ed26854..a942dce736 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java
@@ -22,11 +22,11 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
-import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig;
-import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction;
-import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.ClearRule;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
-import org.thingsboard.server.common.msg.notification.trigger.AlarmTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig.AlarmAction;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig.ClearRule;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.AlarmTrigger;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@@ -108,6 +108,8 @@ public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor {
+public class DeviceActivityTriggerProcessor implements NotificationRuleTriggerProcessor {
private final TbDeviceProfileCache deviceProfileCache;
@Override
- public boolean matchesFilter(RuleEngineMsgTrigger trigger, DeviceActivityNotificationRuleTriggerConfig triggerConfig) {
- if (trigger.getMsg().getOriginator().getEntityType() != EntityType.DEVICE) {
- return false;
- }
- DeviceEvent event = trigger.getMsg().getType().equals(DataConstants.ACTIVITY_EVENT) ? DeviceEvent.ACTIVE : DeviceEvent.INACTIVE;
+ public boolean matchesFilter(DeviceActivityTrigger trigger, DeviceActivityNotificationRuleTriggerConfig triggerConfig) {
+ DeviceEvent event = trigger.isActive() ? DeviceEvent.ACTIVE : DeviceEvent.INACTIVE;
if (!triggerConfig.getNotifyOn().contains(event)) {
return false;
}
- DeviceId deviceId = (DeviceId) trigger.getMsg().getOriginator();
+ DeviceId deviceId = trigger.getDeviceId();
if (CollectionUtils.isNotEmpty(triggerConfig.getDevices())) {
return triggerConfig.getDevices().contains(deviceId.getId());
} else if (CollectionUtils.isNotEmpty(triggerConfig.getDeviceProfiles())) {
@@ -61,15 +53,14 @@ public class DeviceActivityTriggerProcessor implements RuleEngineMsgNotification
}
@Override
- public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger) {
- TbMsg msg = trigger.getMsg();
+ public RuleOriginatedNotificationInfo constructNotificationInfo(DeviceActivityTrigger trigger) {
return DeviceActivityNotificationInfo.builder()
- .eventType(trigger.getMsg().getType().equals(DataConstants.ACTIVITY_EVENT) ? "active" : "inactive")
- .deviceId(msg.getOriginator().getId())
- .deviceName(msg.getMetaData().getValue("deviceName"))
- .deviceType(msg.getMetaData().getValue("deviceType"))
- .deviceLabel(msg.getMetaData().getValue("deviceLabel"))
- .deviceCustomerId(msg.getCustomerId())
+ .eventType(trigger.isActive() ? "active" : "inactive")
+ .deviceId(trigger.getDeviceId().getId())
+ .deviceName(trigger.getDeviceName())
+ .deviceType(trigger.getDeviceType())
+ .deviceLabel(trigger.getDeviceLabel())
+ .deviceCustomerId(trigger.getCustomerId())
.build();
}
@@ -78,9 +69,4 @@ public class DeviceActivityTriggerProcessor implements RuleEngineMsgNotification
return NotificationRuleTriggerType.DEVICE_ACTIVITY;
}
- @Override
- public Set getSupportedMsgTypes() {
- return Set.of(DataConstants.ACTIVITY_EVENT, DataConstants.INACTIVITY_EVENT);
- }
-
}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java
index 95ace87653..9539b42a43 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java
@@ -19,10 +19,10 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.notification.info.EntitiesLimitNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
-import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.EntitiesLimitNotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
-import org.thingsboard.server.common.msg.notification.trigger.EntitiesLimitTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitTrigger;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService;
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java
index ea4902fb05..bc290fa978 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java
@@ -20,9 +20,9 @@ import org.thingsboard.server.common.data.HasCustomerId;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
-import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
-import org.thingsboard.server.common.msg.notification.trigger.EntityActionTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.EntityActionNotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionTrigger;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NewPlatformVersionTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NewPlatformVersionTriggerProcessor.java
index 639378ad72..aeec1a9b39 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NewPlatformVersionTriggerProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NewPlatformVersionTriggerProcessor.java
@@ -20,9 +20,9 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.notification.info.NewPlatformVersionNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
-import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionNotificationRuleTriggerConfig;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
-import org.thingsboard.server.common.msg.notification.trigger.NewPlatformVersionTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NewPlatformVersionNotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionTrigger;
@Service
@RequiredArgsConstructor
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NotificationRuleTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NotificationRuleTriggerProcessor.java
index fe7fb82ee5..67a8d3d701 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NotificationRuleTriggerProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NotificationRuleTriggerProcessor.java
@@ -16,9 +16,9 @@
package org.thingsboard.server.service.notification.rule.trigger;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
-import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
public interface NotificationRuleTriggerProcessor {
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RateLimitsTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RateLimitsTriggerProcessor.java
new file mode 100644
index 0000000000..bec1502427
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RateLimitsTriggerProcessor.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.notification.rule.trigger;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.notification.info.RateLimitsNotificationInfo;
+import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.RateLimitsNotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.util.CollectionsUtil;
+import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
+import org.thingsboard.server.dao.entity.EntityService;
+import org.thingsboard.server.dao.tenant.TenantService;
+
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class RateLimitsTriggerProcessor implements NotificationRuleTriggerProcessor {
+
+ private final TenantService tenantService;
+ private final EntityService entityService;
+
+ @Override
+ public boolean matchesFilter(RateLimitsTrigger trigger, RateLimitsNotificationRuleTriggerConfig triggerConfig) {
+ return trigger.getLimitLevel() != null && trigger.getApi().getLabel() != null &&
+ CollectionsUtil.emptyOrContains(triggerConfig.getApis(), trigger.getApi());
+ }
+
+ @Override
+ public RuleOriginatedNotificationInfo constructNotificationInfo(RateLimitsTrigger trigger) {
+ EntityId limitLevel = trigger.getLimitLevel();
+ String tenantName = tenantService.findTenantById(trigger.getTenantId()).getName();
+ String limitLevelEntityName = null;
+ if (limitLevel instanceof TenantId) {
+ limitLevelEntityName = tenantName;
+ } else if (limitLevel != null) {
+ limitLevelEntityName = Optional.ofNullable(trigger.getLimitLevelEntityName())
+ .orElseGet(() -> entityService.fetchEntityName(trigger.getTenantId(), limitLevel).orElse(null));
+ }
+ return RateLimitsNotificationInfo.builder()
+ .tenantId(trigger.getTenantId())
+ .tenantName(tenantName)
+ .api(trigger.getApi())
+ .limitLevel(limitLevel)
+ .limitLevelEntityName(limitLevelEntityName)
+ .build();
+ }
+
+ @Override
+ public NotificationRuleTriggerType getTriggerType() {
+ return NotificationRuleTriggerType.RATE_LIMITS;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java
index 6e37db83ef..8aac7ffd66 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java
@@ -23,10 +23,10 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.notification.info.RuleEngineComponentLifecycleEventNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
-import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
-import org.thingsboard.server.common.msg.notification.trigger.RuleEngineComponentLifecycleEventTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventTrigger;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.queue.discovery.PartitionService;
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 6203c68f15..4abce4fb0a 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
@@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.rpc.RpcError;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.TbActorMsg;
-import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
@@ -64,7 +64,7 @@ import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
-import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
+import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
index c47057b8dd..6ea73a39dd 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
@@ -258,7 +258,21 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
}
void launchConsumer(TbQueueConsumer> consumer, Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) {
- consumersExecutor.execute(() -> consumerLoop(consumer, configuration, stats, threadSuffix));
+ if (isReady) {
+ consumersExecutor.execute(() -> consumerLoop(consumer, configuration, stats, threadSuffix));
+ } else {
+ scheduleLaunchConsumer(consumer, configuration, stats, threadSuffix);
+ }
+ }
+
+ private void scheduleLaunchConsumer(TbQueueConsumer> consumer, Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) {
+ repartitionExecutor.schedule(() -> {
+ if (isReady) {
+ consumersExecutor.execute(() -> consumerLoop(consumer, configuration, stats, threadSuffix));
+ } else {
+ scheduleLaunchConsumer(consumer, configuration, stats, threadSuffix);
+ }
+ }, 10, TimeUnit.SECONDS);
}
void consumerLoop(TbQueueConsumer> consumer, org.thingsboard.server.common.data.queue.Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) {
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
index 961d080aad..727cd2d703 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
@@ -68,7 +68,7 @@ public abstract class AbstractConsumerService findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) {
- return resourceService.findAllTenantResourcesByTenantId(tenantId, pageLink);
+ public PageData findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) {
+ return resourceService.findAllTenantResourcesByTenantId(filter, pageLink);
}
@Override
- public PageData findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) {
- return resourceService.findTenantResourcesByTenantId(tenantId, pageLink);
+ public PageData findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) {
+ return resourceService.findTenantResourcesByTenantId(filter, pageLink);
}
@Override
@@ -165,6 +169,8 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
} else {
resource.setResourceKey(resource.getFileName());
}
+ HashCode hashCode = Hashing.sha256().hashBytes(Base64.getDecoder().decode(resource.getData().getBytes()));
+ resource.setEtag(hashCode.toString());
return resourceService.saveResource(resource);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java
index 024d391482..40fe7dda9a 100644
--- a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java
+++ b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.service.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.TbResourceInfoFilter;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
@@ -35,9 +36,9 @@ public interface TbResourceService extends SimpleTbEntityService {
TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId);
- PageData findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink);
+ PageData findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink);
- PageData findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink);
+ PageData findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink);
List findLwM2mObject(TenantId tenantId,
String sortOrder,
diff --git a/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java b/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java
index c2e425e14a..5756101ee1 100644
--- a/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java
+++ b/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java
@@ -15,10 +15,13 @@
*/
package org.thingsboard.server.service.rule;
+import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration;
import org.thingsboard.rule.engine.flow.TbRuleChainOutputNode;
@@ -42,12 +45,13 @@ import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleChainUpdateResult;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.rule.RuleNodeUpdateResult;
+import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.install.InstallScripts;
-import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import java.util.ArrayList;
import java.util.Collections;
@@ -70,8 +74,7 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
private final RuleChainService ruleChainService;
private final RelationService relationService;
private final InstallScripts installScripts;
-
- private final EntitiesVersionControlService vcService;
+ private final ComponentDiscoveryService componentDiscoveryService;
@Override
public Set getRuleChainOutputLabels(TenantId tenantId, RuleChainId ruleChainId) {
@@ -277,7 +280,7 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
RuleChainId ruleChainId = ruleChain.getId();
RuleChainId ruleChainMetaDataId = ruleChainMetaData.getRuleChainId();
try {
- RuleChainUpdateResult result = ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData);
+ RuleChainUpdateResult result = ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData, this::updateRuleNodeConfiguration);
checkNotNull(result.isSuccess() ? true : null);
List updatedRuleChains;
@@ -404,6 +407,45 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
}
}
+ @Override
+ public RuleNode updateRuleNodeConfiguration(RuleNode node) {
+ var ruleChainId = node.getRuleChainId();
+ var ruleNodeId = node.getId();
+ var ruleNodeType = node.getType();
+ try {
+ var ruleNodeClass = componentDiscoveryService.getRuleNodeInfo(ruleNodeType)
+ .orElseThrow(() -> new RuntimeException("Rule node " + ruleNodeType + " is not supported!"));
+ if (ruleNodeClass.isVersioned()) {
+ TbVersionedNode tbVersionedNode = (TbVersionedNode) ruleNodeClass.getClazz().getDeclaredConstructor().newInstance();
+ int fromVersion = node.getConfigurationVersion();
+ int toVersion = ruleNodeClass.getCurrentVersion();
+ if (fromVersion < toVersion) {
+ log.debug("Going to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",
+ ruleNodeId, ruleNodeType, fromVersion, toVersion);
+ try {
+ TbPair upgradeResult = tbVersionedNode.upgrade(fromVersion, node.getConfiguration());
+ if (upgradeResult.getFirst()) {
+ node.setConfiguration(upgradeResult.getSecond());
+ }
+ node.setConfigurationVersion(toVersion);
+ log.debug("Successfully upgrade rule node with id: {} type: {}, rule chain id: {} fromVersion: {} toVersion: {}",
+ ruleNodeId, ruleNodeType, ruleChainId, fromVersion, toVersion);
+ } catch (TbNodeException e) {
+ log.warn("Failed to upgrade rule node with id: {} type: {} rule chain id: {} fromVersion: {} toVersion: {} due to: ",
+ ruleNodeId, ruleNodeType, ruleChainId, fromVersion, toVersion, e);
+ }
+ } else {
+ log.debug("Rule node with id: {} type: {} ruleChainId: {} already set to latest version!",
+ ruleNodeId, ruleChainId, ruleNodeType);
+ }
+ }
+ } catch (Exception e) {
+ log.error("Failed to update the rule node with id: {} type: {}, rule chain id: {}",
+ ruleNodeId, ruleNodeType, ruleChainId, e);
+ }
+ return node;
+ }
+
private Set updateRelatedRuleChains(TenantId tenantId, RuleChainId ruleChainId, Map labelsMap) {
Set updatedRuleChains = new HashSet<>();
List usageList = getOutputLabelUsage(tenantId, ruleChainId);
diff --git a/application/src/main/java/org/thingsboard/server/service/rule/TbRuleChainService.java b/application/src/main/java/org/thingsboard/server/service/rule/TbRuleChainService.java
index 02b79544f4..17f1338bba 100644
--- a/application/src/main/java/org/thingsboard/server/service/rule/TbRuleChainService.java
+++ b/application/src/main/java/org/thingsboard/server/service/rule/TbRuleChainService.java
@@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainOutputLabelsUsage;
import org.thingsboard.server.common.data.rule.RuleChainUpdateResult;
+import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
import java.util.List;
@@ -54,4 +55,6 @@ public interface TbRuleChainService extends SimpleTbEntityService {
RuleChain setAutoAssignToEdgeRuleChain(TenantId tenantId, RuleChain ruleChain, User user) throws ThingsboardException;
RuleChain unsetAutoAssignToEdgeRuleChain(TenantId tenantId, RuleChain ruleChain, User user) throws ThingsboardException;
+
+ RuleNode updateRuleNodeConfiguration(RuleNode ruleNode);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
index a58c801583..08717986bf 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
@@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccoun
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.dao.user.UserService;
-import org.thingsboard.server.dao.util.limits.LimitedApi;
+import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
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 1a8f87713e..bcd1c287c4 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
@@ -19,9 +19,13 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.HasCustomerId;
import org.thingsboard.server.common.data.HasTenantId;
+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.User;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.service.security.model.SecurityUser;
@@ -44,6 +48,7 @@ public class CustomerUserPermissions extends AbstractPermissions {
put(Resource.RPC, rpcPermissionChecker);
put(Resource.DEVICE_PROFILE, profilePermissionChecker);
put(Resource.ASSET_PROFILE, profilePermissionChecker);
+ put(Resource.TB_RESOURCE, customerResourcePermissionChecker);
}
private static final PermissionChecker customerAlarmPermissionChecker = new PermissionChecker() {
@@ -95,6 +100,26 @@ public class CustomerUserPermissions extends AbstractPermissions {
};
+ private static final PermissionChecker customerResourcePermissionChecker =
+ new PermissionChecker() {
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean hasPermission(SecurityUser user, Operation operation, TbResourceId resourceId, TbResourceInfo resource) {
+ if (operation != Operation.READ) {
+ return false;
+ }
+ if (resource.getResourceType() == null || !resource.getResourceType().isCustomerAccess()) {
+ return false;
+ }
+ if (resource.getTenantId() == null || resource.getTenantId().isNullUid()) {
+ return true;
+ }
+ return user.getTenantId().equals(resource.getTenantId());
+ }
+
+ };
+
private static final PermissionChecker customerDashboardPermissionChecker =
new PermissionChecker.GenericPermissionChecker(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY) {
diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
index 402be010ac..3d0499e3f4 100644
--- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
@@ -62,6 +62,8 @@ import org.thingsboard.server.common.data.query.EntityListFilter;
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.notification.NotificationRuleProcessor;
+import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityTrigger;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
@@ -155,6 +157,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService HINTS = new LinkedHashSet<>(Arrays.asList(EntityType.RULE_CHAIN, EntityType.DEVICE, EntityType.ASSET));
+ private final TbRuleChainService tbRuleChainService;
private final RuleChainService ruleChainService;
private final RuleNodeDao ruleNodeDao;
@@ -106,7 +108,7 @@ public class RuleChainImportService extends BaseEntityImportService
+
+
+
diff --git a/application/src/main/resources/templates/mail_config_templates.json b/application/src/main/resources/templates/mail_config_templates.json
new file mode 100644
index 0000000000..f287880a5e
--- /dev/null
+++ b/application/src/main/resources/templates/mail_config_templates.json
@@ -0,0 +1,52 @@
+[
+ {
+ "providerId": "SENDGRID",
+ "smtpProtocol": "SMTPS",
+ "smtpHost": "smtp.sendgrid.net",
+ "smtpPort": 465,
+ "timeout": 10000,
+ "enableTls": true,
+ "tlsVersion": "TLSv1.2",
+ "authorizationUri": null,
+ "accessTokenUri": null,
+ "scope": [
+ ""
+ ],
+ "helpLink": null,
+ "name": "SendGrid"
+ },
+ {
+ "providerId": "GOOGLE",
+ "smtpProtocol": "SMTPS",
+ "smtpHost": "smtp.gmail.com",
+ "smtpPort": 465,
+ "timeout": 10000,
+ "enableTls": true,
+ "tlsVersion": "TLSv1.2",
+ "authorizationUri": "https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline",
+ "accessTokenUri": "https://oauth2.googleapis.com/token",
+ "scope": [
+ "https://mail.google.com/"
+ ],
+ "helpLink": "https://support.google.com/googleapi/answer/6158849",
+ "name": "Google"
+ },
+ {
+ "providerId": "OFFICE_365",
+ "smtpProtocol": "SMTP",
+ "smtpHost": "smtp.office365.com",
+ "smtpPort": 587,
+ "timeout": 10000,
+ "enableTls": true,
+ "tlsVersion": "TLSv1.2",
+ "authorizationUri": "https://login.microsoftonline.com/%s/oauth2/v2.0/authorize",
+ "accessTokenUri": "https://login.microsoftonline.com/%s/oauth2/v2.0/token",
+ "scope": [
+ "https://outlook.office365.com/SMTP.Send",
+ "offline_access",
+ "openid"
+ ],
+ "helpLink": "https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth",
+ "name": "Office 365"
+ }
+]
\ No newline at end of file
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 7473121866..5522e8a5bf 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -135,6 +135,10 @@ security:
path: "${SECURITY_JAVA_CACERTS_PATH:${java.home}/lib/security/cacerts}"
password: "${SECURITY_JAVA_CACERTS_PASSWORD:changeit}"
+mail:
+ oauth2:
+ refreshTokenCheckingInterval: "${REFRESH_TOKEN_EXPIRATION_CHECKING_INTERVAL:86400}" # Number of seconds (1 day).
+
# Usage statistics parameters
usage:
stats:
@@ -389,6 +393,10 @@ actors:
queue_size: "${ACTORS_RULE_TRANSACTION_QUEUE_SIZE:15000}"
# Time in milliseconds for transaction to complete
duration: "${ACTORS_RULE_TRANSACTION_DURATION:60000}"
+ external:
+ # Force acknowledgement of the incoming message for external rule nodes to decrease processing latency.
+ # Enqueue the result of external node processing as a separate message to the rule engine.
+ force_ack: "${ACTORS_RULE_EXTERNAL_NODE_FORCE_ACK:false}"
rpc:
max_retries: "${ACTORS_RPC_MAX_RETRIES:5}"
sequential: "${ACTORS_RPC_SEQUENTIAL:false}"
@@ -502,7 +510,7 @@ cache:
spring.data.redis.repositories.enabled: false
redis:
- # standalone or cluster
+ # standalone or cluster or sentinel
connection:
type: "${REDIS_CONNECTION_TYPE:standalone}"
standalone:
@@ -522,6 +530,16 @@ redis:
nodes: "${REDIS_NODES:}"
# Maximum number of redirects to follow when executing commands across the cluster.
max-redirects: "${REDIS_MAX_REDIRECTS:12}"
+ # if set false will be used pool config build from values of the pool config section
+ useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
+ sentinel:
+ # name of master node
+ master: "${REDIS_MASTER:}"
+ # comma-separated list of "host:port" pairs of sentinels
+ sentinels: "${REDIS_SENTINELS:}"
+ # password to authenticate with sentinel
+ password: "${REDIS_SENTINEL_PASSWORD:}"
+ # if set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
# db index
db: "${REDIS_DB:0}"
@@ -1263,6 +1281,9 @@ vc:
notification_system:
thread_pool_size: "${TB_NOTIFICATION_SYSTEM_THREAD_POOL_SIZE:10}"
+ rules:
+ # Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'
+ deduplication_durations: "${TB_NOTIFICATION_RULES_DEDUPLICATION_DURATIONS:NEW_PLATFORM_VERSION:0;RATE_LIMITS:14400000;}"
management:
endpoints:
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java
index 6c42c7604d..d5eabdff2a 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java
@@ -264,7 +264,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
int cntTime = 1;
testNotificationMsgToEdgeServiceTime(entityId, tenantId, actionType, cntTime);
testLogEntityAction(entity, originatorId, tenantId, customerId, userId, userName, actionType, cntTime, additionalInfo);
- tesPushMsgToCoreTime(cntTime);
+ testPushMsgToCoreTime(cntTime);
Mockito.reset(tbClusterService, auditLogService);
}
@@ -363,13 +363,13 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
Mockito.any(entityId.getClass()), Mockito.any(ComponentLifecycleEvent.class));
}
- private void tesPushMsgToCoreTime(int cntTime) {
+ private void testPushMsgToCoreTime(int cntTime) {
Mockito.verify(tbClusterService, times(cntTime)).pushMsgToCore(Mockito.any(ToDeviceActorNotificationMsg.class), Mockito.isNull());
}
protected void testLogEntityAction(HasName entity, EntityId originatorId, TenantId tenantId,
- CustomerId customerId, UserId userId, String userName,
- ActionType actionType, int cntTime, Object... additionalInfo) {
+ CustomerId customerId, UserId userId, String userName,
+ ActionType actionType, int cntTime, Object... additionalInfo) {
ArgumentMatcher matcherEntityEquals = entity == null ? Objects::isNull : argument -> argument.toString().equals(entity.toString());
ArgumentMatcher matcherOriginatorId = argument -> argument.equals(originatorId);
ArgumentMatcher matcherCustomerId = customerId == null ?
@@ -380,10 +380,10 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
actionType, cntTime, extractMatcherAdditionalInfo(additionalInfo));
}
- private void testLogEntityActionEntityEqClass(HasName entity, EntityId originatorId, TenantId tenantId,
- CustomerId customerId, UserId userId, String userName,
- ActionType actionType, int cntTime, Object... additionalInfo) {
- ArgumentMatcher matcherEntityEquals = argument -> argument.getClass().equals(entity.getClass());
+ protected void testLogEntityActionEntityEqClass(HasName entity, EntityId originatorId, TenantId tenantId,
+ CustomerId customerId, UserId userId, String userName,
+ ActionType actionType, int cntTime, Object... additionalInfo) {
+ ArgumentMatcher matcherEntityEquals = argument -> entity.getClass().isAssignableFrom(argument.getClass());
ArgumentMatcher matcherOriginatorId = argument -> argument.equals(originatorId);
ArgumentMatcher matcherCustomerId = customerId == null ?
argument -> argument.getClass().equals(CustomerId.class) : argument -> argument.equals(customerId);
@@ -600,8 +600,8 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
return fieldName + " length must be equal or less than 255";
}
- protected String msgErrorNoFound(String entityClassName, String assetIdStr) {
- return entityClassName + " with id [" + assetIdStr + "] is not found";
+ protected String msgErrorNoFound(String entityClassName, String entityIdStr) {
+ return entityClassName + " with id [" + entityIdStr + "] is not found";
}
private String entityClassToEntityTypeName(HasName entity) {
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 0e597e5656..6a871665fe 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
@@ -168,6 +168,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
private static final String CUSTOMER_USER_PASSWORD = "customer";
protected static final String DIFFERENT_CUSTOMER_USER_EMAIL = "testdifferentcustomer@thingsboard.org";
+
+ protected static final String DIFFERENT_TENANT_CUSTOMER_USER_EMAIL = "testdifferenttenantcustomer@thingsboard.org";
private static final String DIFFERENT_CUSTOMER_USER_PASSWORD = "diffcustomer";
/**
@@ -193,9 +195,13 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected CustomerId customerId;
protected TenantId differentTenantId;
protected CustomerId differentCustomerId;
+
+ protected CustomerId differentTenantCustomerId;
protected UserId customerUserId;
protected UserId differentCustomerUserId;
+ protected UserId differentTenantCustomerUserId;
+
@SuppressWarnings("rawtypes")
private HttpMessageConverter mappingJackson2HttpMessageConverter;
@@ -367,7 +373,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected Tenant savedDifferentTenant;
protected User savedDifferentTenantUser;
private Customer savedDifferentCustomer;
+ private Customer savedDifferentTenantCustomer;
protected User differentCustomerUser;
+ protected User differentTenantCustomerUser;
protected void loginDifferentTenant() throws Exception {
if (savedDifferentTenant != null) {
@@ -409,6 +417,24 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
}
}
+ protected void loginDifferentTenantCustomer() throws Exception {
+ if (savedDifferentTenantCustomer != null) {
+ login(savedDifferentTenantCustomer.getEmail(), CUSTOMER_USER_PASSWORD);
+ } else {
+ createDifferentTenantCustomer();
+
+ loginDifferentTenant();
+ differentTenantCustomerUser = new User();
+ differentTenantCustomerUser.setAuthority(Authority.CUSTOMER_USER);
+ differentTenantCustomerUser.setTenantId(savedDifferentTenantCustomer.getTenantId());
+ differentTenantCustomerUser.setCustomerId(savedDifferentTenantCustomer.getId());
+ differentTenantCustomerUser.setEmail(DIFFERENT_TENANT_CUSTOMER_USER_EMAIL);
+
+ differentTenantCustomerUser = createUserAndLogin(differentTenantCustomerUser, DIFFERENT_CUSTOMER_USER_PASSWORD);
+ differentTenantCustomerUserId = differentTenantCustomerUser.getId();
+ }
+ }
+
protected void createDifferentCustomer() throws Exception {
loginTenantAdmin();
@@ -421,6 +447,18 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
resetTokens();
}
+ protected void createDifferentTenantCustomer() throws Exception {
+ loginDifferentTenant();
+
+ Customer customer = new Customer();
+ customer.setTitle("Different tenant customer");
+ savedDifferentTenantCustomer = doPost("/api/customer", customer, Customer.class);
+ Assert.assertNotNull(savedDifferentTenantCustomer);
+ differentTenantCustomerId = savedDifferentTenantCustomer.getId();
+
+ resetTokens();
+ }
+
protected void deleteDifferentTenant() throws Exception {
if (savedDifferentTenant != null) {
loginSysAdmin();
@@ -603,6 +641,13 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return mockMvc.perform(getRequest);
}
+ protected ResultActions doGet(String urlTemplate, HttpHeaders httpHeaders, Object... urlVariables) throws Exception {
+ MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables);
+ getRequest.headers(httpHeaders);
+ setJwtToken(getRequest);
+ return mockMvc.perform(getRequest);
+ }
+
protected T doGet(String urlTemplate, Class responseClass, Object... urlVariables) throws Exception {
return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass);
}
diff --git a/application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java
index 287ae7cfe1..bf3e7528c9 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java
@@ -46,6 +46,7 @@ import java.util.LinkedList;
import java.util.List;
import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
@@ -104,7 +105,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
AlarmComment createdComment = createAlarmComment(alarm.getId());
- testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ADDED_COMMENT, 1, createdComment);
+ testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ADDED_COMMENT, 1, createdComment);
}
@Test
@@ -116,7 +117,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
AlarmComment createdComment = createAlarmComment(alarm.getId());
Assert.assertEquals(AlarmCommentType.OTHER, createdComment.getType());
- testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ADDED_COMMENT, 1, createdComment);
+ testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ADDED_COMMENT, 1, createdComment);
}
@Test
@@ -135,7 +136,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
Assert.assertEquals("true", updatedAlarmComment.getComment().get("edited").asText());
Assert.assertNotNull(updatedAlarmComment.getComment().get("editedOn"));
- testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.UPDATED_COMMENT, 1, savedComment);
+ testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.UPDATED_COMMENT, 1, savedComment);
}
@Test
@@ -154,7 +155,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
Assert.assertEquals("true", updatedAlarmComment.getComment().get("edited").asText());
Assert.assertNotNull(updatedAlarmComment.getComment().get("editedOn"));
- testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.UPDATED_COMMENT, 1, updatedAlarmComment);
+ testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.UPDATED_COMMENT, 1, updatedAlarmComment);
}
@Test
@@ -169,8 +170,8 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
savedComment.setComment(newComment);
doPost("/api/alarm/" + alarm.getId() + "/comment", savedComment)
- .andExpect(status().isForbidden())
- .andExpect(statusReason(containsString(msgErrorPermission)));
+ .andExpect(status().isNotFound())
+ .andExpect(statusReason(equalTo(msgErrorNoFound("Alarm", alarm.getId().toString()))));
testNotifyEntityNever(alarm.getId(), savedComment);
}
@@ -209,7 +210,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
.comment(JacksonUtil.newObjectNode().put("text", String.format("User %s deleted his comment",
CUSTOMER_USER_EMAIL)))
.build();
- testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.DELETED_COMMENT, 1, expectedAlarmComment);
+ testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.DELETED_COMMENT, 1, expectedAlarmComment);
}
@Test
@@ -228,7 +229,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
.comment(JacksonUtil.newObjectNode().put("text", String.format("User %s deleted his comment",
TENANT_ADMIN_EMAIL)))
.build();
- testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.DELETED_COMMENT, 1, expectedAlarmComment);
+ testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.DELETED_COMMENT, 1, expectedAlarmComment);
}
@Test
@@ -355,16 +356,18 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
Assert.assertTrue("Created alarm doesn't match the found one!", equals);
}
- private AlarmComment createAlarmComment(AlarmId alarmId, String text) {
+ private AlarmComment createAlarmComment(AlarmId alarmId, String text) {
AlarmComment alarmComment = AlarmComment.builder()
.comment(JacksonUtil.newObjectNode().set("text", new TextNode(text)))
.build();
return saveAlarmComment(alarmId, alarmComment);
}
- private AlarmComment createAlarmComment(AlarmId alarmId) {
+
+ private AlarmComment createAlarmComment(AlarmId alarmId) {
return createAlarmComment(alarmId, "Please take a look");
}
+
private AlarmComment saveAlarmComment(AlarmId alarmId, AlarmComment alarmComment) {
alarmComment = doPost("/api/alarm/" + alarmId + "/comment", alarmComment, AlarmComment.class);
Assert.assertNotNull(alarmComment);
diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java
index c9eba1dbce..59a305fe5a 100644
--- a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java
@@ -21,6 +21,7 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
@@ -30,6 +31,9 @@ import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.action.TbCreateAlarmNode;
import org.thingsboard.rule.engine.action.TbCreateAlarmNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbVersionedNode;
+import org.thingsboard.rule.engine.metadata.TbGetRelatedAttributeNode;
+import org.thingsboard.rule.engine.metadata.TbGetRelatedDataNodeConfiguration;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
@@ -129,6 +133,78 @@ public class RuleChainControllerTest extends AbstractControllerTest {
ActionType.UPDATED);
}
+ @Test
+ public void testSaveRuleChainMetadataWithVersionedNodes() throws Exception {
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setName("RuleChain");
+
+ RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class);
+ Assert.assertNotNull(savedRuleChain);
+ RuleChainId ruleChainId = savedRuleChain.getId();
+ Assert.assertNotNull(ruleChainId);
+ Assert.assertTrue(savedRuleChain.getCreatedTime() > 0);
+ Assert.assertEquals(ruleChain.getName(), savedRuleChain.getName());
+
+ var annotation = TbGetRelatedAttributeNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class);
+ String ruleNodeType = TbGetRelatedAttributeNode.class.getName();
+ int currentVersion = annotation.version();
+
+ String oldConfig = "{\"attrMapping\":{\"serialNumber\":\"sn\"}," +
+ "\"relationsQuery\":{\"direction\":\"FROM\",\"maxLevel\":1," +
+ "\"filters\":[{\"relationType\":\"Contains\",\"entityTypes\":[]}]," +
+ "\"fetchLastLevelOnly\":false},\"telemetry\":false}";
+
+ TbGetRelatedDataNodeConfiguration defaultConfiguration = new TbGetRelatedDataNodeConfiguration().defaultConfiguration();
+ String newConfig = JacksonUtil.toString(defaultConfiguration);
+
+ var ruleChainMetaData = createRuleChainMetadataWithTbVersionedNodes(
+ ruleChainId,
+ ruleNodeType,
+ currentVersion,
+ oldConfig,
+ newConfig
+ );
+ var savedRuleChainMetaData = doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class);
+
+ Assert.assertEquals(ruleChainId, savedRuleChainMetaData.getRuleChainId());
+ Assert.assertEquals(2, savedRuleChainMetaData.getNodes().size());
+
+ for (RuleNode ruleNode : savedRuleChainMetaData.getNodes()) {
+ Assert.assertNotNull(ruleNode.getId());
+ Assert.assertEquals(currentVersion, ruleNode.getConfigurationVersion());
+ Assert.assertEquals(defaultConfiguration, JacksonUtil.treeToValue(ruleNode.getConfiguration(), defaultConfiguration.getClass()));
+ }
+ }
+
+ private RuleChainMetaData createRuleChainMetadataWithTbVersionedNodes(
+ RuleChainId ruleChainId,
+ String ruleNodeType,
+ int currentVersion,
+ String oldConfig,
+ String newConfig
+ ) {
+ RuleChainMetaData ruleChainMetaData = new RuleChainMetaData();
+ ruleChainMetaData.setRuleChainId(ruleChainId);
+
+ var ruleNodeWithOldConfig = new RuleNode();
+ ruleNodeWithOldConfig.setName("Old Rule Node");
+ ruleNodeWithOldConfig.setType(ruleNodeType);
+ ruleNodeWithOldConfig.setConfiguration(JacksonUtil.toJsonNode(oldConfig));
+
+ var ruleNodeWithNewConfig = new RuleNode();
+ ruleNodeWithNewConfig.setName("New Rule Node");
+ ruleNodeWithNewConfig.setType(ruleNodeType);
+ ruleNodeWithNewConfig.setConfigurationVersion(currentVersion);
+ ruleNodeWithNewConfig.setConfiguration(JacksonUtil.toJsonNode(newConfig));
+
+ List ruleNodes = new ArrayList<>();
+ ruleNodes.add(ruleNodeWithOldConfig);
+ ruleNodes.add(ruleNodeWithNewConfig);
+ ruleChainMetaData.setFirstNodeIndex(0);
+ ruleChainMetaData.setNodes(ruleNodes);
+ return ruleChainMetaData;
+ }
+
@Test
public void testSaveRuleChainWithViolationOfLengthValidation() throws Exception {
diff --git a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
index de8f717c0d..b4735519a5 100644
--- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
@@ -16,11 +16,18 @@
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.mockito.Mockito;
+import org.springframework.http.HttpHeaders;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.test.web.servlet.ResultActions;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource;
@@ -35,6 +42,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
+import java.util.Base64;
import java.util.Collections;
import java.util.List;
@@ -47,6 +55,10 @@ public class TbResourceControllerTest extends AbstractControllerTest {
private IdComparator idComparator = new IdComparator<>();
private static final String DEFAULT_FILE_NAME = "test.jks";
+ private static final String DEFAULT_FILE_NAME_2 = "test2.jks";
+ private static final String JS_TEST_FILE_NAME = "test.js";
+ private static final String TEST_DATA = "77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQpGSUxFIElORk9STUFUSU9OCgpPTUEgUGVybWFuZW50IERvY3VtZW50CiAgIEZpbGU6IE9NQS1TVVAtTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci1WMV8wXzEtMjAxOTAyMjEtQQogICBUeXBlOiB4bWwKClB1YmxpYyBSZWFjaGFibGUgSW5mb3JtYXRpb24KICAgUGF0aDogaHR0cDovL3d3dy5vcGVubW9iaWxlYWxsaWFuY2Uub3JnL3RlY2gvcHJvZmlsZXMKICAgTmFtZTogTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci12MV8wXzEueG1sCgpOT1JNQVRJVkUgSU5GT1JNQVRJT04KCiAgSW5mb3JtYXRpb24gYWJvdXQgdGhpcyBmaWxlIGNhbiBiZSBmb3VuZCBpbiB0aGUgbGF0ZXN0IHJldmlzaW9uIG9mCgogIE9NQS1UUy1MV00yTV9CaW5hcnlBcHBEYXRhQ29udGFpbmVyLVYxXzBfMQoKICBUaGlzIGlzIGF2YWlsYWJsZSBhdCBodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvCgogIFNlbmQgY29tbWVudHMgdG8gaHR0cHM6Ly9naXRodWIuY29tL09wZW5Nb2JpbGVBbGxpYW5jZS9PTUFfTHdNMk1fZm9yX0RldmVsb3BlcnMvaXNzdWVzCgpDSEFOR0UgSElTVE9SWQoKMTUwNjIwMTggU3RhdHVzIGNoYW5nZWQgdG8gQXBwcm92ZWQgYnkgRE0sIERvYyBSZWYgIyBPTUEtRE0mU0UtMjAxOC0wMDYxLUlOUF9MV00yTV9BUFBEQVRBX1YxXzBfRVJQX2Zvcl9maW5hbF9BcHByb3ZhbAoyMTAyMjAxOSBTdGF0dXMgY2hhbmdlZCB0byBBcHByb3ZlZCBieSBJUFNPLCBEb2MgUmVmICMgT01BLUlQU08tMjAxOS0wMDI1LUlOUF9Md00yTV9PYmplY3RfQXBwX0RhdGFfQ29udGFpbmVyXzFfMF8xX2Zvcl9GaW5hbF9BcHByb3ZhbAoKTEVHQUwgRElTQ0xBSU1FUgoKQ29weXJpZ2h0IDIwMTkgT3BlbiBNb2JpbGUgQWxsaWFuY2UuCgpSZWRpc3RyaWJ1dGlvbiBhbmQgdXNlIGluIHNvdXJjZSBhbmQgYmluYXJ5IGZvcm1zLCB3aXRoIG9yIHdpdGhvdXQKbW9kaWZpY2F0aW9uLCBhcmUgcGVybWl0dGVkIHByb3ZpZGVkIHRoYXQgdGhlIGZvbGxvd2luZyBjb25kaXRpb25zCmFyZSBtZXQ6CgoxLiBSZWRpc3RyaWJ1dGlvbnMgb2Ygc291cmNlIGNvZGUgbXVzdCByZXRhaW4gdGhlIGFib3ZlIGNvcHlyaWdodApub3RpY2UsIHRoaXMgbGlzdCBvZiBjb25kaXRpb25zIGFuZCB0aGUgZm9sbG93aW5nIGRpc2NsYWltZXIuCjIuIFJlZGlzdHJpYnV0aW9ucyBpbiBiaW5hcnkgZm9ybSBtdXN0IHJlcHJvZHVjZSB0aGUgYWJvdmUgY29weXJpZ2h0Cm5vdGljZSwgdGhpcyBsaXN0IG9mIGNvbmRpdGlvbnMgYW5kIHRoZSBmb2xsb3dpbmcgZGlzY2xhaW1lciBpbiB0aGUKZG9jdW1lbnRhdGlvbiBhbmQvb3Igb3RoZXIgbWF0ZXJpYWxzIHByb3ZpZGVkIHdpdGggdGhlIGRpc3RyaWJ1dGlvbi4KMy4gTmVpdGhlciB0aGUgbmFtZSBvZiB0aGUgY29weXJpZ2h0IGhvbGRlciBub3IgdGhlIG5hbWVzIG9mIGl0cwpjb250cmlidXRvcnMgbWF5IGJlIHVzZWQgdG8gZW5kb3JzZSBvciBwcm9tb3RlIHByb2R1Y3RzIGRlcml2ZWQKZnJvbSB0aGlzIHNvZnR3YXJlIHdpdGhvdXQgc3BlY2lmaWMgcHJpb3Igd3JpdHRlbiBwZXJtaXNzaW9uLgoKVEhJUyBTT0ZUV0FSRSBJUyBQUk9WSURFRCBCWSBUSEUgQ09QWVJJR0hUIEhPTERFUlMgQU5EIENPTlRSSUJVVE9SUwoiQVMgSVMiIEFORCBBTlkgRVhQUkVTUyBPUiBJTVBMSUVEIFdBUlJBTlRJRVMsIElOQ0xVRElORywgQlVUIE5PVApMSU1JVEVEIFRPLCBUSEUgSU1QTElFRCBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSBBTkQgRklUTkVTUwpGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQVJFIERJU0NMQUlNRUQuIElOIE5PIEVWRU5UIFNIQUxMIFRIRQpDT1BZUklHSFQgSE9MREVSIE9SIENPTlRSSUJVVE9SUyBCRSBMSUFCTEUgRk9SIEFOWSBESVJFQ1QsIElORElSRUNULApJTkNJREVOVEFMLCBTUEVDSUFMLCBFWEVNUExBUlksIE9SIENPTlNFUVVFTlRJQUwgREFNQUdFUyAoSU5DTFVESU5HLApCVVQgTk9UIExJTUlURUQgVE8sIFBST0NVUkVNRU5UIE9GIFNVQlNUSVRVVEUgR09PRFMgT1IgU0VSVklDRVM7CkxPU1MgT0YgVVNFLCBEQVRBLCBPUiBQUk9GSVRTOyBPUiBCVVNJTkVTUyBJTlRFUlJVUFRJT04pIEhPV0VWRVIKQ0FVU0VEIEFORCBPTiBBTlkgVEhFT1JZIE9GIExJQUJJTElUWSwgV0hFVEhFUiBJTiBDT05UUkFDVCwgU1RSSUNUCkxJQUJJTElUWSwgT1IgVE9SVCAoSU5DTFVESU5HIE5FR0xJR0VOQ0UgT1IgT1RIRVJXSVNFKSBBUklTSU5HIElOCkFOWSBXQVkgT1VUIE9GIFRIRSBVU0UgT0YgVEhJUyBTT0ZUV0FSRSwgRVZFTiBJRiBBRFZJU0VEIE9GIFRIRQpQT1NTSUJJTElUWSBPRiBTVUNIIERBTUFHRS4KClRoZSBhYm92ZSBsaWNlbnNlIGlzIHVzZWQgYXMgYSBsaWNlbnNlIHVuZGVyIGNvcHlyaWdodCBvbmx5LiBQbGVhc2UKcmVmZXJlbmNlIHRoZSBPTUEgSVBSIFBvbGljeSBmb3IgcGF0ZW50IGxpY2Vuc2luZyB0ZXJtczoKaHR0cHM6Ly93d3cub21hc3BlY3dvcmtzLm9yZy9hYm91dC9pbnRlbGxlY3R1YWwtcHJvcGVydHktcmlnaHRzLwoKLS0+CjxMV00yTSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6bm9OYW1lc3BhY2VTY2hlbWFMb2NhdGlvbj0iaHR0cDovL29wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS54c2QiPgoJPE9iamVjdCBPYmplY3RUeXBlPSJNT0RlZmluaXRpb24iPgoJCTxOYW1lPkJpbmFyeUFwcERhdGFDb250YWluZXI8L05hbWU+CgkJPERlc2NyaXB0aW9uMT48IVtDREFUQVtUaGlzIEx3TTJNIE9iamVjdHMgcHJvdmlkZXMgdGhlIGFwcGxpY2F0aW9uIHNlcnZpY2UgZGF0YSByZWxhdGVkIHRvIGEgTHdNMk0gU2VydmVyLCBlZy4gV2F0ZXIgbWV0ZXIgZGF0YS4gClRoZXJlIGFyZSBzZXZlcmFsIG1ldGhvZHMgdG8gY3JlYXRlIGluc3RhbmNlIHRvIGluZGljYXRlIHRoZSBtZXNzYWdlIGRpcmVjdGlvbiBiYXNlZCBvbiB0aGUgbmVnb3RpYXRpb24gYmV0d2VlbiBBcHBsaWNhdGlvbiBhbmQgTHdNMk0uIFRoZSBDbGllbnQgYW5kIFNlcnZlciBzaG91bGQgbmVnb3RpYXRlIHRoZSBpbnN0YW5jZShzKSB1c2VkIHRvIGV4Y2hhbmdlIHRoZSBkYXRhLiBGb3IgZXhhbXBsZToKIC0gVXNpbmcgYSBzaW5nbGUgaW5zdGFuY2UgZm9yIGJvdGggZGlyZWN0aW9ucyBjb21tdW5pY2F0aW9uLCBmcm9tIENsaWVudCB0byBTZXJ2ZXIgYW5kIGZyb20gU2VydmVyIHRvIENsaWVudC4KIC0gVXNpbmcgYW4gaW5zdGFuY2UgZm9yIGNvbW11bmljYXRpb24gZnJvbSBDbGllbnQgdG8gU2VydmVyIGFuZCBhbm90aGVyIG9uZSBmb3IgY29tbXVuaWNhdGlvbiBmcm9tIFNlcnZlciB0byBDbGllbnQKIC0gVXNpbmcgc2V2ZXJhbCBpbnN0YW5jZXMKXV0+PC9EZXNjcmlwdGlvbjE+CgkJPE9iamVjdElEPjE5PC9PYmplY3RJRD4KCQk8T2JqZWN0VVJOPnVybjpvbWE6bHdtMm06b21hOjE5PC9PYmplY3RVUk4+CgkJPExXTTJNVmVyc2lvbj4xLjA8L0xXTTJNVmVyc2lvbj4KCQk8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CgkJPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJPFJlc291cmNlcz4KCQkJPEl0ZW0gSUQ9IjAiPjxOYW1lPkRhdGE8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5NdWx0aXBsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+T3BhcXVlPC9UeXBlPgoJCQkJPFJhbmdlRW51bWVyYXRpb24gLz4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgYXBwbGljYXRpb24gZGF0YSBjb250ZW50Ll1dPjwvRGVzY3JpcHRpb24+CgkJCTwvSXRlbT4KCQkJPEl0ZW0gSUQ9IjEiPjxOYW1lPkRhdGEgUHJpb3JpdHk8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgoJCQkJPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+SW50ZWdlcjwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjEgYnl0ZXM8L1JhbmdlRW51bWVyYXRpb24+CgkJCQk8VW5pdHMgLz4KCQkJCTxEZXNjcmlwdGlvbj48IVtDREFUQVtJbmRpY2F0ZXMgdGhlIEFwcGxpY2F0aW9uIGRhdGEgcHJpb3JpdHk6CjA6SW1tZWRpYXRlCjE6QmVzdEVmZm9ydAoyOkxhdGVzdAozLTEwMDogUmVzZXJ2ZWQgZm9yIGZ1dHVyZSB1c2UuCjEwMS0yNTQ6IFByb3ByaWV0YXJ5IG1vZGUuXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iMiI+PE5hbWU+RGF0YSBDcmVhdGlvbiBUaW1lPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlRpbWU8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbiAvPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBEYXRhIGluc3RhbmNlIGNyZWF0aW9uIHRpbWVzdGFtcC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSIzIj48TmFtZT5EYXRhIERlc2NyaXB0aW9uPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlN0cmluZzwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjMyIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkYXRhIGRlc2NyaXB0aW9uLgplLmcuICJtZXRlciByZWFkaW5nIi5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSI0Ij48TmFtZT5EYXRhIEZvcm1hdDwvTmFtZT4KCQkJCTxPcGVyYXRpb25zPlJXPC9PcGVyYXRpb25zPgoJCQkJPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJCQk8VHlwZT5TdHJpbmc8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4zMiBieXRlczwvUmFuZ2VFbnVtZXJhdGlvbj4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgZm9ybWF0IG9mIHRoZSBBcHBsaWNhdGlvbiBEYXRhLgplLmcuIFlHLU1ldGVyLVdhdGVyLVJlYWRpbmcKVVRGOC1zdHJpbmcKXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iNSI+PE5hbWU+QXBwIElEPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPkludGVnZXI8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4yIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkZXN0aW5hdGlvbiBBcHBsaWNhdGlvbiBJRC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+PC9SZXNvdXJjZXM+CgkJPERlc2NyaXB0aW9uMj48IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMj4KCTwvT2JqZWN0Pgo8L0xXTTJNPgo=";
+
private Tenant savedTenant;
private User tenantAdmin;
@@ -87,7 +99,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My first resource");
resource.setFileName(DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
TbResource savedResource = save(resource);
@@ -122,7 +134,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle(StringUtils.randomAlphabetic(300));
resource.setFileName(DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
Mockito.reset(tbClusterService, auditLogService);
@@ -141,7 +153,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My first resource");
resource.setFileName(DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
TbResource savedResource = save(resource);
@@ -170,7 +182,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My first resource");
resource.setFileName(DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
TbResource savedResource = save(resource);
@@ -185,7 +197,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My first resource");
resource.setFileName(DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
TbResource savedResource = save(resource);
@@ -216,7 +228,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setTitle("Resource" + i);
resource.setResourceType(ResourceType.JKS);
resource.setFileName(i + DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
resources.add(new TbResourceInfo(save(resource)));
}
List loadedResources = new ArrayList<>();
@@ -242,6 +254,54 @@ public class TbResourceControllerTest extends AbstractControllerTest {
Assert.assertEquals(resources, loadedResources);
}
+ @Test
+ public void testFindTenantTbResourcesByType() throws Exception {
+ Mockito.reset(tbClusterService, auditLogService);
+
+ List resources = new ArrayList<>();
+ int jksCntEntity = 17;
+ for (int i = 0; i < jksCntEntity; i++) {
+ TbResource resource = new TbResource();
+ resource.setTitle("JKS Resource" + i);
+ resource.setResourceType(ResourceType.JKS);
+ resource.setFileName(i + DEFAULT_FILE_NAME);
+ resource.setData(TEST_DATA);
+ resources.add(new TbResourceInfo(save(resource)));
+ }
+
+ int lwm2mCntEntity = 19;
+ for (int i = 0; i < lwm2mCntEntity; i++) {
+ TbResource resource = new TbResource();
+ resource.setTitle("LWM2M Resource" + i);
+ resource.setResourceType(ResourceType.PKCS_12);
+ resource.setFileName(i + DEFAULT_FILE_NAME_2);
+ resource.setData(TEST_DATA);
+ save(resource);
+ }
+
+ List loadedResources = new ArrayList<>();
+ PageLink pageLink = new PageLink(5);
+ PageData pageData;
+ do {
+ pageData = doGetTypedWithPageLink("/api/resource?resourceType=" + ResourceType.JKS.name() + "&",
+ new TypeReference<>() {
+ }, pageLink);
+ loadedResources.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageLink.nextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ testNotifyManyEntityManyTimeMsgToEdgeServiceNever(new TbResource(), new TbResource(),
+ savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
+ ActionType.ADDED, jksCntEntity + lwm2mCntEntity);
+
+ Collections.sort(resources, idComparator);
+ Collections.sort(loadedResources, idComparator);
+
+ Assert.assertEquals(resources, loadedResources);
+ }
+
@Test
public void testFindSystemTbResources() throws Exception {
loginSysAdmin();
@@ -252,7 +312,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setTitle("Resource" + i);
resource.setResourceType(ResourceType.JKS);
resource.setFileName(i + DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
resources.add(new TbResourceInfo(save(resource)));
}
List loadedResources = new ArrayList<>();
@@ -300,6 +360,86 @@ public class TbResourceControllerTest extends AbstractControllerTest {
Assert.assertTrue(loadedResources.isEmpty());
}
+ @Test
+ public void testFindSystemTbResourcesByType() throws Exception {
+ loginSysAdmin();
+
+ List jksResources = new ArrayList<>();
+ List lwm2mesources = new ArrayList<>();
+ int jksCntEntity = 17;
+ for (int i = 0; i < jksCntEntity; i++) {
+ TbResource resource = new TbResource();
+ resource.setTitle("JKS Resource" + i);
+ resource.setResourceType(ResourceType.JKS);
+ resource.setFileName(i + DEFAULT_FILE_NAME);
+ resource.setData(TEST_DATA);
+ TbResourceInfo saved = new TbResourceInfo(save(resource));
+ jksResources.add(saved);
+ }
+
+ int lwm2mCntEntity = 19;
+ for (int i = 0; i < lwm2mCntEntity; i++) {
+ TbResource resource = new TbResource();
+ resource.setTitle("LWM2M Resource" + i);
+ resource.setResourceType(ResourceType.PKCS_12);
+ resource.setFileName(i + DEFAULT_FILE_NAME_2);
+ resource.setData(TEST_DATA);
+ TbResource saved = save(resource);
+ lwm2mesources.add(saved);
+ }
+
+ List loadedResources = new ArrayList<>();
+ PageLink pageLink = new PageLink(30);
+ PageData pageData;
+ do {
+ pageData = doGetTypedWithPageLink("/api/resource?resourceType=" + ResourceType.JKS + "&",
+ new TypeReference<>() {
+ }, pageLink);
+ loadedResources.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageLink.nextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(jksResources, idComparator);
+ Collections.sort(loadedResources, idComparator);
+
+ Assert.assertEquals(jksResources, loadedResources);
+
+ Mockito.reset(tbClusterService, auditLogService);
+
+ int cntEntity = jksResources.size();
+ for (TbResourceInfo resource : jksResources) {
+ doDelete("/api/resource/" + resource.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ testNotifyManyEntityManyTimeMsgToEdgeServiceNeverAdditionalInfoAny(new TbResource(), new TbResource(),
+ jksResources.get(0).getTenantId(), null, null, SYS_ADMIN_EMAIL,
+ ActionType.DELETED, cntEntity, 1);
+
+ pageLink = new PageLink(27);
+ loadedResources.clear();
+ do {
+ pageData = doGetTypedWithPageLink("/api/resource?resourceType=" + ResourceType.JKS + "&",
+ new TypeReference<>() {
+ }, pageLink);
+ loadedResources.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageLink.nextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Assert.assertTrue(loadedResources.isEmpty());
+
+ loginSysAdmin();
+
+ for (TbResourceInfo resource : lwm2mesources) {
+ doDelete("/api/resource/" + resource.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+ }
+
@Test
public void testFindSystemAndTenantTbResources() throws Exception {
List systemResources = new ArrayList<>();
@@ -309,7 +449,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setTitle("Resource" + i);
resource.setResourceType(ResourceType.JKS);
resource.setFileName(i + DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
expectedResources.add(new TbResourceInfo(save(resource)));
}
@@ -320,7 +460,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setTitle("Resource" + i);
resource.setResourceType(ResourceType.JKS);
resource.setFileName(i + DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
TbResourceInfo savedResource = new TbResourceInfo(save(resource));
systemResources.add(savedResource);
if (i >= 73) {
@@ -356,6 +496,99 @@ public class TbResourceControllerTest extends AbstractControllerTest {
}
}
+ @Test
+ public void testDownloadTbResourceIfChanged() throws Exception {
+ Mockito.reset(tbClusterService, auditLogService);
+
+ TbResource resource = new TbResource();
+ resource.setResourceType(ResourceType.JS_MODULE);
+ resource.setTitle("Js resource");
+ resource.setFileName(JS_TEST_FILE_NAME);
+ resource.setData(TEST_DATA);
+
+ TbResource savedResource = save(resource);
+
+ testNotifyEntityOneTimeMsgToEdgeServiceNever(savedResource, savedResource.getId(), savedResource.getId(),
+ savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
+ ActionType.ADDED);
+
+ ResultActions resultActions = doGet("/api/resource/js/" + savedResource.getId().getId().toString() + "/download")
+ .andExpect(status().isOk());
+ MockHttpServletResponse response = resultActions.andReturn().getResponse();
+ String eTag = response.getHeader("ETag");
+ Assert.assertNotNull(eTag);
+ Assert.assertEquals(Base64.getEncoder().encodeToString(response.getContentAsByteArray()), TEST_DATA);
+
+ //download with if-none-match header
+ HttpHeaders headers = new HttpHeaders();
+ headers.setIfNoneMatch(eTag);
+ doGet("/api/resource/js/" + savedResource.getId().getId().toString() + "/download", headers)
+ .andExpect(status().isNotModified());
+ }
+
+ @Test
+ public void testDownloadTbResourceIfChangedAsPublicCustomer() throws Exception {
+ loginTenantAdmin();
+ Mockito.reset(tbClusterService, auditLogService);
+
+ TbResource resource = new TbResource();
+ resource.setResourceType(ResourceType.JS_MODULE);
+ resource.setTitle("Js resource");
+ resource.setFileName(JS_TEST_FILE_NAME);
+ resource.setData(TEST_DATA);
+
+ TbResource savedResource = save(resource);
+
+ //download as public customer
+ Device device = new Device();
+ device.setName("Test Public Device");
+ device.setLabel("Label");
+ device.setCustomerId(customerId);
+ device = doPost("/api/device", device, Device.class);
+ device = doPost("/api/customer/public/device/" + device.getUuidId(), Device.class);
+
+ String publicId = device.getCustomerId().toString();
+
+ Mockito.reset(tbClusterService, auditLogService);
+ resetTokens();
+
+ JsonNode publicLoginRequest = JacksonUtil.toJsonNode("{\"publicId\": \"" + publicId + "\"}");
+ JsonNode tokens = doPost("/api/auth/login/public", publicLoginRequest, JsonNode.class);
+ this.token = tokens.get("token").asText();
+
+ ResultActions resultActions = doGet("/api/resource/js/" + savedResource.getId().getId().toString() + "/download")
+ .andExpect(status().isOk());
+ MockHttpServletResponse response = resultActions.andReturn().getResponse();
+ String eTag = response.getHeader("ETag");
+ Assert.assertNotNull(eTag);
+ Assert.assertEquals(Base64.getEncoder().encodeToString(response.getContentAsByteArray()), TEST_DATA);
+
+ //download with if-none-match header
+ HttpHeaders headers = new HttpHeaders();
+ headers.setIfNoneMatch(eTag);
+ doGet("/api/resource/js/" + savedResource.getId().getId().toString() + "/download", headers)
+ .andExpect(status().isNotModified());
+ }
+
+ @Test
+ public void testDownloadTbResourceIfChangedAsCustomerOfDifferentTenant() throws Exception {
+ loginTenantAdmin();
+ Mockito.reset(tbClusterService, auditLogService);
+
+ TbResource resource = new TbResource();
+ resource.setResourceType(ResourceType.JS_MODULE);
+ resource.setTitle("Js resource");
+ resource.setFileName(JS_TEST_FILE_NAME);
+ resource.setData(TEST_DATA);
+
+ TbResource savedResource = save(resource);
+
+ loginDifferentTenant();
+ loginDifferentTenantCustomer();
+ doGet("/api/resource/js/" + savedResource.getId().getId().toString() + "/download")
+ .andExpect(status().isForbidden());
+ }
+
private TbResource save(TbResource tbResource) throws Exception {
return doPostWithTypedResponse("/api/resource", tbResource, new TypeReference<>(){});
}
diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
index 135b6d87c8..82b2ce3ec2 100644
--- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
+++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
@@ -100,6 +100,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@TestPropertySource(properties = {
"edges.enabled=true",
+ "queue.rule-engine.stats.enabled=false",
})
abstract public class AbstractEdgeTest extends AbstractControllerTest {
@@ -181,14 +182,14 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
@After
public void afterTest() throws Exception {
+ try {
+ edgeImitator.disconnect();
+ } catch (Exception ignored){}
+
loginSysAdmin();
doDelete("/api/tenant/" + savedTenant.getUuidId())
.andExpect(status().isOk());
-
- try {
- edgeImitator.disconnect();
- } catch (Exception ignored) {}
}
private void installation() {
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
index 8f05e6810f..0edf070aef 100644
--- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
+++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
@@ -94,8 +94,6 @@ public class EdgeImitator {
@Getter
private UplinkResponseMsg latestResponseMsg;
- private boolean connected = false;
-
public EdgeImitator(String host, int port, String routingKey, String routingSecret) throws NoSuchFieldException, IllegalAccessException {
edgeRpcClient = new EdgeGrpcClient();
messagesLatch = new CountDownLatch(0);
@@ -120,7 +118,6 @@ public class EdgeImitator {
}
public void connect() {
- connected = true;
edgeRpcClient.connect(routingKey, routingSecret,
this::onUplinkResponse,
this::onEdgeUpdate,
@@ -131,7 +128,6 @@ public class EdgeImitator {
}
public void disconnect() throws InterruptedException {
- connected = false;
edgeRpcClient.disconnect(false);
}
diff --git a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
index ef94069960..6c2e5e9c5e 100644
--- a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
@@ -27,6 +27,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration;
+import org.thingsboard.rule.engine.metadata.FetchTo;
+import org.thingsboard.rule.engine.metadata.TbGetAttributesNode;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.DataConstants;
@@ -137,16 +139,20 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
RuleNode ruleNode1 = new RuleNode();
ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+ ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
+ configuration1.setFetchTo(FetchTo.METADATA);
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1));
RuleNode ruleNode2 = new RuleNode();
ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+ ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
+ configuration2.setFetchTo(FetchTo.METADATA);
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2));
@@ -239,8 +245,10 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
RuleNode ruleNode1 = new RuleNode();
ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+ ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
+ configuration1.setFetchTo(FetchTo.METADATA);
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1));
@@ -271,8 +279,10 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
RuleNode ruleNode2 = new RuleNode();
ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+ ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
+ configuration2.setFetchTo(FetchTo.METADATA);
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2));
diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
index 96a5e32d95..9753f06aff 100644
--- a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
@@ -24,6 +24,8 @@ import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.rule.engine.metadata.FetchTo;
+import org.thingsboard.rule.engine.metadata.TbGetAttributesNode;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.DataConstants;
@@ -92,8 +94,10 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac
RuleNode ruleNode = new RuleNode();
ruleNode.setName("Simple Rule Node");
ruleNode.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+ ruleNode.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
+ configuration.setFetchTo(FetchTo.METADATA);
configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey"));
ruleNode.setConfiguration(JacksonUtil.valueToTree(configuration));
diff --git a/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java b/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java
index 05ee66f327..c56adb5fd5 100644
--- a/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java
@@ -24,11 +24,12 @@ import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
+import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.util.limits.DefaultRateLimitService;
-import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import java.util.List;
@@ -37,6 +38,7 @@ import java.util.UUID;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;
@@ -50,7 +52,7 @@ public class RateLimitServiceTest {
@Before
public void beforeEach() {
tenantProfileCache = Mockito.mock(TbTenantProfileCache.class);
- rateLimitService = new DefaultRateLimitService(tenantProfileCache, 60, 100);
+ rateLimitService = new DefaultRateLimitService(tenantProfileCache, mock(NotificationRuleProcessor.class), 60, 100);
tenantId = new TenantId(UUID.randomUUID());
}
@@ -73,14 +75,14 @@ public class RateLimitServiceTest {
LimitedApi.ENTITY_EXPORT,
LimitedApi.ENTITY_IMPORT,
LimitedApi.NOTIFICATION_REQUESTS,
- LimitedApi.REST_REQUESTS,
+ LimitedApi.REST_REQUESTS_PER_CUSTOMER,
LimitedApi.CASSANDRA_QUERIES
)) {
testRateLimits(limitedApi, max, tenantId);
}
CustomerId customerId = new CustomerId(UUID.randomUUID());
- testRateLimits(LimitedApi.REST_REQUESTS, max, customerId);
+ testRateLimits(LimitedApi.REST_REQUESTS_PER_CUSTOMER, max, customerId);
NotificationRuleId notificationRuleId = new NotificationRuleId(UUID.randomUUID());
testRateLimits(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, max, notificationRuleId);
diff --git a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
index ac3857dbdf..fb7675bd9e 100644
--- a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.service.notification;
import com.fasterxml.jackson.core.type.TypeReference;
import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.After;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.util.Pair;
@@ -26,6 +27,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.NotificationTemplateId;
+import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.Notification;
@@ -38,11 +40,12 @@ import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.rule.DefaultNotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter;
+import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
@@ -54,6 +57,10 @@ 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.DaoUtil;
+import org.thingsboard.server.dao.notification.NotificationRequestService;
+import org.thingsboard.server.dao.notification.NotificationRuleService;
+import org.thingsboard.server.dao.notification.NotificationTargetService;
+import org.thingsboard.server.dao.notification.NotificationTemplateService;
import java.net.URISyntaxException;
import java.util.Arrays;
@@ -72,21 +79,40 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
@MockBean
protected SlackService slackService;
-
@Autowired
protected MailService mailService;
+ @Autowired
+ protected NotificationRuleService notificationRuleService;
+ @Autowired
+ protected NotificationTemplateService notificationTemplateService;
+ @Autowired
+ protected NotificationTargetService notificationTargetService;
+ @Autowired
+ protected NotificationRequestService notificationRequestService;
+
public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test";
public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL;
+ @After
+ public void afterEach() {
+ notificationRequestService.deleteNotificationRequestsByTenantId(TenantId.SYS_TENANT_ID);
+ notificationRuleService.deleteNotificationRulesByTenantId(TenantId.SYS_TENANT_ID);
+ notificationTemplateService.deleteNotificationTemplatesByTenantId(TenantId.SYS_TENANT_ID);
+ notificationTargetService.deleteNotificationTargetsByTenantId(TenantId.SYS_TENANT_ID);
+ }
+
protected NotificationTarget createNotificationTarget(UserId... usersIds) {
- NotificationTarget notificationTarget = new NotificationTarget();
- notificationTarget.setTenantId(tenantId);
- notificationTarget.setName("Users " + List.of(usersIds));
- PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig();
UserListFilter filter = new UserListFilter();
filter.setUsersIds(DaoUtil.toUUIDs(List.of(usersIds)));
- targetConfig.setUsersFilter(filter);
+ return createNotificationTarget(filter);
+ }
+
+ protected NotificationTarget createNotificationTarget(UsersFilter usersFilter) {
+ NotificationTarget notificationTarget = new NotificationTarget();
+ notificationTarget.setName(usersFilter.toString());
+ PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig();
+ targetConfig.setUsersFilter(usersFilter);
notificationTarget.setConfiguration(targetConfig);
return saveNotificationTarget(notificationTarget);
}
diff --git a/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java b/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java
index a1cc82fd5a..a49f8dc8cb 100644
--- a/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java
+++ b/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java
@@ -26,7 +26,7 @@ import org.thingsboard.server.dao.settings.AdminSettingsService;
public class MockNotificationSettingsService extends DefaultNotificationSettingsService {
public MockNotificationSettingsService(AdminSettingsService adminSettingsService) {
- super(adminSettingsService, null, null);
+ super(adminSettingsService, null, null, null);
}
@Override
diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
index 455b897fcd..f89730b689 100644
--- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
@@ -17,12 +17,14 @@ package org.thingsboard.server.service.notification;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.BooleanNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Before;
import org.junit.Test;
import org.junit.function.ThrowingRunnable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.data.util.Pair;
+import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
@@ -31,10 +33,15 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmComment;
+import org.thingsboard.server.common.data.alarm.AlarmCommentType;
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.asset.Asset;
+import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration;
+import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
+import org.thingsboard.server.common.data.device.data.DeviceData;
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;
@@ -42,23 +49,36 @@ 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.DeviceProfileAlarm;
import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec;
+import org.thingsboard.server.common.data.id.AlarmId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestInfo;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo;
+import org.thingsboard.server.common.data.notification.info.RateLimitsNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.DefaultNotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.EscalatedNotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo;
-import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig;
-import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction;
-import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig;
-import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig;
-import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionNotificationRuleTriggerConfig;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig.AlarmAction;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.EntitiesLimitNotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.EntityActionNotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NewPlatformVersionNotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.RateLimitsNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
+import org.thingsboard.server.common.data.notification.targets.platform.AffectedTenantAdministratorsFilter;
+import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@@ -68,13 +88,15 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.Authority;
-import org.thingsboard.server.common.msg.notification.trigger.NewPlatformVersionTrigger;
+import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
+import org.thingsboard.server.dao.notification.DefaultNotifications;
import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DaoSqlTest;
-import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
-import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
+import org.thingsboard.server.queue.notification.DefaultNotificationDeduplicationService;
+import org.thingsboard.server.service.notification.rule.cache.DefaultNotificationRulesCache;
+import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.util.ArrayList;
@@ -88,14 +110,23 @@ import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.offset;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig.Action.ASSIGNED;
+import static org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig.Action.UNASSIGNED;
+import static org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig.DeviceEvent.ACTIVE;
+import static org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig.DeviceEvent.INACTIVE;
@DaoSqlTest
+@TestPropertySource(properties = {
+ "transport.http.enabled=true",
+ "notification_system.rules.deduplication_durations=RATE_LIMITS:10000"
+})
public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@SpyBean
@@ -108,6 +139,12 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
private RuleChainService ruleChainService;
@Autowired
private NotificationRuleProcessor notificationRuleProcessor;
+ @Autowired
+ private DefaultNotifications defaultNotifications;
+ @Autowired
+ private DefaultNotificationRulesCache notificationRulesCache;
+ @Autowired
+ private DeviceStateService deviceStateService;
@Before
public void beforeEach() throws Exception {
@@ -297,7 +334,9 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
notification = getWsClient().getLastDataUpdate().getUpdate();
assertThat(notification.getSubject()).isEqualTo("critical alarm '" + alarmType + "' is CLEARED_UNACK");
- assertThat(findNotificationRequests(EntityType.ALARM).getData()).filteredOn(NotificationRequest::isScheduled).isEmpty();
+ await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+ assertThat(findNotificationRequests(EntityType.ALARM).getData()).filteredOn(NotificationRequest::isScheduled).isEmpty();
+ });
}
@Test
@@ -370,6 +409,176 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
});
}
+ @Test
+ public void testNotificationRuleProcessing_exceededRateLimits() throws Exception {
+ loginSysAdmin();
+ NotificationTarget sysadmins = createNotificationTarget(new SystemAdministratorsFilter());
+ NotificationTarget affectedTenantAdmins = createNotificationTarget(new AffectedTenantAdministratorsFilter());
+ defaultNotifications.create(TenantId.SYS_TENANT_ID, DefaultNotifications.exceededRateLimitsForSysadmin, sysadmins.getId());
+ defaultNotifications.create(TenantId.SYS_TENANT_ID, DefaultNotifications.exceededRateLimits, affectedTenantAdmins.getId());
+ defaultNotifications.create(TenantId.SYS_TENANT_ID, DefaultNotifications.exceededPerEntityRateLimits, affectedTenantAdmins.getId());
+ notificationRulesCache.evict(TenantId.SYS_TENANT_ID);
+
+ int n = 10;
+ updateDefaultTenantProfile(profileConfiguration -> {
+ profileConfiguration.setTenantEntityExportRateLimit(n + ":600");
+ profileConfiguration.setCustomerServerRestLimitsConfiguration(n + ":600");
+ profileConfiguration.setTenantNotificationRequestsPerRuleRateLimit(n + ":600");
+ profileConfiguration.setTransportDeviceTelemetryMsgRateLimit(n + ":600");
+ });
+ loginTenantAdmin();
+ NotificationRule rule = createNotificationRule(AlarmCommentNotificationRuleTriggerConfig.builder()
+ .alarmTypes(Set.of("weklfjkwefa"))
+ .build(), "Test", "Test", createNotificationTarget(tenantAdminUserId).getId());
+ for (int i = 1; i <= n * 2; i++) {
+ rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, tenantId);
+ rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS_PER_CUSTOMER, tenantId, customerId);
+ rateLimitService.checkRateLimit(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, tenantId, rule.getId());
+ }
+
+ loginTenantAdmin();
+ List notifications = await().atMost(30, TimeUnit.SECONDS)
+ .until(() -> getMyNotifications(true, 10), list -> list.size() == 3);
+ assertThat(notifications).allSatisfy(notification -> {
+ assertThat(notification.getSubject()).isEqualTo("Rate limits exceeded");
+ });
+ assertThat(notifications).anySatisfy(notification -> {
+ assertThat(notification.getText()).isEqualTo("Rate limits for entity version creation exceeded");
+ });
+ assertThat(notifications).anySatisfy(notification -> {
+ assertThat(notification.getText()).isEqualTo("Rate limits for REST API requests per customer " +
+ "exceeded for 'Customer'");
+ });
+ assertThat(notifications).anySatisfy(notification -> {
+ assertThat(notification.getText()).isEqualTo("Rate limits for notification requests " +
+ "per rule exceeded for '" + rule.getName() + "'");
+ });
+
+ loginSysAdmin();
+ notifications = await().atMost(30, TimeUnit.SECONDS)
+ .until(() -> getMyNotifications(true, 10), list -> list.size() == 1);
+ assertThat(notifications).allSatisfy(notification -> {
+ assertThat(notification.getSubject()).isEqualTo("Rate limits exceeded for tenant " + TEST_TENANT_NAME);
+ });
+ assertThat(notifications.get(0).getText()).isEqualTo("Rate limits for entity version creation exceeded");
+ }
+
+ @Test
+ public void testNotificationRuleProcessing_alarmAssignment() throws Exception {
+ AlarmAssignmentNotificationRuleTriggerConfig triggerConfig = AlarmAssignmentNotificationRuleTriggerConfig.builder()
+ .alarmTypes(Set.of("test"))
+ .notifyOn(Set.of(ASSIGNED, UNASSIGNED))
+ .build();
+ NotificationTarget target = createNotificationTarget(tenantAdminUserId);
+ String template = "${userEmail} ${action} alarm on ${alarmOriginatorEntityType} '${alarmOriginatorName}'. Assignee: ${assigneeEmail}";
+ createNotificationRule(triggerConfig, "Test", template, target.getId());
+
+ Device device = createDevice("Device A", "123");
+ Alarm alarm = Alarm.builder()
+ .tenantId(tenantId)
+ .originator(device.getId())
+ .cleared(false)
+ .acknowledged(false)
+ .severity(AlarmSeverity.CRITICAL)
+ .type("test")
+ .startTs(System.currentTimeMillis())
+ .build();
+ alarm = doPost("/api/alarm", alarm, Alarm.class);
+ AlarmId alarmId = alarm.getId();
+
+ checkNotificationAfter(() -> {
+ doPost("/api/alarm/" + alarmId + "/assign/" + tenantAdminUserId).andExpect(status().isOk());
+ }, notification -> {
+ assertThat(notification.getText()).isEqualTo(
+ TENANT_ADMIN_EMAIL + " assigned alarm on Device 'Device A'. Assignee: " + TENANT_ADMIN_EMAIL
+ );
+ });
+
+ checkNotificationAfter(() -> {
+ doDelete("/api/alarm/" + alarmId + "/assign").andExpect(status().isOk());
+ }, notification -> {
+ assertThat(notification.getText()).isEqualTo(
+ TENANT_ADMIN_EMAIL + " unassigned alarm on Device 'Device A'. Assignee: "
+ );
+ });
+ }
+
+ @Test
+ public void testNotificationRuleProcessing_alarmComment() throws Exception {
+ AlarmCommentNotificationRuleTriggerConfig triggerConfig = AlarmCommentNotificationRuleTriggerConfig.builder()
+ .alarmTypes(Set.of("test"))
+ .onlyUserComments(true)
+ .notifyOnCommentUpdate(true)
+ .build();
+ NotificationTarget target = createNotificationTarget(tenantAdminUserId);
+ String template = "${userEmail} ${action} comment on alarm ${alarmType}: ${comment}";
+ createNotificationRule(triggerConfig, "Test", template, target.getId());
+
+ Device device = createDevice("Device A", "123");
+ Alarm alarm = Alarm.builder()
+ .tenantId(tenantId)
+ .originator(device.getId())
+ .cleared(false)
+ .acknowledged(false)
+ .severity(AlarmSeverity.CRITICAL)
+ .type("test")
+ .startTs(System.currentTimeMillis())
+ .build();
+ alarm = doPost("/api/alarm", alarm, Alarm.class);
+ AlarmId alarmId = alarm.getId();
+
+ AlarmComment comment = checkNotificationAfter(() -> {
+ return doPost("/api/alarm/" + alarmId + "/comment",
+ AlarmComment.builder()
+ .type(AlarmCommentType.OTHER)
+ .comment(JacksonUtil.newObjectNode()
+ .put("text", "this is bad"))
+ .build(), AlarmComment.class);
+ }, (notification, r) -> {
+ assertThat(notification.getText()).isEqualTo(
+ TENANT_ADMIN_EMAIL + " added comment on alarm test: this is bad"
+ );
+ });
+
+ checkNotificationAfter(() -> {
+ ((ObjectNode) comment.getComment()).put("text", "this is very bad");
+ doPost("/api/alarm/" + alarmId + "/comment", comment);
+ }, notification -> {
+ assertThat(notification.getText()).isEqualTo(
+ TENANT_ADMIN_EMAIL + " updated comment on alarm test: this is very bad"
+ );
+ });
+ }
+
+ @Test
+ public void testNotificationRuleProcessing_deviceActivity() throws Exception {
+ DeviceActivityNotificationRuleTriggerConfig triggerConfig = DeviceActivityNotificationRuleTriggerConfig.builder()
+ .notifyOn(Set.of(ACTIVE, INACTIVE))
+ .build();
+ NotificationTarget target = createNotificationTarget(tenantAdminUserId);
+ String template = "Device ${deviceName} (${deviceLabel}) of type ${deviceType} is now ${eventType}";
+ createNotificationRule(triggerConfig, "Test", template, target.getId());
+
+ Device device = new Device();
+ device.setName("A");
+ device.setLabel("Test Device A");
+ device.setType("test");
+ DeviceData deviceData = new DeviceData();
+ deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration());
+ deviceData.setConfiguration(new DefaultDeviceConfiguration());
+ device.setDeviceData(deviceData);
+ device = doPost("/api/device", device, Device.class);
+ DeviceId deviceId = device.getId();
+
+ checkNotificationAfter(() -> {
+ deviceStateService.onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
+ }, notification -> {
+ assertThat(notification.getText()).isEqualTo(
+ "Device A (Test Device A) of type test is now active"
+ );
+ });
+ }
+
@Test
public void testNotificationRuleInfo() throws Exception {
NotificationDeliveryMethod[] deliveryMethods = {NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL};
@@ -444,7 +653,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
}
@Test
- public void testNotificationsDeduplication() throws Exception {
+ public void testNotificationsDeduplication_newPlatformVersion() throws Exception {
loginSysAdmin();
NewPlatformVersionNotificationRuleTriggerConfig triggerConfig = new NewPlatformVersionNotificationRuleTriggerConfig();
createNotificationRule(triggerConfig, "Test", "Test", createNotificationTarget(tenantAdminUserId).getId());
@@ -471,12 +680,75 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
});
}
+ @Test
+ public void testNotificationsDeduplication_exceededRateLimits() throws Exception {
+ RateLimitsNotificationRuleTriggerConfig triggerConfig = new RateLimitsNotificationRuleTriggerConfig();
+ triggerConfig.setApis(Set.of(LimitedApi.ENTITY_EXPORT, LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE));
+
+ loginSysAdmin();
+ NotificationTarget target = createNotificationTarget(tenantAdminUserId);
+ NotificationRule rule = createNotificationRule(triggerConfig, "Test 1", "Test", target.getId());
+
+ int n = 5;
+ updateDefaultTenantProfile(profileConfiguration -> {
+ profileConfiguration.setTenantEntityExportRateLimit(n + ":600");
+ profileConfiguration.setTransportDeviceTelemetryMsgRateLimit(n + ":800");
+ });
+
+ RateLimitsTrigger expectedTrigger = RateLimitsTrigger.builder()
+ .tenantId(tenantId)
+ .api(LimitedApi.ENTITY_EXPORT)
+ .limitLevel(tenantId)
+ .build();
+ assertThat(DefaultNotificationDeduplicationService.getDeduplicationKey(expectedTrigger, rule))
+ .isEqualTo("RATE_LIMITS:TENANT:" + tenantId + ":ENTITY_EXPORT_" +
+ target.getId() + ":ENTITY_EXPORT,TRANSPORT_MESSAGES_PER_DEVICE");
+
+ loginTenantAdmin();
+ getWsClient().subscribeForUnreadNotifications(10).waitForReply();
+ getWsClient().registerWaitForUpdate(2);
+ Device device = createDevice("Test", "Test");
+ for (int i = 1; i <= n + 1; i++) {
+ rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, tenantId);
+ doPost("/api/v1/" + device.getName() + "/telemetry", "{\"dp1\":123}", String.class);
+ }
+ int expectedNotificationsCount1 = 2;
+ getWsClient().waitForUpdate(true);
+ List notifications1 = getMyNotifications(true, 10);
+ assertThat(notifications1).size().isEqualTo(expectedNotificationsCount1);
+ assertThat(notifications1)
+ .anyMatch(notification -> ((RateLimitsNotificationInfo) notification.getInfo()).getApi() == LimitedApi.ENTITY_EXPORT)
+ .anyMatch(notification -> ((RateLimitsNotificationInfo) notification.getInfo()).getApi() == LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE);
+
+ getWsClient().registerWaitForUpdate(2);
+ for (int i = 0; i < 10; i++) {
+ rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, tenantId);
+ doPost("/api/v1/" + device.getName() + "/telemetry", "{\"dp1\":123}", String.class);
+ }
+ assertThat(getWsClient().waitForUpdate(5000)).isNull();
+
+ int deduplicationDuration = 10000; // configured in TestPropertySource above
+ await().atLeast(2, TimeUnit.SECONDS)
+ .atMost(deduplicationDuration, TimeUnit.MILLISECONDS)
+ .untilAsserted(() -> {
+ rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, tenantId);
+ doPost("/api/v1/" + device.getName() + "/telemetry", "{\"dp1\":123}", String.class);
+
+ Map notifications2 = getMyNotifications(true, 10).stream()
+ .map(notification -> (RateLimitsNotificationInfo) notification.getInfo())
+ .collect(Collectors.groupingBy(RateLimitsNotificationInfo::getApi, Collectors.counting()));
+ assertThat(notifications2.get(LimitedApi.ENTITY_EXPORT)).isEqualTo(2);
+ assertThat(notifications2.get(LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE)).isEqualTo(2);
+ });
+ }
+
@Test
public void testNotificationRuleDisabling() throws Exception {
EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig();
triggerConfig.setEntityTypes(Set.of(EntityType.DEVICE));
triggerConfig.setCreated(true);
NotificationRule rule = createNotificationRule(triggerConfig, "Created", "Created", createNotificationTarget(tenantAdminUserId).getId());
+ notificationRulesCache.evict(tenantId);
assertThat(getMyNotifications(false, 100)).size().isZero();
createDevice("Device 1", "default", "111");
@@ -487,6 +759,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
rule.setEnabled(false);
saveNotificationRule(rule);
+ notificationRulesCache.evict(tenantId);
createDevice("Device 2", "default", "222");
TimeUnit.SECONDS.sleep(5);
@@ -494,7 +767,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
rule.setEnabled(true);
saveNotificationRule(rule);
- TimeUnit.SECONDS.sleep(2); // for rule update event to reach rules cache
+ notificationRulesCache.evict(tenantId);
createDevice("Device 3", "default", "333");
await().atMost(30, TimeUnit.SECONDS)
diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java
index 480bbf939b..76b68bb9bf 100644
--- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java
@@ -24,7 +24,7 @@ import org.springframework.test.web.servlet.ResultMatcher;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
-import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter;
diff --git a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java
index 5c9d94cde2..6f08e47412 100644
--- a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java
@@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.EntityInfo;
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.TbResourceInfoFilter;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User;
@@ -104,6 +105,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
"";
private static final String DEFAULT_FILE_NAME = "test.jks";
+ private static final String TEST_DATA = "77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQpGSUxFIElORk9STUFUSU9OCgpPTUEgUGVybWFuZW50IERvY3VtZW50CiAgIEZpbGU6IE9NQS1TVVAtTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci1WMV8wXzEtMjAxOTAyMjEtQQogICBUeXBlOiB4bWwKClB1YmxpYyBSZWFjaGFibGUgSW5mb3JtYXRpb24KICAgUGF0aDogaHR0cDovL3d3dy5vcGVubW9iaWxlYWxsaWFuY2Uub3JnL3RlY2gvcHJvZmlsZXMKICAgTmFtZTogTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci12MV8wXzEueG1sCgpOT1JNQVRJVkUgSU5GT1JNQVRJT04KCiAgSW5mb3JtYXRpb24gYWJvdXQgdGhpcyBmaWxlIGNhbiBiZSBmb3VuZCBpbiB0aGUgbGF0ZXN0IHJldmlzaW9uIG9mCgogIE9NQS1UUy1MV00yTV9CaW5hcnlBcHBEYXRhQ29udGFpbmVyLVYxXzBfMQoKICBUaGlzIGlzIGF2YWlsYWJsZSBhdCBodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvCgogIFNlbmQgY29tbWVudHMgdG8gaHR0cHM6Ly9naXRodWIuY29tL09wZW5Nb2JpbGVBbGxpYW5jZS9PTUFfTHdNMk1fZm9yX0RldmVsb3BlcnMvaXNzdWVzCgpDSEFOR0UgSElTVE9SWQoKMTUwNjIwMTggU3RhdHVzIGNoYW5nZWQgdG8gQXBwcm92ZWQgYnkgRE0sIERvYyBSZWYgIyBPTUEtRE0mU0UtMjAxOC0wMDYxLUlOUF9MV00yTV9BUFBEQVRBX1YxXzBfRVJQX2Zvcl9maW5hbF9BcHByb3ZhbAoyMTAyMjAxOSBTdGF0dXMgY2hhbmdlZCB0byBBcHByb3ZlZCBieSBJUFNPLCBEb2MgUmVmICMgT01BLUlQU08tMjAxOS0wMDI1LUlOUF9Md00yTV9PYmplY3RfQXBwX0RhdGFfQ29udGFpbmVyXzFfMF8xX2Zvcl9GaW5hbF9BcHByb3ZhbAoKTEVHQUwgRElTQ0xBSU1FUgoKQ29weXJpZ2h0IDIwMTkgT3BlbiBNb2JpbGUgQWxsaWFuY2UuCgpSZWRpc3RyaWJ1dGlvbiBhbmQgdXNlIGluIHNvdXJjZSBhbmQgYmluYXJ5IGZvcm1zLCB3aXRoIG9yIHdpdGhvdXQKbW9kaWZpY2F0aW9uLCBhcmUgcGVybWl0dGVkIHByb3ZpZGVkIHRoYXQgdGhlIGZvbGxvd2luZyBjb25kaXRpb25zCmFyZSBtZXQ6CgoxLiBSZWRpc3RyaWJ1dGlvbnMgb2Ygc291cmNlIGNvZGUgbXVzdCByZXRhaW4gdGhlIGFib3ZlIGNvcHlyaWdodApub3RpY2UsIHRoaXMgbGlzdCBvZiBjb25kaXRpb25zIGFuZCB0aGUgZm9sbG93aW5nIGRpc2NsYWltZXIuCjIuIFJlZGlzdHJpYnV0aW9ucyBpbiBiaW5hcnkgZm9ybSBtdXN0IHJlcHJvZHVjZSB0aGUgYWJvdmUgY29weXJpZ2h0Cm5vdGljZSwgdGhpcyBsaXN0IG9mIGNvbmRpdGlvbnMgYW5kIHRoZSBmb2xsb3dpbmcgZGlzY2xhaW1lciBpbiB0aGUKZG9jdW1lbnRhdGlvbiBhbmQvb3Igb3RoZXIgbWF0ZXJpYWxzIHByb3ZpZGVkIHdpdGggdGhlIGRpc3RyaWJ1dGlvbi4KMy4gTmVpdGhlciB0aGUgbmFtZSBvZiB0aGUgY29weXJpZ2h0IGhvbGRlciBub3IgdGhlIG5hbWVzIG9mIGl0cwpjb250cmlidXRvcnMgbWF5IGJlIHVzZWQgdG8gZW5kb3JzZSBvciBwcm9tb3RlIHByb2R1Y3RzIGRlcml2ZWQKZnJvbSB0aGlzIHNvZnR3YXJlIHdpdGhvdXQgc3BlY2lmaWMgcHJpb3Igd3JpdHRlbiBwZXJtaXNzaW9uLgoKVEhJUyBTT0ZUV0FSRSBJUyBQUk9WSURFRCBCWSBUSEUgQ09QWVJJR0hUIEhPTERFUlMgQU5EIENPTlRSSUJVVE9SUwoiQVMgSVMiIEFORCBBTlkgRVhQUkVTUyBPUiBJTVBMSUVEIFdBUlJBTlRJRVMsIElOQ0xVRElORywgQlVUIE5PVApMSU1JVEVEIFRPLCBUSEUgSU1QTElFRCBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSBBTkQgRklUTkVTUwpGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQVJFIERJU0NMQUlNRUQuIElOIE5PIEVWRU5UIFNIQUxMIFRIRQpDT1BZUklHSFQgSE9MREVSIE9SIENPTlRSSUJVVE9SUyBCRSBMSUFCTEUgRk9SIEFOWSBESVJFQ1QsIElORElSRUNULApJTkNJREVOVEFMLCBTUEVDSUFMLCBFWEVNUExBUlksIE9SIENPTlNFUVVFTlRJQUwgREFNQUdFUyAoSU5DTFVESU5HLApCVVQgTk9UIExJTUlURUQgVE8sIFBST0NVUkVNRU5UIE9GIFNVQlNUSVRVVEUgR09PRFMgT1IgU0VSVklDRVM7CkxPU1MgT0YgVVNFLCBEQVRBLCBPUiBQUk9GSVRTOyBPUiBCVVNJTkVTUyBJTlRFUlJVUFRJT04pIEhPV0VWRVIKQ0FVU0VEIEFORCBPTiBBTlkgVEhFT1JZIE9GIExJQUJJTElUWSwgV0hFVEhFUiBJTiBDT05UUkFDVCwgU1RSSUNUCkxJQUJJTElUWSwgT1IgVE9SVCAoSU5DTFVESU5HIE5FR0xJR0VOQ0UgT1IgT1RIRVJXSVNFKSBBUklTSU5HIElOCkFOWSBXQVkgT1VUIE9GIFRIRSBVU0UgT0YgVEhJUyBTT0ZUV0FSRSwgRVZFTiBJRiBBRFZJU0VEIE9GIFRIRQpQT1NTSUJJTElUWSBPRiBTVUNIIERBTUFHRS4KClRoZSBhYm92ZSBsaWNlbnNlIGlzIHVzZWQgYXMgYSBsaWNlbnNlIHVuZGVyIGNvcHlyaWdodCBvbmx5LiBQbGVhc2UKcmVmZXJlbmNlIHRoZSBPTUEgSVBSIFBvbGljeSBmb3IgcGF0ZW50IGxpY2Vuc2luZyB0ZXJtczoKaHR0cHM6Ly93d3cub21hc3BlY3dvcmtzLm9yZy9hYm91dC9pbnRlbGxlY3R1YWwtcHJvcGVydHktcmlnaHRzLwoKLS0+CjxMV00yTSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6bm9OYW1lc3BhY2VTY2hlbWFMb2NhdGlvbj0iaHR0cDovL29wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS54c2QiPgoJPE9iamVjdCBPYmplY3RUeXBlPSJNT0RlZmluaXRpb24iPgoJCTxOYW1lPkJpbmFyeUFwcERhdGFDb250YWluZXI8L05hbWU+CgkJPERlc2NyaXB0aW9uMT48IVtDREFUQVtUaGlzIEx3TTJNIE9iamVjdHMgcHJvdmlkZXMgdGhlIGFwcGxpY2F0aW9uIHNlcnZpY2UgZGF0YSByZWxhdGVkIHRvIGEgTHdNMk0gU2VydmVyLCBlZy4gV2F0ZXIgbWV0ZXIgZGF0YS4gClRoZXJlIGFyZSBzZXZlcmFsIG1ldGhvZHMgdG8gY3JlYXRlIGluc3RhbmNlIHRvIGluZGljYXRlIHRoZSBtZXNzYWdlIGRpcmVjdGlvbiBiYXNlZCBvbiB0aGUgbmVnb3RpYXRpb24gYmV0d2VlbiBBcHBsaWNhdGlvbiBhbmQgTHdNMk0uIFRoZSBDbGllbnQgYW5kIFNlcnZlciBzaG91bGQgbmVnb3RpYXRlIHRoZSBpbnN0YW5jZShzKSB1c2VkIHRvIGV4Y2hhbmdlIHRoZSBkYXRhLiBGb3IgZXhhbXBsZToKIC0gVXNpbmcgYSBzaW5nbGUgaW5zdGFuY2UgZm9yIGJvdGggZGlyZWN0aW9ucyBjb21tdW5pY2F0aW9uLCBmcm9tIENsaWVudCB0byBTZXJ2ZXIgYW5kIGZyb20gU2VydmVyIHRvIENsaWVudC4KIC0gVXNpbmcgYW4gaW5zdGFuY2UgZm9yIGNvbW11bmljYXRpb24gZnJvbSBDbGllbnQgdG8gU2VydmVyIGFuZCBhbm90aGVyIG9uZSBmb3IgY29tbXVuaWNhdGlvbiBmcm9tIFNlcnZlciB0byBDbGllbnQKIC0gVXNpbmcgc2V2ZXJhbCBpbnN0YW5jZXMKXV0+PC9EZXNjcmlwdGlvbjE+CgkJPE9iamVjdElEPjE5PC9PYmplY3RJRD4KCQk8T2JqZWN0VVJOPnVybjpvbWE6bHdtMm06b21hOjE5PC9PYmplY3RVUk4+CgkJPExXTTJNVmVyc2lvbj4xLjA8L0xXTTJNVmVyc2lvbj4KCQk8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CgkJPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJPFJlc291cmNlcz4KCQkJPEl0ZW0gSUQ9IjAiPjxOYW1lPkRhdGE8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5NdWx0aXBsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+T3BhcXVlPC9UeXBlPgoJCQkJPFJhbmdlRW51bWVyYXRpb24gLz4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgYXBwbGljYXRpb24gZGF0YSBjb250ZW50Ll1dPjwvRGVzY3JpcHRpb24+CgkJCTwvSXRlbT4KCQkJPEl0ZW0gSUQ9IjEiPjxOYW1lPkRhdGEgUHJpb3JpdHk8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgoJCQkJPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+SW50ZWdlcjwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjEgYnl0ZXM8L1JhbmdlRW51bWVyYXRpb24+CgkJCQk8VW5pdHMgLz4KCQkJCTxEZXNjcmlwdGlvbj48IVtDREFUQVtJbmRpY2F0ZXMgdGhlIEFwcGxpY2F0aW9uIGRhdGEgcHJpb3JpdHk6CjA6SW1tZWRpYXRlCjE6QmVzdEVmZm9ydAoyOkxhdGVzdAozLTEwMDogUmVzZXJ2ZWQgZm9yIGZ1dHVyZSB1c2UuCjEwMS0yNTQ6IFByb3ByaWV0YXJ5IG1vZGUuXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iMiI+PE5hbWU+RGF0YSBDcmVhdGlvbiBUaW1lPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlRpbWU8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbiAvPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBEYXRhIGluc3RhbmNlIGNyZWF0aW9uIHRpbWVzdGFtcC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSIzIj48TmFtZT5EYXRhIERlc2NyaXB0aW9uPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlN0cmluZzwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjMyIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkYXRhIGRlc2NyaXB0aW9uLgplLmcuICJtZXRlciByZWFkaW5nIi5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSI0Ij48TmFtZT5EYXRhIEZvcm1hdDwvTmFtZT4KCQkJCTxPcGVyYXRpb25zPlJXPC9PcGVyYXRpb25zPgoJCQkJPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJCQk8VHlwZT5TdHJpbmc8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4zMiBieXRlczwvUmFuZ2VFbnVtZXJhdGlvbj4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgZm9ybWF0IG9mIHRoZSBBcHBsaWNhdGlvbiBEYXRhLgplLmcuIFlHLU1ldGVyLVdhdGVyLVJlYWRpbmcKVVRGOC1zdHJpbmcKXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iNSI+PE5hbWU+QXBwIElEPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPkludGVnZXI8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4yIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkZXN0aW5hdGlvbiBBcHBsaWNhdGlvbiBJRC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+PC9SZXNvdXJjZXM+CgkJPERlc2NyaXB0aW9uMj48IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMj4KCTwvT2JqZWN0Pgo8L0xXTTJNPgo=";
private IdComparator idComparator = new IdComparator<>();
@@ -146,7 +148,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
@Test
public void testSaveResourceWithMaxSumDataSizeOutOfLimit() throws Exception {
loginSysAdmin();
- long limit = 1;
+ long limit = 4;
EntityInfo defaultTenantProfileInfo = doGet("/api/tenantProfileInfo/default", EntityInfo.class);
TenantProfile defaultTenantProfile = doGet("/api/tenantProfile/" + defaultTenantProfileInfo.getId().getId().toString(), TenantProfile.class);
defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxResourcesInBytes(limit).build());
@@ -158,7 +160,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
createResource("test", DEFAULT_FILE_NAME);
- assertEquals(1, resourceService.sumDataSizeByTenantId(tenantId));
+ assertEquals(4, resourceService.sumDataSizeByTenantId(tenantId));
try {
assertThatThrownBy(() -> createResource("test1", 1 + DEFAULT_FILE_NAME))
@@ -176,16 +178,12 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
assertEquals(0, resourceService.sumDataSizeByTenantId(tenantId));
createResource("test", DEFAULT_FILE_NAME);
- assertEquals(1, resourceService.sumDataSizeByTenantId(tenantId));
+ assertEquals(4, resourceService.sumDataSizeByTenantId(tenantId));
- int maxSumDataSize = 8;
-
- for (int i = 2; i <= maxSumDataSize; i++) {
+ for (int i = 2; i < 4; i++) {
createResource("test" + i, i + DEFAULT_FILE_NAME);
- assertEquals(i, resourceService.sumDataSizeByTenantId(tenantId));
+ assertEquals(i*4, resourceService.sumDataSizeByTenantId(tenantId));
}
-
- assertEquals(maxSumDataSize, resourceService.sumDataSizeByTenantId(tenantId));
}
private TbResource createResource(String title, String filename) throws Exception {
@@ -194,7 +192,8 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resource.setTitle(title);
resource.setResourceType(ResourceType.JKS);
resource.setFileName(filename);
- resource.setData("1");
+ byte[] b = new byte[1];
+ resource.setData(Base64.getEncoder().encodeToString(b));
return resourceService.save(resource);
}
@@ -205,7 +204,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My first resource");
resource.setFileName(DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
TbResource savedResource = resourceService.save(resource);
@@ -253,7 +252,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My resource");
resource.setFileName(DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
TbResource savedResource = resourceService.save(resource);
assertEquals(TenantId.SYS_TENANT_ID, savedResource.getTenantId());
@@ -268,7 +267,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My resource");
resource.setFileName(DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
TbResource savedResource = resourceService.save(resource);
@@ -277,7 +276,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My resource");
resource.setFileName(DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
try {
Assertions.assertThrows(DataValidationException.class, () -> {
@@ -294,7 +293,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resource.setTenantId(tenantId);
resource.setResourceType(ResourceType.JKS);
resource.setFileName(DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
Assertions.assertThrows(DataValidationException.class, () -> {
resourceService.save(resource);
});
@@ -307,7 +306,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My resource");
resource.setFileName(DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
Assertions.assertThrows(DataValidationException.class, () -> {
resourceService.save(resource);
});
@@ -334,7 +333,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My resource");
resource.setFileName(DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
TbResource savedResource = resourceService.save(resource);
TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId());
@@ -350,7 +349,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resource.setTenantId(tenantId);
resource.setTitle("My resource");
resource.setFileName(DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
TbResource savedResource = resourceService.save(resource);
TbResource foundResource = resourceService.getResource(tenantId, savedResource.getResourceType(), savedResource.getResourceKey());
@@ -365,7 +364,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My resource");
resource.setFileName(DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
TbResource savedResource = resourceService.save(resource);
TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId());
@@ -391,7 +390,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resource.setTitle("Resource" + i);
resource.setResourceType(ResourceType.JKS);
resource.setFileName(i + DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
resources.add(new TbResourceInfo(resourceService.save(resource)));
}
@@ -399,7 +398,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
PageLink pageLink = new PageLink(16);
PageData pageData;
do {
- pageData = resourceService.findTenantResourcesByTenantId(tenantId, pageLink);
+ TbResourceInfoFilter filter = TbResourceInfoFilter.builder()
+ .tenantId(tenantId)
+ .build();
+ pageData = resourceService.findTenantResourcesByTenantId(filter, pageLink);
loadedResources.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@@ -414,7 +416,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resourceService.deleteResourcesByTenantId(tenantId);
pageLink = new PageLink(31);
- pageData = resourceService.findTenantResourcesByTenantId(tenantId, pageLink);
+ TbResourceInfoFilter filter = TbResourceInfoFilter.builder()
+ .tenantId(tenantId)
+ .build();
+ pageData = resourceService.findTenantResourcesByTenantId(filter, pageLink);
Assert.assertFalse(pageData.hasNext());
Assert.assertTrue(pageData.getData().isEmpty());
@@ -439,7 +444,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resource.setTitle("System Resource" + i);
resource.setResourceType(ResourceType.JKS);
resource.setFileName(i + DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
TbResourceInfo tbResourceInfo = new TbResourceInfo(resourceService.save(resource));
if (i >= 50) {
resources.add(tbResourceInfo);
@@ -452,7 +457,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resource.setTitle("Tenant Resource" + i);
resource.setResourceType(ResourceType.JKS);
resource.setFileName(i + DEFAULT_FILE_NAME);
- resource.setData("Test Data");
+ resource.setData(TEST_DATA);
resources.add(new TbResourceInfo(resourceService.save(resource)));
}
@@ -460,7 +465,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
PageLink pageLink = new PageLink(10);
PageData pageData;
do {
- pageData = resourceService.findAllTenantResourcesByTenantId(tenantId, pageLink);
+ TbResourceInfoFilter filter = TbResourceInfoFilter.builder()
+ .tenantId(tenantId)
+ .build();
+ pageData = resourceService.findAllTenantResourcesByTenantId(filter, pageLink);
loadedResources.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@@ -475,14 +483,20 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
resourceService.deleteResourcesByTenantId(tenantId);
pageLink = new PageLink(100);
- pageData = resourceService.findAllTenantResourcesByTenantId(tenantId, pageLink);
+ TbResourceInfoFilter filter = TbResourceInfoFilter.builder()
+ .tenantId(tenantId)
+ .build();
+ pageData = resourceService.findAllTenantResourcesByTenantId(filter, pageLink);
Assert.assertFalse(pageData.hasNext());
assertEquals(pageData.getData().size(), 100);
resourceService.deleteResourcesByTenantId(TenantId.SYS_TENANT_ID);
pageLink = new PageLink(100);
- pageData = resourceService.findAllTenantResourcesByTenantId(TenantId.SYS_TENANT_ID, pageLink);
+ filter = TbResourceInfoFilter.builder()
+ .tenantId(TenantId.SYS_TENANT_ID)
+ .build();
+ pageData = resourceService.findAllTenantResourcesByTenantId(filter, pageLink);
Assert.assertFalse(pageData.hasNext());
Assert.assertTrue(pageData.getData().isEmpty());
diff --git a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java
index 8245284af6..52f2ec5c9d 100644
--- a/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java
@@ -29,12 +29,11 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.TsValue;
+import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceService;
-import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.discovery.PartitionService;
-import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import java.util.Map;
import java.util.UUID;
@@ -42,6 +41,7 @@ import java.util.UUID;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.BDDMockito.willReturn;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -69,7 +69,7 @@ public class DefaultDeviceStateServiceTest {
@Before
public void setUp() {
- service = spy(new DefaultDeviceStateService(deviceService, attributesService, tsService, clusterService, partitionService, null, null, null));
+ service = spy(new DefaultDeviceStateService(deviceService, attributesService, tsService, clusterService, partitionService, null, null, null, mock(NotificationRuleProcessor.class)));
}
@Test
diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java
index aa15b1da4c..39b61c6c47 100644
--- a/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java
@@ -23,6 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.debug.TbMsgGeneratorNode;
import org.thingsboard.rule.engine.debug.TbMsgGeneratorNodeConfiguration;
+import org.thingsboard.rule.engine.metadata.TbGetAttributesNode;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
@@ -86,6 +87,7 @@ import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.UUID;
+import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
@@ -333,6 +335,7 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest
RuleNode ruleNode2 = new RuleNode();
ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+ ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
@@ -341,7 +344,7 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest
metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2));
metaData.setFirstNodeIndex(0);
metaData.addConnectionInfo(0, 1, "Success");
- ruleChainService.saveRuleChainMetaData(tenantId, metaData);
+ ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity());
return ruleChainService.findRuleChainById(tenantId, ruleChain.getId());
}
@@ -361,6 +364,7 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest
RuleNode ruleNode1 = new RuleNode();
ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+ ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
@@ -369,6 +373,7 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest
RuleNode ruleNode2 = new RuleNode();
ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+ ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
@@ -377,7 +382,7 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest
metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2));
metaData.setFirstNodeIndex(0);
metaData.addConnectionInfo(0, 1, "Success");
- ruleChainService.saveRuleChainMetaData(tenantId, metaData);
+ ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity());
return ruleChainService.findRuleChainById(tenantId, ruleChain.getId());
}
diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/CoapTestCallback.java b/application/src/test/java/org/thingsboard/server/transport/coap/CoapTestCallback.java
index 2dda86d7a4..07eadd731d 100644
--- a/application/src/test/java/org/thingsboard/server/transport/coap/CoapTestCallback.java
+++ b/application/src/test/java/org/thingsboard/server/transport/coap/CoapTestCallback.java
@@ -21,25 +21,14 @@ import org.eclipse.californium.core.CoapHandler;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.coap.CoAP;
-import java.util.concurrent.CountDownLatch;
-
@Slf4j
@Data
public class CoapTestCallback implements CoapHandler {
- protected final CountDownLatch latch;
protected Integer observe;
protected byte[] payloadBytes;
protected CoAP.ResponseCode responseCode;
- public CoapTestCallback() {
- this.latch = new CountDownLatch(1);
- }
-
- public CoapTestCallback(int subscribeCount) {
- this.latch = new CountDownLatch(subscribeCount);
- }
-
public Integer getObserve() {
return observe;
}
@@ -57,7 +46,6 @@ public class CoapTestCallback implements CoapHandler {
observe = response.getOptions().getObserve();
payloadBytes = response.getPayload();
responseCode = response.getCode();
- latch.countDown();
}
@Override
diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java
index 697779c178..03ef6f9503 100644
--- a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java
@@ -232,7 +232,7 @@ public abstract class AbstractCoapAttributesIntegrationTest extends AbstractCoap
}
client = new CoapTestClient(accessToken, FeatureType.ATTRIBUTES);
- CoapTestCallback callbackCoap = new CoapTestCallback(1);
+ CoapTestCallback callbackCoap = new CoapTestCallback();
CoapObserveRelation observeRelation = client.getObserveRelation(callbackCoap);
String awaitAlias = "await Json Test Subscribe To AttributesUpdates (client.getObserveRelation)";
@@ -279,7 +279,7 @@ public abstract class AbstractCoapAttributesIntegrationTest extends AbstractCoap
}
client = new CoapTestClient(accessToken, FeatureType.ATTRIBUTES);
- CoapTestCallback callbackCoap = new CoapTestCallback(1);
+ CoapTestCallback callbackCoap = new CoapTestCallback();
String awaitAlias = "await Proto Test Subscribe To Attributes Updates (add attributes)";
CoapObserveRelation observeRelation = client.getObserveRelation(callbackCoap);
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 a2b44b6e58..445ac51ccd 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
@@ -41,7 +41,6 @@ import org.thingsboard.server.transport.coap.AbstractCoapIntegrationTest;
import org.thingsboard.server.transport.coap.CoapTestCallback;
import org.thingsboard.server.transport.coap.CoapTestClient;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
@@ -74,17 +73,16 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC
protected void processOneWayRpcTest(boolean protobuf) throws Exception {
client = new CoapTestClient(accessToken, FeatureType.RPC);
- CoapTestCallback callbackCoap = new TestCoapCallbackForRPC(client, 1, true, protobuf);
+ CoapTestCallback callbackCoap = new TestCoapCallbackForRPC(client, true, protobuf);
CoapObserveRelation observeRelation = client.getObserveRelation(callbackCoap);
String awaitAlias = "await One Way Rpc (client.getObserveRelation)";
await(awaitAlias)
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> CoAP.ResponseCode.VALID.equals(callbackCoap.getResponseCode()) &&
- callbackCoap.getObserve() != null &&
- 0 == callbackCoap.getObserve().intValue());
+ callbackCoap.getObserve() != null && 0 == callbackCoap.getObserve());
validateCurrentStateNotification(callbackCoap);
- int expectedObserveCountAfterGpioRequest = callbackCoap.getObserve().intValue() + 1;
+ int expectedObserveAfterRpcProcessed = callbackCoap.getObserve() + 1;
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
String deviceId = savedDevice.getId().getId().toString();
String result = doPostAsync("/api/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk());
@@ -92,8 +90,7 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC
await(awaitAlias)
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) &&
- callbackCoap.getObserve() != null &&
- expectedObserveCountAfterGpioRequest == callbackCoap.getObserve().intValue());
+ callbackCoap.getObserve() != null && expectedObserveAfterRpcProcessed == callbackCoap.getObserve());
validateOneWayStateChangedNotification(callbackCoap, result);
observeRelation.proactiveCancel();
@@ -102,7 +99,7 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC
protected void processTwoWayRpcTest(String expectedResponseResult, boolean protobuf) throws Exception {
client = new CoapTestClient(accessToken, FeatureType.RPC);
- CoapTestCallback callbackCoap = new TestCoapCallbackForRPC(client, 1, false, protobuf);
+ CoapTestCallback callbackCoap = new TestCoapCallbackForRPC(client, false, protobuf);
CoapObserveRelation observeRelation = client.getObserveRelation(callbackCoap);
String awaitAlias = "await Two Way Rpc (client.getObserveRelation)";
@@ -110,29 +107,29 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> CoAP.ResponseCode.VALID.equals(callbackCoap.getResponseCode()) &&
callbackCoap.getObserve() != null &&
- 0 == callbackCoap.getObserve().intValue());
+ 0 == callbackCoap.getObserve());
validateCurrentStateNotification(callbackCoap);
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}";
String deviceId = savedDevice.getId().getId().toString();
- int expectedObserveCountAfterGpioRequest1 = callbackCoap.getObserve().intValue() + 1;
+ int expectedObserveCountAfterGpioRequest1 = callbackCoap.getObserve() + 1;
String actualResult = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk());
awaitAlias = "await Two Way Rpc (setGpio(method, params, value) first";
await(awaitAlias)
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) &&
callbackCoap.getObserve() != null &&
- expectedObserveCountAfterGpioRequest1 == callbackCoap.getObserve().intValue());
+ expectedObserveCountAfterGpioRequest1 == callbackCoap.getObserve());
validateTwoWayStateChangedNotification(callbackCoap, expectedResponseResult, actualResult);
- int expectedObserveCountAfterGpioRequest2 = callbackCoap.getObserve().intValue() + 1;
+ int expectedObserveCountAfterGpioRequest2 = callbackCoap.getObserve() + 1;
actualResult = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk());
awaitAlias = "await Two Way Rpc (setGpio(method, params, value) first";
await(awaitAlias)
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) &&
callbackCoap.getObserve() != null &&
- expectedObserveCountAfterGpioRequest2 == callbackCoap.getObserve().intValue());
+ expectedObserveCountAfterGpioRequest2 == callbackCoap.getObserve());
validateTwoWayStateChangedNotification(callbackCoap, expectedResponseResult, actualResult);
@@ -140,24 +137,24 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC
assertTrue(observeRelation.isCanceled());
}
- protected void processOnLoadResponse(CoapResponse response, CoapTestClient client, Integer observe, CountDownLatch latch) {
+ protected void processOnLoadResponse(CoapResponse response, CoapTestClient client) {
JsonNode responseJson = JacksonUtil.fromBytes(response.getPayload());
- client.setURI(CoapTestClient.getFeatureTokenUrl(accessToken, FeatureType.RPC, responseJson.get("id").asInt()));
+ int requestId = responseJson.get("id").asInt();
+ client.setURI(CoapTestClient.getFeatureTokenUrl(accessToken, FeatureType.RPC, requestId));
client.postMethod(new CoapHandler() {
@Override
public void onLoad(CoapResponse response) {
- log.warn("Command Response Ack: {}, {}", response.getCode(), response.getResponseText());
- latch.countDown();
+ log.warn("RPC {} command response ack: {}", requestId, response.getCode());
}
@Override
public void onError() {
- log.warn("Command Response Ack Error, No connect");
+ log.warn("RPC {} command response ack error, no connect", requestId);
}
}, DEVICE_RESPONSE, MediaTypeRegistry.APPLICATION_JSON);
}
- protected void processOnLoadProtoResponse(CoapResponse response, CoapTestClient client, Integer observe, CountDownLatch latch) {
+ protected void processOnLoadProtoResponse(CoapResponse response, CoapTestClient client) {
ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = getProtoTransportPayloadConfiguration();
ProtoFileElement rpcRequestProtoFileElement = DynamicProtoUtils.getProtoFileElement(protoTransportPayloadConfiguration.getDeviceRpcRequestProtoSchema());
DynamicSchema rpcRequestProtoSchema = DynamicProtoUtils.getDynamicSchema(rpcRequestProtoFileElement, ProtoTransportPayloadConfiguration.RPC_REQUEST_PROTO_SCHEMA);
@@ -180,13 +177,12 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC
client.postMethod(new CoapHandler() {
@Override
public void onLoad(CoapResponse response) {
- log.warn("Command Response Ack: {}", response.getCode());
- latch.countDown();
+ log.warn("RPC {} command response ack: {}", requestId, response.getCode());
}
@Override
public void onError() {
- log.warn("Command Response Ack Error, No connect");
+ log.warn("RPC {} command response ack error, no connect", requestId);
}
}, rpcResponseMsg.toByteArray(), MediaTypeRegistry.APPLICATION_JSON);
} catch (InvalidProtocolBufferException e) {
@@ -226,8 +222,7 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC
private final boolean isOneWayRpc;
private final boolean protobuf;
- TestCoapCallbackForRPC(CoapTestClient client, int subscribeCount, boolean isOneWayRpc, boolean protobuf) {
- super(subscribeCount);
+ TestCoapCallbackForRPC(CoapTestClient client, boolean isOneWayRpc, boolean protobuf) {
this.client = client;
this.isOneWayRpc = isOneWayRpc;
this.protobuf = protobuf;
@@ -241,12 +236,10 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC
if (observe != null) {
if (!isOneWayRpc && observe > 0) {
if (!protobuf){
- processOnLoadResponse(response, client, observe, latch);
+ processOnLoadResponse(response, client);
} else {
- processOnLoadProtoResponse(response, client, observe, latch);
+ processOnLoadProtoResponse(response, client);
}
- } else {
- latch.countDown();
}
}
}
diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestCallback.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestCallback.java
index 3240647956..208189ac4e 100644
--- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestCallback.java
+++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestCallback.java
@@ -32,7 +32,6 @@ public class MqttTestCallback implements MqttCallback {
protected final CountDownLatch deliveryLatch;
protected int qoS;
protected byte[] payloadBytes;
- protected String awaitSubTopic;
protected boolean pubAckReceived;
public MqttTestCallback() {
@@ -45,12 +44,6 @@ public class MqttTestCallback implements MqttCallback {
this.deliveryLatch = new CountDownLatch(1);
}
- public MqttTestCallback(String awaitSubTopic) {
- this.subscribeLatch = new CountDownLatch(1);
- this.deliveryLatch = new CountDownLatch(1);
- this.awaitSubTopic = awaitSubTopic;
- }
-
@Override
public void connectionLost(Throwable throwable) {
log.warn("connectionLost: ", throwable);
@@ -59,23 +52,10 @@ public class MqttTestCallback implements MqttCallback {
@Override
public void messageArrived(String requestTopic, MqttMessage mqttMessage) {
- if (awaitSubTopic == null) {
- log.warn("messageArrived on topic: {}", requestTopic);
- qoS = mqttMessage.getQos();
- payloadBytes = mqttMessage.getPayload();
- subscribeLatch.countDown();
- } else {
- messageArrivedOnAwaitSubTopic(requestTopic, mqttMessage);
- }
- }
-
- protected void messageArrivedOnAwaitSubTopic(String requestTopic, MqttMessage mqttMessage) {
- log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic);
- if (awaitSubTopic.equals(requestTopic)) {
- qoS = mqttMessage.getQos();
- payloadBytes = mqttMessage.getPayload();
- subscribeLatch.countDown();
- }
+ log.warn("messageArrived on topic: {}", requestTopic);
+ qoS = mqttMessage.getQos();
+ payloadBytes = mqttMessage.getPayload();
+ subscribeLatch.countDown();
}
@Override
diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestSubscribeOnTopicCallback.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestSubscribeOnTopicCallback.java
new file mode 100644
index 0000000000..0f3cc14629
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestSubscribeOnTopicCallback.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt.mqttv3;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+
+@Data
+@Slf4j
+@EqualsAndHashCode(callSuper = true)
+public class MqttTestSubscribeOnTopicCallback extends MqttTestCallback {
+
+ protected final String awaitSubTopic;
+
+ public MqttTestSubscribeOnTopicCallback(String awaitSubTopic) {
+ super();
+ this.awaitSubTopic = awaitSubTopic;
+ }
+
+ @Override
+ public void messageArrived(String requestTopic, MqttMessage mqttMessage) {
+ log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic);
+ if (awaitSubTopic.equals(requestTopic)) {
+ qoS = mqttMessage.getQos();
+ payloadBytes = mqttMessage.getPayload();
+ subscribeLatch.countDown();
+ }
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java
index 941e97e500..71d8809e38 100644
--- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java
@@ -45,6 +45,7 @@ import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback;
+import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestSubscribeOnTopicCallback;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient;
import java.util.ArrayList;
@@ -358,7 +359,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
String update = getWsClient().waitForUpdate();
assertThat(update).as("ws update received").isNotBlank();
- MqttTestCallback callback = new MqttTestCallback(attrSubTopic.replace("+", "1"));
+ MqttTestCallback callback = new MqttTestSubscribeOnTopicCallback(attrSubTopic.replace("+", "1"));
client.setCallback(callback);
String payloadStr = "{\"clientKeys\":\"" + clientKeysStr + "\", \"sharedKeys\":\"" + sharedKeysStr + "\"}";
client.publishAndWait(attrReqTopicPrefix + "1", payloadStr.getBytes());
@@ -389,7 +390,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
String update = getWsClient().waitForUpdate();
assertThat(update).as("ws update received").isNotBlank();
- MqttTestCallback callback = new MqttTestCallback(attrSubTopic.replace("+", "1"));
+ MqttTestCallback callback = new MqttTestSubscribeOnTopicCallback(attrSubTopic.replace("+", "1"));
client.setCallback(callback);
TransportApiProtos.AttributesRequest.Builder attributesRequestBuilder = TransportApiProtos.AttributesRequest.newBuilder();
attributesRequestBuilder.setClientKeys(clientKeysStr);
@@ -448,14 +449,14 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
client.subscribeAndWait(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, MqttQoS.AT_LEAST_ONCE);
//RequestAttributes does not make any subscriptions in device actor
- MqttTestCallback clientAttributesCallback = new MqttTestCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC);
+ MqttTestCallback clientAttributesCallback = new MqttTestSubscribeOnTopicCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC);
client.setCallback(clientAttributesCallback);
String csKeysStr = "[\"clientStr\", \"clientBool\", \"clientDbl\", \"clientLong\", \"clientJson\"]";
String csRequestPayloadStr = "{\"id\": 1, \"device\": \"" + deviceName + "\", \"client\": true, \"keys\": " + csKeysStr + "}";
client.publishAndWait(GATEWAY_ATTRIBUTES_REQUEST_TOPIC, csRequestPayloadStr.getBytes());
validateJsonResponseGateway(clientAttributesCallback, deviceName, CLIENT_ATTRIBUTES_PAYLOAD);
- MqttTestCallback sharedAttributesCallback = new MqttTestCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC);
+ MqttTestCallback sharedAttributesCallback = new MqttTestSubscribeOnTopicCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC);
client.setCallback(sharedAttributesCallback);
String shKeysStr = "[\"sharedStr\", \"sharedBool\", \"sharedDbl\", \"sharedLong\", \"sharedJson\"]";
String shRequestPayloadStr = "{\"id\": 1, \"device\": \"" + deviceName + "\", \"client\": false, \"keys\": " + shKeysStr + "}";
@@ -502,13 +503,13 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
client.subscribeAndWait(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, MqttQoS.AT_LEAST_ONCE);
awaitForDeviceActorToReceiveSubscription(device.getId(), FeatureType.ATTRIBUTES, 1);
- MqttTestCallback clientAttributesCallback = new MqttTestCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC);
+ MqttTestCallback clientAttributesCallback = new MqttTestSubscribeOnTopicCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC);
client.setCallback(clientAttributesCallback);
TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(deviceName, clientKeysList, true);
client.publishAndWait(GATEWAY_ATTRIBUTES_REQUEST_TOPIC, gatewayAttributesRequestMsg.toByteArray());
validateProtoClientResponseGateway(clientAttributesCallback, deviceName);
- MqttTestCallback sharedAttributesCallback = new MqttTestCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC);
+ MqttTestCallback sharedAttributesCallback = new MqttTestSubscribeOnTopicCallback(GATEWAY_ATTRIBUTES_RESPONSE_TOPIC);
client.setCallback(sharedAttributesCallback);
gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(deviceName, sharedKeysList, false);
client.publishAndWait(GATEWAY_ATTRIBUTES_REQUEST_TOPIC, gatewayAttributesRequestMsg.toByteArray());
diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionJsonDeviceTest.java
index 9081baa420..de0a3d6f3c 100644
--- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionJsonDeviceTest.java
+++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionJsonDeviceTest.java
@@ -35,6 +35,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest;
import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback;
+import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestSubscribeOnTopicCallback;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient;
import java.util.concurrent.TimeUnit;
@@ -270,7 +271,7 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest {
String provisionRequestMsg = createTestProvisionMessage(deviceCredentials);
MqttTestClient client = new MqttTestClient();
client.connectAndWait("provision");
- MqttTestCallback onProvisionCallback = new MqttTestCallback(DEVICE_PROVISION_RESPONSE_TOPIC);
+ MqttTestCallback onProvisionCallback = new MqttTestSubscribeOnTopicCallback(DEVICE_PROVISION_RESPONSE_TOPIC);
client.setCallback(onProvisionCallback);
client.subscribe(DEVICE_PROVISION_RESPONSE_TOPIC, MqttQoS.AT_MOST_ONCE);
client.publishAndWait(DEVICE_PROVISION_REQUEST_TOPIC, provisionRequestMsg.getBytes());
diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionProtoDeviceTest.java
index fcea0f249b..c6d013ba20 100644
--- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionProtoDeviceTest.java
+++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionProtoDeviceTest.java
@@ -43,6 +43,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509Ce
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest;
import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback;
+import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestSubscribeOnTopicCallback;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient;
import java.util.concurrent.TimeUnit;
@@ -269,7 +270,7 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest {
protected byte[] createMqttClientAndPublish(byte[] provisionRequestMsg) throws Exception {
MqttTestClient client = new MqttTestClient();
client.connectAndWait("provision");
- MqttTestCallback onProvisionCallback = new MqttTestCallback(DEVICE_PROVISION_RESPONSE_TOPIC);
+ MqttTestCallback onProvisionCallback = new MqttTestSubscribeOnTopicCallback(DEVICE_PROVISION_RESPONSE_TOPIC);
client.setCallback(onProvisionCallback);
client.subscribe(DEVICE_PROVISION_RESPONSE_TOPIC, MqttQoS.AT_MOST_ONCE);
client.publishAndWait(DEVICE_PROVISION_REQUEST_TOPIC, provisionRequestMsg);
diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java
index 8537ee9fd8..e1f20a3d95 100644
--- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java
@@ -41,6 +41,7 @@ import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.gen.transport.TransportApiProtos;
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback;
+import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestSubscribeOnTopicCallback;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient;
import java.util.ArrayList;
@@ -81,7 +82,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
protected void processOneWayRpcTest(String rpcSubTopic) throws Exception {
MqttTestClient client = new MqttTestClient();
client.connectAndWait(accessToken);
- MqttTestCallback callback = new MqttTestCallback(rpcSubTopic.replace("+", "0"));
+ MqttTestCallback callback = new MqttTestSubscribeOnTopicCallback(rpcSubTopic.replace("+", "0"));
client.setCallback(callback);
subscribeAndWait(client, rpcSubTopic, savedDevice.getId(), FeatureType.RPC);
@@ -221,7 +222,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
);
assertNotNull(savedDevice);
- MqttTestCallback callback = new MqttTestCallback(GATEWAY_RPC_TOPIC);
+ MqttTestCallback callback = new MqttTestSubscribeOnTopicCallback(GATEWAY_RPC_TOPIC);
client.setCallback(callback);
subscribeAndCheckSubscription(client, GATEWAY_RPC_TOPIC, savedDevice.getId(), FeatureType.RPC);
@@ -320,7 +321,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
}
}
- protected class MqttTestRpcJsonCallback extends MqttTestCallback {
+ protected class MqttTestRpcJsonCallback extends MqttTestSubscribeOnTopicCallback {
private final MqttTestClient client;
@@ -330,7 +331,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
}
@Override
- protected void messageArrivedOnAwaitSubTopic(String requestTopic, MqttMessage mqttMessage) {
+ public void messageArrived(String requestTopic, MqttMessage mqttMessage) {
log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic);
if (awaitSubTopic.equals(requestTopic)) {
qoS = mqttMessage.getQos();
@@ -349,9 +350,10 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
subscribeLatch.countDown();
}
}
+
}
- protected class MqttTestRpcProtoCallback extends MqttTestCallback {
+ protected class MqttTestRpcProtoCallback extends MqttTestSubscribeOnTopicCallback {
private final MqttTestClient client;
@@ -361,7 +363,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
}
@Override
- protected void messageArrivedOnAwaitSubTopic(String requestTopic, MqttMessage mqttMessage) {
+ public void messageArrived(String requestTopic, MqttMessage mqttMessage) {
log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic);
if (awaitSubTopic.equals(requestTopic)) {
qoS = mqttMessage.getQos();
@@ -380,6 +382,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
subscribeLatch.countDown();
}
}
+
}
protected byte[] processProtoMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException {
@@ -446,7 +449,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM
@Override
public void messageArrived(String requestTopic, MqttMessage mqttMessage) {
- log.warn("messageArrived on topic: {}, awaitSubTopic: {}", requestTopic, awaitSubTopic);
+ log.warn("messageArrived on topic: {}", requestTopic);
expected.add(new String(mqttMessage.getPayload()));
String responseTopic = requestTopic.replace("request", "response");
qoS = mqttMessage.getQos();
diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties
index ad86ff736b..99055e0e5f 100644
--- a/application/src/test/resources/application-test.properties
+++ b/application/src/test/resources/application-test.properties
@@ -14,6 +14,7 @@ edges.enabled=false
edges.storage.no_read_records_sleep=500
edges.storage.sleep_between_batches=500
actors.rpc.sequential=true
+queue.rule-engine.stats.enabled=true
# Transports disabled to speed up the context init. Particular transport will be enabled with @TestPropertySource in respective tests
transport.http.enabled=false
diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml
index 953b7094a4..14db2eea7b 100644
--- a/application/src/test/resources/logback-test.xml
+++ b/application/src/test/resources/logback-test.xml
@@ -28,6 +28,12 @@
+
+
+
+
+
+
diff --git a/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java b/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java
index 587da8f3b1..ad1604f7b0 100644
--- a/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java
+++ b/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java
@@ -19,6 +19,7 @@ import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorError;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbActorStopReason;
@@ -69,9 +70,14 @@ public final class TbActorMailbox implements TbActorCtx {
}
}
} catch (Throwable t) {
- log.debug("[{}] Failed to init actor, attempt: {}", selfId, attempt, t);
+ InitFailureStrategy strategy;
int attemptIdx = attempt + 1;
- InitFailureStrategy strategy = actor.onInitFailure(attempt, t);
+ if (isUnrecoverable(t)) {
+ strategy = InitFailureStrategy.stop();
+ } else {
+ log.debug("[{}] Failed to init actor, attempt: {}", selfId, attempt, t);
+ strategy = actor.onInitFailure(attempt, t);
+ }
if (strategy.isStop() || (settings.getMaxActorInitAttempts() > 0 && attemptIdx > settings.getMaxActorInitAttempts())) {
log.info("[{}] Failed to init actor, attempt {}, going to stop attempts.", selfId, attempt, t);
stopReason = TbActorStopReason.INIT_FAILED;
@@ -88,6 +94,13 @@ public final class TbActorMailbox implements TbActorCtx {
}
}
+ private static boolean isUnrecoverable(Throwable t) {
+ if (t instanceof TbActorException && t.getCause() != null) {
+ t = t.getCause();
+ }
+ return t instanceof TbActorError && ((TbActorError) t).isUnrecoverable();
+ }
+
private void enqueue(TbActorMsg msg, boolean highPriority) {
if (!destroyInProgress.get()) {
if (highPriority) {
diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java
index 9c4c962dc2..36f3dddc3d 100644
--- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java
+++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java
@@ -26,14 +26,19 @@ import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.util.Assert;
+import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
import redis.clients.jedis.JedisPoolConfig;
import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
@Configuration
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@@ -41,6 +46,9 @@ import java.time.Duration;
@Data
public abstract class TBRedisCacheConfiguration {
+ private static final String COMMA = ",";
+ private static final String COLON = ":";
+
@Value("${redis.evictTtlInMs:60000}")
private int evictTtlInMs;
@@ -126,4 +134,19 @@ public abstract class TBRedisCacheConfiguration {
poolConfig.setBlockWhenExhausted(blockWhenExhausted);
return poolConfig;
}
+
+ protected List getNodes(String nodes) {
+ List result;
+ if (StringUtils.isBlank(nodes)) {
+ result = Collections.emptyList();
+ } else {
+ result = new ArrayList<>();
+ for (String hostPort : nodes.split(COMMA)) {
+ String host = hostPort.split(COLON)[0];
+ int port = Integer.parseInt(hostPort.split(COLON)[1]);
+ result.add(new RedisNode(host, port));
+ }
+ }
+ return result;
+ }
}
diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java
index dfc6ebd225..0a378103b0 100644
--- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java
+++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java
@@ -20,22 +20,13 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
-import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
-import org.thingsboard.server.common.data.StringUtils;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
@Configuration
@ConditionalOnMissingBean(TbCaffeineCacheConfiguration.class)
@ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "cluster")
public class TBRedisClusterConfiguration extends TBRedisCacheConfiguration {
- private static final String COMMA = ",";
- private static final String COLON = ":";
-
@Value("${redis.cluster.nodes:}")
private String clusterNodes;
@@ -59,19 +50,4 @@ public class TBRedisClusterConfiguration extends TBRedisCacheConfiguration {
return new JedisConnectionFactory(clusterConfiguration, buildPoolConfig());
}
}
-
- private List getNodes(String nodes) {
- List result;
- if (StringUtils.isBlank(nodes)) {
- result = Collections.emptyList();
- } else {
- result = new ArrayList<>();
- for (String hostPort : nodes.split(COMMA)) {
- String host = hostPort.split(COLON)[0];
- Integer port = Integer.valueOf(hostPort.split(COLON)[1]);
- result.add(new RedisNode(host, port));
- }
- }
- return result;
- }
-}
\ No newline at end of file
+}
diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisSentinelConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisSentinelConfiguration.java
new file mode 100644
index 0000000000..78cb445d82
--- /dev/null
+++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisSentinelConfiguration.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.cache;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisSentinelConfiguration;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+
+@Configuration
+@ConditionalOnMissingBean(TbCaffeineCacheConfiguration.class)
+@ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "sentinel")
+public class TBRedisSentinelConfiguration extends TBRedisCacheConfiguration {
+
+ @Value("${redis.sentinel.master:}")
+ private String master;
+
+ @Value("${redis.sentinel.sentinels:}")
+ private String sentinels;
+
+ @Value("${redis.sentinel.password:}")
+ private String sentinelPassword;
+
+ @Value("${redis.sentinel.useDefaultPoolConfig:true}")
+ private boolean useDefaultPoolConfig;
+
+ @Value("${redis.db:}")
+ private Integer database;
+
+ @Value("${redis.password:}")
+ private String password;
+
+ public JedisConnectionFactory loadFactory() {
+ RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration();
+ redisSentinelConfiguration.setMaster(master);
+ redisSentinelConfiguration.setSentinels(getNodes(sentinels));
+ redisSentinelConfiguration.setSentinelPassword(sentinelPassword);
+ redisSentinelConfiguration.setPassword(password);
+ redisSentinelConfiguration.setDatabase(database);
+ if (useDefaultPoolConfig) {
+ return new JedisConnectionFactory(redisSentinelConfiguration);
+ } else {
+ return new JedisConnectionFactory(redisSentinelConfiguration, buildPoolConfig());
+ }
+ }
+
+}
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
index dcb3a5232a..9055202f4f 100644
--- 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
@@ -26,7 +26,7 @@ public interface EdgeEventService {
ListenableFuture saveAsync(EdgeEvent edgeEvent);
- PageData findEdgeEvents(TenantId tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate);
+ PageData findEdgeEvents(TenantId tenantId, EdgeId edgeId, Long seqIdStart, Long seqIdEnd, TimePageLink pageLink);
/**
* Executes stored procedure to cleanup old edge events.
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationRuleService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationRuleService.java
index bd23f1c48d..d1b7513031 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationRuleService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationRuleService.java
@@ -19,7 +19,7 @@ import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java
index 46eb5ca54e..a5433915b3 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java
@@ -26,4 +26,6 @@ public interface NotificationSettingsService {
void createDefaultNotificationConfigs(TenantId tenantId);
+ void updateDefaultNotificationConfigs(TenantId tenantId);
+
}
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetService.java
index 05299c1675..bd442f6bb6 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetService.java
@@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
+import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@@ -40,6 +41,8 @@ public interface NotificationTargetService {
List findNotificationTargetsByTenantIdAndIds(TenantId tenantId, List ids);
+ List findNotificationTargetsByTenantIdAndUsersFilterType(TenantId tenantId, UsersFilterType filterType);
+
PageData findRecipientsForNotificationTarget(TenantId tenantId, CustomerId customerId, NotificationTargetId targetId, PageLink pageLink);
PageData findRecipientsForNotificationTargetConfig(TenantId tenantId, PlatformUsersNotificationTargetConfig targetConfig, PageLink pageLink);
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
index 5e05cdddc6..6f0e362209 100644
--- 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
@@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
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.TbResourceInfoFilter;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@@ -39,9 +40,9 @@ public interface ResourceService extends EntityDaoService {
ListenableFuture findResourceInfoByIdAsync(TenantId tenantId, TbResourceId resourceId);
- PageData findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink);
+ PageData findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink);
- PageData findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink);
+ PageData findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink);
List findTenantResourcesByResourceTypeAndObjectIds(TenantId tenantId, ResourceType lwm2mModel, String[] objectIds);
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 331fa2e5db..95cdc0e02d 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
@@ -35,6 +35,7 @@ import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.Collection;
import java.util.List;
+import java.util.function.Function;
/**
* Created by igor on 3/12/18.
@@ -45,7 +46,7 @@ public interface RuleChainService extends EntityDaoService {
boolean setRootRuleChain(TenantId tenantId, RuleChainId ruleChainId);
- RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData);
+ RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater);
RuleChainMetaData loadRuleChainMetaData(TenantId tenantId, RuleChainId ruleChainId);
@@ -75,7 +76,7 @@ public interface RuleChainService extends EntityDaoService {
RuleChainData exportTenantRuleChains(TenantId tenantId, PageLink pageLink) throws ThingsboardException;
- List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite);
+ List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite, Function ruleNodeUpdater);
RuleChain assignRuleChainToEdge(TenantId tenantId, RuleChainId ruleChainId, EdgeId edgeId);
@@ -99,6 +100,8 @@ public interface RuleChainService extends EntityDaoService {
PageData findAllRuleNodesByType(String type, PageLink pageLink);
+ PageData findAllRuleNodesByTypeAndVersionLessThan(String type, int version, PageLink pageLink);
+
RuleNode saveRuleNode(TenantId tenantId, RuleNode ruleNode);
void deleteRuleNodes(TenantId tenantId, RuleChainId ruleChainId);
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
index 186813f476..c2bc997235 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
@@ -42,6 +42,8 @@ public interface TimeseriesService {
ListenableFuture> findLatest(TenantId tenantId, EntityId entityId, Collection keys);
+ List findLatestSync(TenantId tenantId, EntityId entityId, Collection keys);
+
ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId);
ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry);
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java
index 31c87ecfde..3c2b9a4bc0 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java
@@ -15,6 +15,21 @@
*/
package org.thingsboard.server.common.data;
+import lombok.Getter;
+
public enum ResourceType {
- LWM2M_MODEL, JKS, PKCS_12
+ LWM2M_MODEL("application/xml", false),
+ JKS("application/x-java-keystore", false),
+ PKCS_12("application/x-pkcs12", false),
+ JS_MODULE("application/javascript", true);
+
+ @Getter
+ private final String mediaType;
+ @Getter
+ private final boolean customerAccess;
+
+ ResourceType(String mediaType, boolean customerAccess) {
+ this.mediaType = mediaType;
+ this.customerAccess = customerAccess;
+ }
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java
index 3f4aa11c1f..a7671f4327 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java
@@ -24,6 +24,9 @@ import java.util.Base64;
import static org.apache.commons.lang3.StringUtils.repeat;
public class StringUtils {
+
+ private static final int DEFAULT_TOKEN_LENGTH = 8;
+
public static final SecureRandom RANDOM = new SecureRandom();
public static final String EMPTY = "";
@@ -205,4 +208,8 @@ public class StringUtils {
return encoder.encodeToString(bytes);
}
+ public static String generateSafeToken() {
+ return generateSafeToken(DEFAULT_TOKEN_LENGTH);
+ }
+
}
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 c6aa878948..86800b3d54 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
@@ -74,6 +74,8 @@ public class TbResource extends TbResourceInfo {
builder.append(fileName);
builder.append(", data=");
builder.append(data);
+ builder.append(", hashCode=");
+ builder.append(getEtag());
builder.append("]");
return builder.toString();
}
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 f3e8f1bf8f..6f46002fcc 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
@@ -47,6 +47,8 @@ public class TbResourceInfo extends BaseData implements HasName, H
private String resourceKey;
@Schema(description = "Resource search text.", example = "19_1.0:binaryappdatacontainer", accessMode = Schema.AccessMode.READ_ONLY)
private String searchText;
+ @Schema(description = "Resource etag.", example = "33a64df551425fcc55e4d42a148795d9f25f89d4", accessMode = Schema.AccessMode.READ_ONLY)
+ private String etag;
public TbResourceInfo() {
super();
@@ -63,12 +65,13 @@ public class TbResourceInfo extends BaseData implements HasName, H
this.resourceType = resourceInfo.getResourceType();
this.resourceKey = resourceInfo.getResourceKey();
this.searchText = resourceInfo.getSearchText();
+ this.etag = resourceInfo.getEtag();
}
@Schema(description = "JSON object with the Resource Id. " +
"Specify this field to update the Resource. " +
"Referencing non-existing Resource Id will cause error. " +
- "Omit this field to create new Resource." )
+ "Omit this field to create new Resource.")
@Override
public TbResourceId getId() {
return super.getId();
@@ -106,6 +109,8 @@ public class TbResourceInfo extends BaseData implements HasName, H
builder.append(resourceType);
builder.append(", resourceKey=");
builder.append(resourceKey);
+ builder.append(", hashCode=");
+ builder.append(etag);
builder.append("]");
return builder.toString();
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java
new file mode 100644
index 0000000000..9057fc144b
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data;
+
+import lombok.Builder;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.TenantId;
+
+@Data
+@Builder
+public class TbResourceInfoFilter {
+
+ private TenantId tenantId;
+ private ResourceType resourceType;
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
index 222570e914..62fd440d05 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
@@ -152,7 +152,6 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha
}
public static AlarmStatus toStatus(boolean cleared, boolean acknowledged) {
-
if (cleared) {
return acknowledged ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK;
} else {
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
index 71c35f4bd8..3688f5c6c2 100644
--- 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
@@ -31,6 +31,7 @@ import java.util.UUID;
@ToString(callSuper = true)
public class EdgeEvent extends BaseData {
+ private long seqId;
private TenantId tenantId;
private EdgeId edgeId;
private EdgeEventActionType action;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java
index 7b11e86c9f..a5c24f2269 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@@ -28,6 +29,7 @@ public class NotificationId extends UUIDBased implements EntityId {
super(id);
}
+ @Schema(required = true, description = "string", example = "NOTIFICATION", allowableValues = "NOTIFICATION")
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java
index 3696aa0582..50c10138c5 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@@ -28,6 +29,7 @@ public class NotificationRequestId extends UUIDBased implements EntityId {
super(id);
}
+ @Schema(required = true, description = "string", example = "NOTIFICATION_REQUEST", allowableValues = "NOTIFICATION_REQUEST")
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_REQUEST;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java
index d9294b7fba..550c64e209 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@@ -28,6 +29,7 @@ public class NotificationRuleId extends UUIDBased implements EntityId {
super(id);
}
+ @Schema(required = true, description = "string", example = "NOTIFICATION_RULE", allowableValues = "NOTIFICATION_RULE")
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_RULE;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java
index 85d26ffde0..91bb25b89b 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@@ -28,6 +29,7 @@ public class NotificationTargetId extends UUIDBased implements EntityId {
super(id);
}
+ @Schema(required = true, description = "string", example = "NOTIFICATION_TARGET", allowableValues = "NOTIFICATION_TARGET")
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_TARGET;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java
index 6570d76c57..2709d3a2a5 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@@ -28,6 +29,7 @@ public class NotificationTemplateId extends UUIDBased implements EntityId {
super(id);
}
+ @Schema(required = true, description = "string", example = "NOTIFICATION_TEMPLATE", allowableValues = "NOTIFICATION_TEMPLATE")
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_TEMPLATE;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java
index 4ebc531c4b..6b6b37fb5b 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@@ -34,6 +35,7 @@ public class QueueId extends UUIDBased implements EntityId {
return new QueueId(UUID.fromString(queueId));
}
+ @Schema(required = true, description = "string", example = "QUEUE", allowableValues = "QUEUE")
@Override
public EntityType getEntityType() {
return EntityType.QUEUE;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
new file mode 100644
index 0000000000..e709ac596e
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.limit;
+
+import lombok.Getter;
+import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+public enum LimitedApi {
+
+ ENTITY_EXPORT(DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit, "entity version creation", true),
+ ENTITY_IMPORT(DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit, "entity version load", true),
+ NOTIFICATION_REQUESTS(DefaultTenantProfileConfiguration::getTenantNotificationRequestsRateLimit, "notification requests", true),
+ NOTIFICATION_REQUESTS_PER_RULE(DefaultTenantProfileConfiguration::getTenantNotificationRequestsPerRuleRateLimit, "notification requests per rule", false),
+ REST_REQUESTS_PER_TENANT(DefaultTenantProfileConfiguration::getTenantServerRestLimitsConfiguration, "REST API requests", true),
+ REST_REQUESTS_PER_CUSTOMER(DefaultTenantProfileConfiguration::getCustomerServerRestLimitsConfiguration, "REST API requests per customer", false),
+ WS_UPDATES_PER_SESSION(DefaultTenantProfileConfiguration::getWsUpdatesPerSessionRateLimit, "WS updates per session", true),
+ CASSANDRA_QUERIES(DefaultTenantProfileConfiguration::getCassandraQueryTenantRateLimitsConfiguration, "Cassandra queries", true),
+ PASSWORD_RESET(false, true),
+ TWO_FA_VERIFICATION_CODE_SEND(false, true),
+ TWO_FA_VERIFICATION_CODE_CHECK(false, true),
+ TRANSPORT_MESSAGES_PER_TENANT("transport messages", true),
+ TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false);
+
+ private Function configExtractor;
+ @Getter
+ private final boolean perTenant;
+ @Getter
+ private boolean refillRateLimitIntervally;
+ @Getter
+ private String label;
+
+ LimitedApi(Function configExtractor, String label, boolean perTenant) {
+ this.configExtractor = configExtractor;
+ this.label = label;
+ this.perTenant = perTenant;
+ }
+
+ LimitedApi(boolean perTenant, boolean refillRateLimitIntervally) {
+ this.perTenant = perTenant;
+ this.refillRateLimitIntervally = refillRateLimitIntervally;
+ }
+
+ LimitedApi(String label, boolean perTenant) {
+ this.label = label;
+ this.perTenant = perTenant;
+ }
+
+ public String getLimitConfig(DefaultTenantProfileConfiguration profileConfiguration) {
+ return Optional.ofNullable(configExtractor)
+ .map(extractor -> extractor.apply(profileConfiguration))
+ .orElse(null);
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mail/MailOauth2Provider.java b/common/data/src/main/java/org/thingsboard/server/common/data/mail/MailOauth2Provider.java
new file mode 100644
index 0000000000..c0cdb92500
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/mail/MailOauth2Provider.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.mail;
+
+public enum MailOauth2Provider {
+ GOOGLE("Google"), OFFICE_365("Office 365"), SENDGRID("SendGrid"), CUSTOM("Custom");
+
+ public final String label;
+
+ MailOauth2Provider(String label) {
+ this.label = label;
+ }
+
+ @Override
+ public String toString() {
+ return label;
+ }
+}
\ No newline at end of file
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java
index 31e05a2cb3..619e1ad38f 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java
@@ -62,6 +62,9 @@ public class NotificationRequestStats {
return;
}
String errorMessage = error.getMessage();
+ if (errorMessage == null) {
+ errorMessage = error.getClass().getSimpleName();
+ }
errors.computeIfAbsent(deliveryMethod, k -> new ConcurrentHashMap<>()).put(recipient.getTitle(), errorMessage);
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
index de07d03c67..251fae2ac0 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
@@ -27,6 +27,7 @@ public enum NotificationType {
NEW_PLATFORM_VERSION,
ENTITIES_LIMIT,
API_USAGE_LIMIT,
- RULE_NODE
+ RULE_NODE,
+ RATE_LIMITS
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java
index d3af30907d..fd1b4ee0f8 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java
@@ -42,6 +42,8 @@ public class AlarmNotificationInfo implements RuleOriginatedNotificationInfo {
private String alarmOriginatorName;
private AlarmSeverity alarmSeverity;
private AlarmStatus alarmStatus;
+ private boolean acknowledged;
+ private boolean cleared;
private CustomerId alarmCustomerId;
@Override
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/RateLimitsNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/RateLimitsNotificationInfo.java
new file mode 100644
index 0000000000..57fc278cff
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/RateLimitsNotificationInfo.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.notification.info;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.limit.LimitedApi;
+
+import java.util.Map;
+
+import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class RateLimitsNotificationInfo implements RuleOriginatedNotificationInfo {
+
+ private TenantId tenantId;
+ private String tenantName;
+ private LimitedApi api;
+ private EntityId limitLevel;
+ private String limitLevelEntityName;
+
+ @Override
+ public Map getTemplateData() {
+ return mapOf(
+ "api", api.getLabel(),
+ "limitLevelEntityType", limitLevel != null ? limitLevel.getEntityType().getNormalName() : null,
+ "limitLevelEntityId", limitLevel != null ? limitLevel.getId().toString() : null,
+ "limitLevelEntityName", limitLevelEntityName,
+ "tenantName", tenantName,
+ "tenantId", tenantId.toString()
+ );
+ }
+
+ @Override
+ public TenantId getAffectedTenantId() {
+ return tenantId;
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java
index 05fd9db6c0..e2d872146b 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java
@@ -26,8 +26,8 @@ import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.NotificationTemplateId;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
@@ -36,6 +36,8 @@ import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.io.Serializable;
+import java.util.List;
+import java.util.stream.Collectors;
@Data
@NoArgsConstructor
@@ -84,4 +86,12 @@ public class NotificationRule extends BaseData implements Ha
triggerType == recipientsConfig.getTriggerType();
}
+ @JsonIgnore
+ public String getDeduplicationKey() {
+ String targets = recipientsConfig.getTargetsTable().values().stream()
+ .flatMap(List::stream).sorted().map(Object::toString)
+ .collect(Collectors.joining(","));
+ return String.join(":", targets, triggerConfig.getDeduplicationKey());
+ }
+
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java
index ed3a8c2cd6..2019310a70 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java
@@ -21,7 +21,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.Data;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import jakarta.validation.constraints.NotNull;
import java.io.Serializable;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmAssignmentTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmAssignmentTrigger.java
new file mode 100644
index 0000000000..11d24386bc
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmAssignmentTrigger.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.notification.rule.trigger;
+
+import lombok.Builder;
+import lombok.Data;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.alarm.AlarmInfo;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
+
+@Data
+@Builder
+public class AlarmAssignmentTrigger implements NotificationRuleTrigger {
+
+ private final TenantId tenantId;
+ private final AlarmInfo alarmInfo;
+ private final ActionType actionType;
+ private final User user;
+
+ @Override
+ public EntityId getOriginatorEntityId() {
+ return alarmInfo.getOriginator();
+ }
+
+ @Override
+ public NotificationRuleTriggerType getType() {
+ return NotificationRuleTriggerType.ALARM_ASSIGNMENT;
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentTrigger.java
new file mode 100644
index 0000000000..65ad9c9619
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentTrigger.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.notification.rule.trigger;
+
+import lombok.Builder;
+import lombok.Data;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmComment;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
+
+@Data
+@Builder
+public class AlarmCommentTrigger implements NotificationRuleTrigger {
+
+ private final TenantId tenantId;
+ private final AlarmComment comment;
+ private final Alarm alarm;
+ private final ActionType actionType;
+ private final User user;
+
+ @Override
+ public NotificationRuleTriggerType getType() {
+ return NotificationRuleTriggerType.ALARM_COMMENT;
+ }
+
+ @Override
+ public EntityId getOriginatorEntityId() {
+ return alarm.getId();
+ }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/AlarmTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmTrigger.java
similarity index 92%
rename from common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/AlarmTrigger.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmTrigger.java
index b395ba9ebe..674355150f 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/AlarmTrigger.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmTrigger.java
@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.msg.notification.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
@Data
@Builder
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/ApiUsageLimitTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ApiUsageLimitTrigger.java
similarity index 88%
rename from common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/ApiUsageLimitTrigger.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ApiUsageLimitTrigger.java
index 368a16712c..323f2b275f 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/ApiUsageLimitTrigger.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ApiUsageLimitTrigger.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.msg.notification.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.Builder;
import lombok.Data;
@@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.ApiUsageRecordState;
import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
@Data
@Builder
@@ -36,11 +36,6 @@ public class ApiUsageLimitTrigger implements NotificationRuleTrigger {
return NotificationRuleTriggerType.API_USAGE_LIMIT;
}
- @Override
- public TenantId getTenantId() {
- return tenantId;
- }
-
@Override
public EntityId getOriginatorEntityId() {
return tenantId;
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/RuleEngineMsgTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/DeviceActivityTrigger.java
similarity index 63%
rename from common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/RuleEngineMsgTrigger.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/DeviceActivityTrigger.java
index 361264a0c2..0c0139410e 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/RuleEngineMsgTrigger.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/DeviceActivityTrigger.java
@@ -13,34 +13,37 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.msg.notification.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.Builder;
import lombok.Data;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
-import org.thingsboard.server.common.msg.TbMsg;
-
-import java.util.Map;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
@Data
@Builder
-public class RuleEngineMsgTrigger implements NotificationRuleTrigger {
+public class DeviceActivityTrigger implements NotificationRuleTrigger {
private final TenantId tenantId;
- private final TbMsg msg;
+ private final CustomerId customerId;
+ private final DeviceId deviceId;
+ private final boolean active;
- public static Map msgTypeToTriggerType; // set on init by DefaultNotificationRuleProcessor
+ private final String deviceName;
+ private final String deviceType;
+ private final String deviceLabel;
@Override
- public NotificationRuleTriggerType getType() {
- return msgTypeToTriggerType != null ? msgTypeToTriggerType.get(msg.getType()) : null;
+ public EntityId getOriginatorEntityId() {
+ return deviceId;
}
@Override
- public EntityId getOriginatorEntityId() {
- return msg.getOriginator();
+ public NotificationRuleTriggerType getType() {
+ return NotificationRuleTriggerType.DEVICE_ACTIVITY;
}
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/EntitiesLimitTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntitiesLimitTrigger.java
similarity index 92%
rename from common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/EntitiesLimitTrigger.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntitiesLimitTrigger.java
index e6ce143fd0..3222e3fc6d 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/EntitiesLimitTrigger.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntitiesLimitTrigger.java
@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.msg.notification.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
@Data
@Builder
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/EntityActionTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntityActionTrigger.java
similarity index 93%
rename from common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/EntityActionTrigger.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntityActionTrigger.java
index 47d1789fec..aaf61ab0bc 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/EntityActionTrigger.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntityActionTrigger.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.msg.notification.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.Builder;
import lombok.Data;
@@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
@Data
@Builder
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/NewPlatformVersionTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java
similarity index 72%
rename from common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/NewPlatformVersionTrigger.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java
index 0da2f0c57b..9776bfb575 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/NewPlatformVersionTrigger.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java
@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.msg.notification.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
@Data
@Builder
@@ -43,4 +43,21 @@ public class NewPlatformVersionTrigger implements NotificationRuleTrigger {
return TenantId.SYS_TENANT_ID;
}
+
+ @Override
+ public boolean deduplicate() {
+ return true;
+ }
+
+ @Override
+ public String getDeduplicationKey() {
+ return String.join(":", NotificationRuleTrigger.super.getDeduplicationKey(),
+ updateInfo.getCurrentVersion(), updateInfo.getLatestVersion());
+ }
+
+ @Override
+ public long getDefaultDeduplicationDuration() {
+ return 0;
+ }
+
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/NotificationRuleTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTrigger.java
similarity index 66%
rename from common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/NotificationRuleTrigger.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTrigger.java
index b511062549..2aac6fe75e 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/NotificationRuleTrigger.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTrigger.java
@@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.msg.notification.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.io.Serializable;
@@ -29,4 +29,18 @@ public interface NotificationRuleTrigger extends Serializable {
EntityId getOriginatorEntityId();
+
+ default boolean deduplicate() {
+ return false;
+ }
+
+ default String getDeduplicationKey() {
+ EntityId originatorEntityId = getOriginatorEntityId();
+ return String.join(":", getType().toString(), originatorEntityId.getEntityType().toString(), originatorEntityId.getId().toString());
+ }
+
+ default long getDefaultDeduplicationDuration() {
+ return 0;
+ }
+
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RateLimitsTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RateLimitsTrigger.java
new file mode 100644
index 0000000000..3941ade824
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RateLimitsTrigger.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.notification.rule.trigger;
+
+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.limit.LimitedApi;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
+
+import java.util.concurrent.TimeUnit;
+
+@Data
+@Builder
+public class RateLimitsTrigger implements NotificationRuleTrigger {
+
+ private final TenantId tenantId;
+ private final LimitedApi api;
+ private final EntityId limitLevel;
+ private final String limitLevelEntityName;
+
+ @Override
+ public NotificationRuleTriggerType getType() {
+ return NotificationRuleTriggerType.RATE_LIMITS;
+ }
+
+ @Override
+ public EntityId getOriginatorEntityId() {
+ return limitLevel != null ? limitLevel : tenantId;
+ }
+
+
+ @Override
+ public boolean deduplicate() {
+ return true;
+ }
+
+ @Override
+ public String getDeduplicationKey() {
+ return String.join(":", NotificationRuleTrigger.super.getDeduplicationKey(), api.toString());
+ }
+
+ @Override
+ public long getDefaultDeduplicationDuration() {
+ return TimeUnit.HOURS.toMillis(4);
+ }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/RuleEngineComponentLifecycleEventTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RuleEngineComponentLifecycleEventTrigger.java
similarity index 93%
rename from common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/RuleEngineComponentLifecycleEventTrigger.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RuleEngineComponentLifecycleEventTrigger.java
index 56f4298b29..f1509eb2dc 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/RuleEngineComponentLifecycleEventTrigger.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RuleEngineComponentLifecycleEventTrigger.java
@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.msg.notification.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.Builder;
import lombok.Data;
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.notification.rule.trigger.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@Data
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmAssignmentNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java
similarity index 99%
rename from common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmAssignmentNotificationRuleTriggerConfig.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java
index 02a3bb8e23..80992e9c91 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmAssignmentNotificationRuleTriggerConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.data.notification.rule.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger.config;
import lombok.AllArgsConstructor;
import lombok.Builder;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java
similarity index 99%
rename from common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentNotificationRuleTriggerConfig.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java
index c34127b611..f0d8a7ef15 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentNotificationRuleTriggerConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.data.notification.rule.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger.config;
import lombok.AllArgsConstructor;
import lombok.Builder;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java
similarity index 99%
rename from common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmNotificationRuleTriggerConfig.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java
index 3a2eeaadc7..0593b41b0e 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmNotificationRuleTriggerConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.data.notification.rule.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger.config;
import lombok.AllArgsConstructor;
import lombok.Builder;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ApiUsageLimitNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ApiUsageLimitNotificationRuleTriggerConfig.java
similarity index 99%
rename from common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ApiUsageLimitNotificationRuleTriggerConfig.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ApiUsageLimitNotificationRuleTriggerConfig.java
index d5ce9dff68..ec49e3828a 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ApiUsageLimitNotificationRuleTriggerConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ApiUsageLimitNotificationRuleTriggerConfig.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.data.notification.rule.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger.config;
import lombok.AllArgsConstructor;
import lombok.Builder;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/DeviceActivityNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/DeviceActivityNotificationRuleTriggerConfig.java
similarity index 99%
rename from common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/DeviceActivityNotificationRuleTriggerConfig.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/DeviceActivityNotificationRuleTriggerConfig.java
index c0ce99e76d..ec1dbac7a2 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/DeviceActivityNotificationRuleTriggerConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/DeviceActivityNotificationRuleTriggerConfig.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.data.notification.rule.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger.config;
import lombok.AllArgsConstructor;
import lombok.Builder;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntitiesLimitNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntitiesLimitNotificationRuleTriggerConfig.java
similarity index 99%
rename from common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntitiesLimitNotificationRuleTriggerConfig.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntitiesLimitNotificationRuleTriggerConfig.java
index 8aeb715a11..84d061ba72 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntitiesLimitNotificationRuleTriggerConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntitiesLimitNotificationRuleTriggerConfig.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.data.notification.rule.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger.config;
import lombok.AllArgsConstructor;
import lombok.Builder;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntityActionNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntityActionNotificationRuleTriggerConfig.java
similarity index 99%
rename from common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntityActionNotificationRuleTriggerConfig.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntityActionNotificationRuleTriggerConfig.java
index 3a627a9e56..ed61d5eb94 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntityActionNotificationRuleTriggerConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntityActionNotificationRuleTriggerConfig.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.data.notification.rule.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger.config;
import lombok.AllArgsConstructor;
import lombok.Builder;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NewPlatformVersionNotificationRuleTriggerConfig.java
similarity index 98%
rename from common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionNotificationRuleTriggerConfig.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NewPlatformVersionNotificationRuleTriggerConfig.java
index 230be3b9d5..ebec3967d0 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionNotificationRuleTriggerConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NewPlatformVersionNotificationRuleTriggerConfig.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.data.notification.rule.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger.config;
import lombok.Data;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java
similarity index 89%
rename from common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerConfig.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java
index 3406a802c7..db75e395b1 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java
@@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.data.notification.rule.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger.config;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
@@ -34,9 +35,15 @@ import java.io.Serializable;
@Type(value = NewPlatformVersionNotificationRuleTriggerConfig.class, name = "NEW_PLATFORM_VERSION"),
@Type(value = EntitiesLimitNotificationRuleTriggerConfig.class, name = "ENTITIES_LIMIT"),
@Type(value = ApiUsageLimitNotificationRuleTriggerConfig.class, name = "API_USAGE_LIMIT"),
+ @Type(value = RateLimitsNotificationRuleTriggerConfig.class, name = "RATE_LIMITS"),
})
public interface NotificationRuleTriggerConfig extends Serializable {
NotificationRuleTriggerType getTriggerType();
+ @JsonIgnore
+ default String getDeduplicationKey() {
+ return "#";
+ }
+
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java
similarity index 79%
rename from common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerType.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java
index e79e7f1195..1d73de0d2b 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java
@@ -13,13 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.data.notification.rule.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger.config;
import lombok.Getter;
-import lombok.RequiredArgsConstructor;
@Getter
-@RequiredArgsConstructor
public enum NotificationRuleTriggerType {
ENTITY_ACTION,
@@ -28,15 +26,19 @@ public enum NotificationRuleTriggerType {
ALARM_ASSIGNMENT,
DEVICE_ACTIVITY,
RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT,
- NEW_PLATFORM_VERSION(false, true),
- ENTITIES_LIMIT(false, false),
- API_USAGE_LIMIT(false, false);
+ NEW_PLATFORM_VERSION(false),
+ ENTITIES_LIMIT(false),
+ API_USAGE_LIMIT(false),
+ RATE_LIMITS(false);
private final boolean tenantLevel;
- private final boolean deduplicate;
NotificationRuleTriggerType() {
- this(true, false);
+ this(true);
+ }
+
+ NotificationRuleTriggerType(boolean tenantLevel) {
+ this.tenantLevel = tenantLevel;
}
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RateLimitsNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RateLimitsNotificationRuleTriggerConfig.java
new file mode 100644
index 0000000000..6cb447bc8f
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RateLimitsNotificationRuleTriggerConfig.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.notification.rule.trigger.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.common.data.limit.LimitedApi;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class RateLimitsNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
+
+ private Set apis;
+
+ @Override
+ public NotificationRuleTriggerType getTriggerType() {
+ return NotificationRuleTriggerType.RATE_LIMITS;
+ }
+
+ @Override
+ public String getDeduplicationKey() {
+ return apis == null ? "#" : apis.stream().sorted().map(Enum::name).collect(Collectors.joining(","));
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java
similarity index 99%
rename from common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java
index c28e190cc9..005d86ac6f 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.common.data.notification.rule.trigger;
+package org.thingsboard.server.common.data.notification.rule.trigger.config;
import lombok.AllArgsConstructor;
import lombok.Builder;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java
index 3387f85e6f..464287b98f 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java
@@ -21,10 +21,11 @@ import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.thingsboard.server.common.data.BaseData;
-import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo;
import org.thingsboard.server.common.data.id.ComponentDescriptorId;
import org.thingsboard.server.common.data.validation.Length;
+import java.util.Objects;
+
/**
* @author Andrew Shvayka
*/
@@ -47,6 +48,8 @@ public class ComponentDescriptor extends BaseData {
@Getter @Setter private String clazz;
@Schema(description = "Complex JSON object that represents the Rule Node configuration.", accessMode = Schema.AccessMode.READ_ONLY)
@Getter @Setter private transient JsonNode configurationDescriptor;
+ @Schema(description = "Rule node configuration version. By default, this value is 0. If the rule node is a versioned node, this value might be greater than 0.", accessMode = Schema.AccessMode.READ_ONLY)
+ @Getter @Setter private int configurationVersion;
@Length(fieldName = "actions")
@Schema(description = "Rule Node Actions. Deprecated. Always null.", accessMode = Schema.AccessMode.READ_ONLY)
@Getter @Setter private String actions;
@@ -63,9 +66,11 @@ public class ComponentDescriptor extends BaseData {
super(plugin);
this.type = plugin.getType();
this.scope = plugin.getScope();
+ this.clusteringMode = plugin.getClusteringMode();
this.name = plugin.getName();
this.clazz = plugin.getClazz();
this.configurationDescriptor = plugin.getConfigurationDescriptor();
+ this.configurationVersion = plugin.getConfigurationVersion();
this.actions = plugin.getActions();
}
@@ -93,10 +98,11 @@ public class ComponentDescriptor extends BaseData {
if (type != that.type) return false;
if (scope != that.scope) return false;
- if (name != null ? !name.equals(that.name) : that.name != null) return false;
- if (actions != null ? !actions.equals(that.actions) : that.actions != null) return false;
- if (configurationDescriptor != null ? !configurationDescriptor.equals(that.configurationDescriptor) : that.configurationDescriptor != null) return false;
- return clazz != null ? clazz.equals(that.clazz) : that.clazz == null;
+ if (!Objects.equals(name, that.name)) return false;
+ if (!Objects.equals(actions, that.actions)) return false;
+ if (!Objects.equals(configurationDescriptor, that.configurationDescriptor)) return false;
+ if (configurationVersion != that.configurationVersion) return false;
+ return Objects.equals(clazz, that.clazz);
}
@Override
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
index 1926bb8c3c..1fea13563a 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
@@ -49,7 +49,9 @@ public class RuleNode extends BaseDataWithAdditionalInfo implements
private boolean debugMode;
@Schema(description = "Enable/disable singleton mode. ", example = "false")
private boolean singletonMode;
- @Schema(description = "JSON with the rule node configuration. Structure depends on the rule node implementation.",implementation = com.fasterxml.jackson.databind.JsonNode.class)
+ @Schema(description = "Version of rule node configuration. ", example = "0")
+ private int configurationVersion;
+ @Schema(description = "JSON with the rule node configuration. Structure depends on the rule node implementation.", implementation = com.fasterxml.jackson.databind.JsonNode.class)
private transient JsonNode configuration;
@JsonIgnore
private byte[] configurationBytes;
@@ -91,7 +93,7 @@ public class RuleNode extends BaseDataWithAdditionalInfo implements
@Schema(description = "JSON object with the Rule Node Id. " +
"Specify this field to update the Rule Node. " +
"Referencing non-existing Rule Node Id will cause error. " +
- "Omit this field to create new rule node." )
+ "Omit this field to create new rule node.")
@Override
public RuleNodeId getId() {
return super.getId();
@@ -103,7 +105,7 @@ public class RuleNode extends BaseDataWithAdditionalInfo implements
return super.getCreatedTime();
}
- @Schema(description = "Additional parameters of the rule node. Contains 'layoutX' and 'layoutY' properties for visualization.",implementation = com.fasterxml.jackson.databind.JsonNode.class)
+ @Schema(description = "Additional parameters of the rule node. Contains 'layoutX' and 'layoutY' properties for visualization.", implementation = com.fasterxml.jackson.databind.JsonNode.class)
@Override
public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo();
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java
index d8d17613de..a0736ca50e 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java
@@ -16,7 +16,6 @@
package org.thingsboard.server.common.data.util;
import java.util.Collection;
-import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -51,20 +50,19 @@ public class CollectionsUtil {
}
@SuppressWarnings("unchecked")
- public static Map mapOf(Object... kvs) {
- Map map = new HashMap<>();
+ public static Map mapOf(T... kvs) {
+ if (kvs.length % 2 != 0) {
+ throw new IllegalArgumentException("Invalid number of parameters");
+ }
+ Map map = new HashMap<>();
for (int i = 0; i < kvs.length; i += 2) {
- K key = (K) kvs[i];
- V value = (V) kvs[i + 1];
+ T key = kvs[i];
+ T value = kvs[i + 1];
map.put(key, value);
}
return map;
}
- public static Map unmodifiableMapOf(Object... kvs) {
- return Collections.unmodifiableMap(mapOf(kvs));
- }
-
public static boolean emptyOrContains(Collection collection, V element) {
return isEmpty(collection) || collection.contains(element);
}
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 04b5d3fb35..0a39d8d51b 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
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.msg;
+import lombok.Getter;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
@@ -28,7 +29,7 @@ public enum MsgType {
*
* See {@link PartitionChangeMsg}
*/
- PARTITION_CHANGE_MSG,
+ PARTITION_CHANGE_MSG(true),
APP_INIT_MSG,
@@ -108,7 +109,7 @@ public enum MsgType {
* Message that is sent from the Device Actor to Rule Engine. Requires acknowledgement
*/
- SESSION_TIMEOUT_MSG,
+ SESSION_TIMEOUT_MSG(true),
STATS_PERSIST_TICK_MSG,
@@ -130,4 +131,14 @@ public enum MsgType {
EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG,
EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG;
+ @Getter
+ private final boolean ignoreOnStart;
+
+ MsgType() {
+ this.ignoreOnStart = false;
+ }
+
+ MsgType(boolean ignoreOnStart) {
+ this.ignoreOnStart = ignoreOnStart;
+ }
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformNodeConfiguration.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorError.java
similarity index 84%
rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformNodeConfiguration.java
rename to common/message/src/main/java/org/thingsboard/server/common/msg/TbActorError.java
index 160bd6d8d1..fc27feb096 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformNodeConfiguration.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorError.java
@@ -13,11 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.rule.engine.transform;
+package org.thingsboard.server.common.msg;
-import lombok.Data;
+public interface TbActorError {
-@Data
-public class TbTransformNodeConfiguration {
+ boolean isUnrecoverable();
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
index f04104bc51..17bc7c0a8f 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
@@ -277,6 +277,11 @@ public final class TbMsg implements Serializable {
this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx, callback);
}
+ public TbMsg copyWithNewCtx() {
+ return new TbMsg(this.queueName, this.id, this.ts, this.type, this.originator, this.customerId,
+ this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx.copy(), TbMsgCallback.EMPTY);
+ }
+
public TbMsgCallback getCallback() {
// May be null in case of deserialization;
if (callback != null) {
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgProcessingCtx.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgProcessingCtx.java
index 9010fc0b54..1b1cbcdf54 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgProcessingCtx.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgProcessingCtx.java
@@ -64,7 +64,10 @@ public final class TbMsgProcessingCtx implements Serializable {
}
public TbMsgProcessingStackItem pop() {
- return !stack.isEmpty() ? stack.removeLast() : null;
+ if (stack == null || stack.isEmpty()) {
+ return null;
+ }
+ return stack.removeLast();
}
public static TbMsgProcessingCtx fromProto(MsgProtos.TbMsgProcessingCtxProto ctx) {
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleChainAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleChainAwareMsg.java
index d0e90ae421..5fbc857e0c 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleChainAwareMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleChainAwareMsg.java
@@ -17,9 +17,12 @@ package org.thingsboard.server.common.msg.aware;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
public interface RuleChainAwareMsg extends TbActorMsg {
RuleChainId getRuleChainId();
+
+ TbMsg getMsg();
}
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/notification/NotificationRuleProcessor.java b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/NotificationRuleProcessor.java
similarity index 82%
rename from common/queue/src/main/java/org/thingsboard/server/queue/notification/NotificationRuleProcessor.java
rename to common/message/src/main/java/org/thingsboard/server/common/msg/notification/NotificationRuleProcessor.java
index 1fa9d82863..8516a50b2f 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/notification/NotificationRuleProcessor.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/NotificationRuleProcessor.java
@@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.queue.notification;
+package org.thingsboard.server.common.msg.notification;
-import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
public interface NotificationRuleProcessor {
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationService.java b/common/queue/src/main/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationService.java
new file mode 100644
index 0000000000..871408ac40
--- /dev/null
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationService.java
@@ -0,0 +1,125 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.queue.notification;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+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.stereotype.Service;
+import org.springframework.util.ConcurrentReferenceHashMap;
+import org.thingsboard.server.common.data.CacheConstants;
+import org.thingsboard.server.common.data.notification.rule.NotificationRule;
+import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
+import org.thingsboard.server.queue.util.PropertyUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentMap;
+
+import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType.SOFT;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class DefaultNotificationDeduplicationService implements NotificationDeduplicationService {
+
+ private Map deduplicationDurations;
+
+ @Autowired(required = false)
+ private CacheManager cacheManager;
+ private final ConcurrentMap localCache = new ConcurrentReferenceHashMap<>(16, SOFT);
+
+ @Override
+ public boolean alreadyProcessed(NotificationRuleTrigger trigger) {
+ String deduplicationKey = trigger.getDeduplicationKey();
+ return alreadyProcessed(trigger, deduplicationKey, true);
+ }
+
+ @Override
+ public boolean alreadyProcessed(NotificationRuleTrigger trigger, NotificationRule rule) {
+ String deduplicationKey = getDeduplicationKey(trigger, rule);
+ return alreadyProcessed(trigger, deduplicationKey, false);
+ }
+
+ private boolean alreadyProcessed(NotificationRuleTrigger trigger, String deduplicationKey, boolean onlyLocalCache) {
+ Long lastProcessedTs = localCache.get(deduplicationKey);
+ if (lastProcessedTs == null && !onlyLocalCache) {
+ Cache externalCache = getExternalCache();
+ if (externalCache != null) {
+ lastProcessedTs = externalCache.get(deduplicationKey, Long.class);
+ } else {
+ log.warn("Sent notifications cache is not set up");
+ }
+ }
+
+ boolean alreadyProcessed = false;
+ long deduplicationDuration = getDeduplicationDuration(trigger);
+ if (lastProcessedTs != null) {
+ long passed = System.currentTimeMillis() - lastProcessedTs;
+ log.trace("Deduplicating trigger {} by key '{}'. Deduplication duration: {} ms, passed: {} ms",
+ trigger.getType(), deduplicationKey, deduplicationDuration, passed);
+ if (deduplicationDuration == 0 || passed <= deduplicationDuration) {
+ alreadyProcessed = true;
+ }
+ }
+
+ if (!alreadyProcessed) {
+ lastProcessedTs = System.currentTimeMillis();
+ }
+ localCache.put(deduplicationKey, lastProcessedTs);
+ if (!onlyLocalCache) {
+ if (!alreadyProcessed || deduplicationDuration == 0) {
+ // if lastProcessedTs is changed or if deduplicating infinitely (so that cache value not removed by ttl)
+ Cache externalCache = getExternalCache();
+ if (externalCache != null) {
+ externalCache.put(deduplicationKey, lastProcessedTs);
+ }
+ }
+ }
+ return alreadyProcessed;
+ }
+
+ public static String getDeduplicationKey(NotificationRuleTrigger trigger, NotificationRule rule) {
+ return String.join("_", trigger.getDeduplicationKey(), rule.getDeduplicationKey());
+ }
+
+ private long getDeduplicationDuration(NotificationRuleTrigger trigger) {
+ return deduplicationDurations.computeIfAbsent(trigger.getType(), triggerType -> {
+ return trigger.getDefaultDeduplicationDuration();
+ });
+ }
+
+ private Cache getExternalCache() {
+ return Optional.ofNullable(cacheManager)
+ .map(cacheManager -> cacheManager.getCache(CacheConstants.SENT_NOTIFICATIONS_CACHE))
+ .orElse(null);
+ }
+
+ @Autowired
+ public void setDeduplicationDurations(@Value("${notification_system.rules.deduplication_durations:}")
+ String deduplicationDurationsStr) {
+ this.deduplicationDurations = new HashMap<>();
+ PropertyUtils.getProps(deduplicationDurationsStr).forEach((triggerType, duration) -> {
+ this.deduplicationDurations.put(NotificationRuleTriggerType.valueOf(triggerType), Long.parseLong(duration));
+ });
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineMsgNotificationRuleTriggerProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/notification/NotificationDeduplicationService.java
similarity index 62%
rename from application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineMsgNotificationRuleTriggerProcessor.java
rename to common/queue/src/main/java/org/thingsboard/server/queue/notification/NotificationDeduplicationService.java
index ac8f692f02..3ccc98ecb6 100644
--- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineMsgNotificationRuleTriggerProcessor.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/notification/NotificationDeduplicationService.java
@@ -13,15 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.notification.rule.trigger;
+package org.thingsboard.server.queue.notification;
-import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
-import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger;
+import org.thingsboard.server.common.data.notification.rule.NotificationRule;
+import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
-import java.util.Set;
+public interface NotificationDeduplicationService {
-public interface RuleEngineMsgNotificationRuleTriggerProcessor extends NotificationRuleTriggerProcessor {
+ boolean alreadyProcessed(NotificationRuleTrigger trigger);
- Set getSupportedMsgTypes();
+ boolean alreadyProcessed(NotificationRuleTrigger trigger, NotificationRule rule);
}
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java
index 3c3988389f..5304056a88 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java
@@ -20,7 +20,8 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.stereotype.Service;
-import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger;
+import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
+import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos;
@@ -38,6 +39,7 @@ import java.util.UUID;
@Slf4j
public class RemoteNotificationRuleProcessor implements NotificationRuleProcessor {
+ private final NotificationDeduplicationService deduplicationService;
private final TbQueueProducerProvider producerProvider;
private final NotificationsTopicService notificationsTopicService;
private final PartitionService partitionService;
@@ -46,7 +48,11 @@ public class RemoteNotificationRuleProcessor implements NotificationRuleProcesso
@Override
public void process(NotificationRuleTrigger trigger) {
try {
- log.trace("Submitting notification rule trigger: {}", trigger);
+ if (trigger.deduplicate() && deduplicationService.alreadyProcessed(trigger)) {
+ return;
+ }
+
+ log.debug("Submitting notification rule trigger: {}", trigger);
TransportProtos.NotificationRuleProcessorMsg.Builder msg = TransportProtos.NotificationRuleProcessorMsg.newBuilder()
.setTrigger(ByteString.copyFrom(encodingService.encode(trigger)));
@@ -57,7 +63,7 @@ public class RemoteNotificationRuleProcessor implements NotificationRuleProcesso
.setNotificationRuleProcessorMsg(msg)
.build()), null);
});
- } catch (Exception e) {
+ } catch (Throwable e) {
log.error("Failed to submit notification rule trigger: {}", trigger, e);
}
}
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java
index aee2ff39dc..6f7195c0d3 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueFactory.java
@@ -20,6 +20,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg;
@@ -110,6 +111,11 @@ public class AwsSqsTransportQueueFactory implements TbTransportQueueFactory {
return new TbAwsSqsProducerTemplate<>(coreAdmin, sqsSettings, coreSettings.getTopic());
}
+ @Override
+ public TbQueueProducer> createTbCoreNotificationsMsgProducer() {
+ return new TbAwsSqsProducerTemplate<>(notificationAdmin, sqsSettings, coreSettings.getTopic());
+ }
+
@Override
public TbQueueConsumer> createTransportNotificationsConsumer() {
return new TbAwsSqsConsumerTemplate<>(notificationAdmin, sqsSettings, transportNotificationSettings.getNotificationsTopic() + "_" + serviceInfoProvider.getServiceId(),
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java
index 60c464ecab..6b6253acc4 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java
@@ -20,6 +20,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg;
@@ -100,6 +101,11 @@ public class InMemoryTbTransportQueueFactory implements TbTransportQueueFactory
return new InMemoryTbQueueProducer<>(storage, coreSettings.getTopic());
}
+ @Override
+ public TbQueueProducer> createTbCoreNotificationsMsgProducer() {
+ return new InMemoryTbQueueProducer<>(storage, coreSettings.getTopic());
+ }
+
@Override
public TbQueueConsumer> createTransportNotificationsConsumer() {
return new InMemoryTbQueueConsumer<>(storage, transportNotificationSettings.getNotificationsTopic() + "." + serviceInfoProvider.getServiceId());
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java
index 67e12568f9..b5f0275f8e 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java
@@ -18,7 +18,9 @@ package org.thingsboard.server.queue.provider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
+import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg;
@@ -134,6 +136,16 @@ public class KafkaTbTransportQueueFactory implements TbTransportQueueFactory {
return requestBuilder.build();
}
+ @Override
+ public TbQueueProducer> createTbCoreNotificationsMsgProducer() {
+ TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder> requestBuilder = TbKafkaProducerTemplate.builder();
+ requestBuilder.settings(kafkaSettings);
+ requestBuilder.clientId("transport-node-to-core-notifications-" + serviceInfoProvider.getServiceId());
+ requestBuilder.defaultTopic(coreSettings.getTopic());
+ requestBuilder.admin(notificationAdmin);
+ return requestBuilder.build();
+ }
+
@Override
public TbQueueConsumer> createTransportNotificationsConsumer() {
TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> responseBuilder = TbKafkaConsumerTemplate.builder();
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java
index 9581e3833e..9fa49ddf9e 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTransportQueueFactory.java
@@ -18,7 +18,9 @@ package org.thingsboard.server.queue.provider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
+import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg;
@@ -108,6 +110,11 @@ public class PubSubTransportQueueFactory implements TbTransportQueueFactory {
return new TbPubSubProducerTemplate<>(coreAdmin, pubSubSettings, coreSettings.getTopic());
}
+ @Override
+ public TbQueueProducer> createTbCoreNotificationsMsgProducer() {
+ return new TbPubSubProducerTemplate<>(notificationAdmin, pubSubSettings, coreSettings.getTopic());
+ }
+
@Override
public TbQueueConsumer