diff --git a/.gitignore b/.gitignore index ee8c78dad3..833038e151 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ rebuild-docker.sh .run .claude .planning +docs/superpowers/ \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/air_quality.json b/application/src/main/data/json/system/widget_bundles/air_quality.json deleted file mode 100644 index 534b509dba..0000000000 --- a/application/src/main/data/json/system/widget_bundles/air_quality.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "widgetsBundle": { - "alias": "air_quality", - "title": "Air quality", - "image": "tb-image:YWlyX3F1YWxpdHlfd2lkZ2V0c19idW5kbGUuc3Zn:YWlyX3F1YWxpdHlfd2lkZ2V0c19idW5kbGUuc3Zn;data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMTYwIiBmaWxsPSJub25lIj4KICA8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9Ijc2IiBmaWxsPSIjZmZmIiByeD0iNCIgLz4KICA8cmVjdCB3aWR0aD0iMTk5IiBoZWlnaHQ9Ijc1IiB4PSIuNSIgeT0iLjUiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9Ii4xIiByeD0iMy41IiAvPgogIDxwYXRoIGZpbGw9IiMwMDAiIGZpbGwtb3BhY2l0eT0iLjkiCiAgICBkPSJNMTIgMTYuMWgxYTIuMyAyLjMgMCAwIDEtLjQgMSAxLjkgMS45IDAgMCAxLS43LjdsLTEuMi4zYy0uNCAwLS43IDAtMS0uMmEyIDIgMCAwIDEtLjctLjUgMi40IDIuNCAwIDAgMS0uNC0uOWwtLjItMS4xdi0uNWEzLjYgMy42IDAgMCAxIC4yLTFjMC0uNC4yLS43LjQtMWwuOC0uNSAxLS4yYy40IDAgLjguMSAxIC4zLjQuMi42LjQuOC43LjIuMy4zLjYuMyAxaC0xYTEuOCAxLjggMCAwIDAtLjEtLjYuOS45IDAgMCAwLS40LS40bC0uNy0uMi0uNi4xYTEuMSAxLjEgMCAwIDAtLjQuNCAxLjggMS44IDAgMCAwLS4yLjYgMy42IDMuNiAwIDAgMC0uMS44djEuM2wuMy42LjQuNC42LjEuNy0uMWEuOS45IDAgMCAwIC40LS40IDEuOCAxLjggMCAwIDAgLjEtLjdabTYuNi0xLjF2LjNjMCAuNCAwIC44LS4yIDEuMmEyLjUgMi41IDAgMCAxLS41LjggMiAyIDAgMCAxLS43LjZsLTEgLjJjLS40IDAtLjYgMC0xLS4yYTIuMSAyLjEgMCAwIDEtLjctLjYgMi42IDIuNiAwIDAgMS0uNS0uOCAzLjYgMy42IDAgMCAxLS4xLTEuMlYxNWwuMS0xLjIuNS0uOGMuMi0uMy41LS40LjgtLjZsMS0uMi45LjIuNy42LjUuOC4yIDEuMlptLTEgLjN2LTEuMWwtLjQtLjdhMS4xIDEuMSAwIDAgMC0uNC0uMyAxLjMgMS4zIDAgMCAwLS42LS4yYy0uMiAwLS40IDAtLjUuMi0uMiAwLS40LjItLjUuM2ExLjggMS44IDAgMCAwLS4zLjcgMy40IDMuNCAwIDAgMCAwIC44djEuMmwuMy42LjUuNGguNWExLjMgMS4zIDAgMCAwIC42IDBsLjQtLjQuMy0uNi4xLS45Wm01LjkgMnYuN2gtMy44di0uNmwxLjgtMmE3IDcgMCAwIDAgLjUtLjdsLjItLjRhMS4xIDEuMSAwIDAgMCAwLS45LjcuNyAwIDAgMC0uMy0uMy45LjkgMCAwIDAtLjQtLjEgMSAxIDAgMCAwLS42LjEuOS45IDAgMCAwLS4zLjRsLS4xLjVoLTFsLjMtLjkuNi0uNmEyIDIgMCAwIDEgMS0uM2wxIC4yLjcuNi4yLjgtLjEuNS0uMy42YTQuNSA0LjUgMCAwIDEtLjMuNWwtLjUuNS0xLjIgMS4zaDIuNlptNC4zLTUuM3Y2aC0xdi02aDFabTMuMSA2aC0uOGExLjkgMS45IDAgMCAxLS43LS41IDIgMiAwIDAgMS0uNC0uN0wyOSAxNlYxNmwuMS0xYTIgMiAwIDAgMSAuNC0uNmwuNi0uNS44LS4xLjguMS42LjUuMy42LjEuOXYuNGgtMy4zdi0uN2gyLjRhMS4zIDEuMyAwIDAgMC0uMS0uNi44LjggMCAwIDAtLjMtLjMuOC44IDAgMCAwLS41LS4yLjguOCAwIDAgMC0uNy40bC0uMi41YTIuNSAyLjUgMCAwIDAgMCAuNnYuNmwuMi41YTEgMSAwIDAgMCAuOS4zIDEuMyAxLjMgMCAwIDAgMS0uNWwuNS41LS4zLjQtLjYuM0gzMVptNC0uNyAxLTMuNWgxTDM1LjUgMThoLS43bC4xLS43Wm0tLjgtMy41IDEgMy41LjEuN2gtLjZsLTEuNS00LjJoMVptNS41IDQuM2MtLjMgMC0uNiAwLS45LS4yYTEuOSAxLjkgMCAwIDEtLjYtLjQgMiAyIDAgMCAxLS40LS43bC0uMi0uOFYxNmMwLS40IDAtLjcuMi0xIDAtLjIuMi0uNC40LS42bC42LS41LjgtLjEuNy4xYy4zLjEuNS4zLjYuNWwuMy42LjIuOXYuNEgzOHYtLjdoMi4zYTEuMyAxLjMgMCAwIDAgMC0uNi44LjggMCAwIDAtLjQtLjMuOC44IDAgMCAwLS41LS4yLjguOCAwIDAgMC0uNC4xLjguOCAwIDAgMC0uMy4zbC0uMi41YTIuNSAyLjUgMCAwIDAgMCAuNnYuNmwuMy41LjMuMi41LjFhMS4zIDEuMyAwIDAgMCAxLS41bC42LjVhMS44IDEuOCAwIDAgMS0xIC43aC0uNlptMy42LTYuMXY2aC0uOXYtNmgxWiIgLz4KICA8cGF0aCBmaWxsPSIjM0ZBNzFBIgogICAgZD0iTTY1LjYgNDl2Mi4ySDUzLjN2LTEuN2w3LjMtMTEuNmgyLjNsLTIuNSA0LjItNC4yIDYuOGg5LjRabS0yLTExdjE3aC0zVjM4aDNabTE1IDd2Mi44YzAgMS40IDAgMi41LS4zIDMuNWE2IDYgMCAwIDEtMS4yIDIuMiA0LjQgNC40IDAgMCAxLTEuNyAxLjMgNiA2IDAgMCAxLTIuMy40IDYuNiA2LjYgMCAwIDEtMS45LS4yIDQuNiA0LjYgMCAwIDEtMS42LS44IDQuOCA0LjggMCAwIDEtMS4xLTEuNSA4IDggMCAwIDEtLjgtMmMtLjItLjktLjItMS44LS4yLTIuOVY0NWMwLTEuMyAwLTIuNC40LTMuNGE2IDYgMCAwIDEgMS0yLjIgNC4zIDQuMyAwIDAgMSAxLjgtMS4zYy43LS4zIDEuNS0uNCAyLjMtLjQuNyAwIDEuNCAwIDIgLjNhNC40IDQuNCAwIDAgMSAxLjUuNyA3IDcgMCAwIDEgMiAzLjVsLjEgMi44Wm0tMi44IDMuMnYtNS40bC0uNC0xLjNhMi42IDIuNiAwIDAgMC0uNi0uOSAyIDIgMCAwIDAtLjgtLjUgMi43IDIuNyAwIDAgMC0xLS4yYy0uNCAwLS44LjEtMS4xLjMtLjQuMi0uNy40LS45LjgtLjIuNC0uNC44LS41IDEuNGwtLjIgMi4ydjMuNmwuMSAxLjhjMCAuNi4yIDEgLjQgMS40bC41IDEgLjguNCAxIC4yIDEuMi0uM2MuMy0uMS42LS40LjgtLjguMy0uNC40LS45LjUtMS41LjItLjYuMi0xLjMuMi0yLjJaTTkyLjMgNDV2Mi44YzAgMS40LS4xIDIuNS0uNCAzLjVhNiA2IDAgMCAxLTEuMSAyLjIgNC40IDQuNCAwIDAgMS0xLjggMS4zIDYgNiAwIDAgMS0yLjMuNCA2LjYgNi42IDAgMCAxLTEuOS0uMiA0LjYgNC42IDAgMCAxLTEuNS0uOCA0LjggNC44IDAgMCAxLTEuMi0xLjUgOCA4IDAgMCAxLS43LTJjLS4yLS45LS4zLTEuOC0uMy0yLjlWNDVjMC0xLjMuMS0yLjQuNC0zLjRhNiA2IDAgMCAxIDEuMS0yLjIgNC4zIDQuMyAwIDAgMSAxLjgtMS4zYy43LS4zIDEuNC0uNCAyLjMtLjQuNyAwIDEuMyAwIDEuOS4zYTQuNCA0LjQgMCAwIDEgMS41LjdjLjUuNC45LjkgMS4yIDEuNS4zLjUuNiAxLjIuNyAyIC4yLjguMyAxLjguMyAyLjhabS0yLjggMy4ydi0zLjZjMC0uNyAwLTEuMy0uMi0xLjggMC0uNS0uMS0xLS4zLTEuM2EyLjYgMi42IDAgMCAwLS42LS45IDIgMiAwIDAgMC0uNy0uNSAyLjcgMi43IDAgMCAwLTEtLjJjLS41IDAtLjguMS0xLjIuM2wtLjkuOGMtLjIuNC0uNC44LS41IDEuNGwtLjIgMi4ydjMuNmMwIC43IDAgMS4zLjIgMS44IDAgLjYuMSAxIC4zIDEuNC4yLjQuMy43LjYgMWwuNy40IDEgLjJjLjUgMCAuOS0uMSAxLjItLjMuMy0uMS42LS40LjktLjhsLjUtMS41LjItMi4yWm0xNC4zLTMuNHYxNUgxMDFWNDIuNGgyLjZsLjIgMi41Wm04LjIgMy43di4zYzAgMSAwIDEuOC0uMyAyLjZhNi4zIDYuMyAwIDAgMS0xIDIgNC40IDQuNCAwIDAgMS0xLjUgMS40Yy0uNi4zLTEuMy40LTIgLjQtLjkgMC0xLjYtLjEtMi4xLS40YTQuMyA0LjMgMCAwIDEtMS41LTEuMyA2LjggNi44IDAgMCAxLTEtMS45IDEyLjIgMTIuMiAwIDAgMS0uNS0yLjR2LTFjMC0uOC4yLTEuNy41LTIuNC4yLS44LjYtMS41IDEtMmE0LjIgNC4yIDAgMCAxIDEuNS0xLjNjLjUtLjMgMS4yLS40IDItLjRzMS41LjEgMi4xLjVhNC4zIDQuMyAwIDAgMSAxLjYgMS4zYy40LjUuNyAxLjIgMSAybC4yIDIuNlptLTIuOC4zdi0uM2wtLjEtMS42YTQuMyA0LjMgMCAwIDAtLjYtMS4zIDIuNSAyLjUgMCAwIDAtLjktMWwtMS4zLS4yYy0uNSAwLS45IDAtMS4yLjJsLTEgLjdhMy4zIDMuMyAwIDAgMC0uNSAxbC0uMyAxLjNWNTBsLjUgMS41Yy4yLjUuNS45IDEgMS4yLjQuMiAxIC40IDEuNi40LjUgMCAuOS0uMSAxLjMtLjNsLjgtMWMuMy0uMy40LS44LjYtMS4zYTcuNyA3LjcgMCAwIDAgLjEtMS42Wm04LTR2MTVoLTIuN1Y0Mi40aDIuNmwuMiAyLjVabTguMyAzLjd2LjNjMCAxIDAgMS44LS4zIDIuNmE2LjMgNi4zIDAgMCAxLTEgMiA0LjQgNC40IDAgMCAxLTEuNSAxLjRjLS42LjMtMS4zLjQtMiAuNC0uOSAwLTEuNi0uMS0yLjEtLjRhNC4zIDQuMyAwIDAgMS0xLjUtMS4zIDYuOCA2LjggMCAwIDEtMS0xLjkgMTIuMiAxMi4yIDAgMCAxLS41LTIuNHYtMWMwLS44LjItMS43LjUtMi40LjItLjguNi0xLjUgMS0yYTQuMiA0LjIgMCAwIDEgMS41LTEuM2MuNS0uMyAxLjItLjQgMi0uNHMxLjUuMSAyLjEuNWE0LjMgNC4zIDAgMCAxIDEuNiAxLjNjLjQuNS43IDEuMiAxIDJsLjIgMi42Wm0tMi44LjN2LS4zbC0uMS0xLjZhNC4zIDQuMyAwIDAgMC0uNi0xLjMgMi41IDIuNSAwIDAgMC0uOS0xbC0xLjMtLjJjLS41IDAtLjkgMC0xLjIuMmwtMSAuN2EzLjMgMy4zIDAgMCAwLS41IDFsLS4zIDEuM1Y1MGwuNSAxLjVjLjIuNS41LjkgMSAxLjIuNC4yIDEgLjQgMS42LjQuNSAwIC45LS4xIDEuMy0uM2wuOC0xYy4zLS4zLjQtLjguNi0xLjNhNy43IDcuNyAwIDAgMCAuMS0xLjZabTgtMy45VjU1SDEyOFY0Mi4zaDIuN2wuMiAyLjZabS0uNCAzLjNoLTFjMC0uOS4yLTEuNy40LTIuNGE1LjggNS44IDAgMCAxIDEtMmMuNC0uNS45LTEgMS41LTEuM2E1IDUgMCAwIDEgMi4yLS40Yy42IDAgMS4xIDAgMS42LjJzLjkuNSAxLjIuOGMuNC40LjcuOC45IDEuNC4yLjYuMyAxLjMuMyAyVjU1aC0yLjl2LTguMmMwLS42IDAtMS4xLS4zLTEuNWExLjYgMS42IDAgMCAwLS43LS43Yy0uNC0uMi0uOC0uMi0xLjItLjItLjYgMC0xIDAtMS40LjMtLjQuMi0uNy40LS45LjhhMy44IDMuOCAwIDAgMC0uNSAxLjIgNi40IDYuNCAwIDAgMC0uMiAxLjVabTcuOS0uOC0xLjMuM2MwLS43IDAtMS41LjMtMi4xLjItLjcuNS0xLjMgMS0xLjhhNC41IDQuNSAwIDAgMSAxLjUtMS4zYy42LS4zIDEuMy0uNCAyLjEtLjQuNiAwIDEuMiAwIDEuNy4zYTMuMiAzLjIgMCAwIDEgMS4zLjhjLjQuNC43LjguOSAxLjQuMi42LjIgMS40LjIgMi4yVjU1aC0yLjh2LTguMmMwLS43IDAtMS4yLS4zLTEuNS0uMS0uNC0uNC0uNi0uNy0uNy0uNC0uMi0uOC0uMi0xLjItLjItLjUgMC0uOSAwLTEuMi4yYTIuMyAyLjMgMCAwIDAtLjguNyAzIDMgMCAwIDAtLjYgMWwtLjEgMS4xWiIgLz4KICA8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9Ijc2IiB5PSI4NCIgZmlsbD0iI2ZmZiIgcng9IjQiIC8+CiAgPHJlY3Qgd2lkdGg9IjE5OSIgaGVpZ2h0PSI3NSIgeD0iLjUiIHk9Ijg0LjUiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9Ii4xIiByeD0iMy41IiAvPgogIDxwYXRoIGZpbGw9IiMwMDAiIGZpbGwtb3BhY2l0eT0iLjkiCiAgICBkPSJNMTAuNyA5OS45SDkuM1Y5OWgxLjRsLjctLjFhLjguOCAwIDAgMCAuMy0uNGwuMS0uNXYtLjVhLjkuOSAwIDAgMC0uNC0uNGwtLjctLjFIOS42djQuOWgtMXYtNS43aDIuMWMuNSAwIC45IDAgMS4yLjJsLjcuNy4yIDFjMCAuMyAwIC42LS4yLjlhMS42IDEuNiAwIDAgMS0uNy42bC0xLjIuMlptMy42LTMuNmgxbDEuNiA0LjQgMS42LTQuNGguOWwtMi4yIDUuN2gtLjdsLTIuMi01LjdabS0uNCAwaC45bC4xIDMuOHYxLjloLTF2LTUuN1ptNSAwaC45djUuN2gtMXYtMS45bC4xLTMuOFptNiA1di43SDIxdi0uNmwxLjktMmE3IDcgMCAwIDAgLjUtLjdsLjItLjRhMS4xIDEuMSAwIDAgMCAwLS45LjcuNyAwIDAgMC0uMy0uMy45LjkgMCAwIDAtLjUtLjEgMSAxIDAgMCAwLS41LjEuOS45IDAgMCAwLS4zLjRsLS4xLjVoLTFsLjMtLjljLjEtLjIuMy0uNS42LS42YTIgMiAwIDAgMSAxLS4zbDEgLjJjLjMuMi41LjMuNi42LjIuMi4yLjUuMi44di41bC0uMy42YTQuNSA0LjUgMCAwIDEtLjQuNWwtLjQuNS0xLjMgMS40SDI1Wm0xIC4yYzAtLjEgMC0uMi4yLS4zYS41LjUgMCAwIDEgLjQtLjJjLjIgMCAuMyAwIC40LjJhLjUuNSAwIDAgMSAuMS4zbC0uMS40YS41LjUgMCAwIDEtLjQuMS41LjUgMCAwIDEtLjQtLjEuNS41IDAgMCAxLS4yLS40Wm0zLjQtMi4xLS44LS4yLjQtMi45aDN2LjhoLTIuM2wtLjEgMS40YTEuOCAxLjggMCAwIDEgLjQtLjFsLjUtLjEuNy4xYy4yIDAgLjQuMi41LjQuMi4xLjMuNC40LjZhMi41IDIuNSAwIDAgMSAwIDEuNWMwIC4yLS4yLjQtLjQuNmExLjYgMS42IDAgMCAxLS41LjQgMi4xIDIuMSAwIDAgMS0uOS4ybC0uNi0uMWExLjkgMS45IDAgMCAxLS42LS4zIDEuNiAxLjYgMCAwIDEtLjQtLjUgMS43IDEuNyAwIDAgMS0uMi0uN2guOWMwIC4xIDAgLjMuMi40IDAgLjIuMS4zLjMuM2wuNC4xYTEgMSAwIDAgMCAuNCAwIC44LjggMCAwIDAgLjMtLjNsLjItLjRhMS44IDEuOCAwIDAgMCAwLS45IDEgMSAwIDAgMC0uMi0uNC44LjggMCAwIDAtLjMtLjIgMSAxIDAgMCAwLS41IDBoLS41YTEuNiAxLjYgMCAwIDAtLjMuM1oiIC8+CiAgPGcgY2xpcC1wYXRoPSJ1cmwoI2EpIj4KICAgIDxwYXRoIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9Ii4xIiBzdHJva2Utd2lkdGg9Ii4zIiBkPSJNNy41IDE0MS43SDE5MSIgLz4KICAgIDxwYXRoIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9Ii45IiBzdHJva2Utd2lkdGg9IjEuMyIKICAgICAgZD0iTTguNyAxNDQuOGg5LjhsMTMgMi4yIDExLjItOCAyMS42IDUuOCAyNC44LTUuOSA3LjIuMmM0LjggMi4yIDE0LjUgNy45IDE1IDcuOWgxNC41bDEyLjQtN2gxMC40bDkuMi00IDEyLjQgMS4xIDE5LjYgMi44IiAvPgogICAgPHBhdGggZmlsbD0iIzAwMCIgZmlsbC1vcGFjaXR5PSIuNCIgZmlsbC1ydWxlPSJldmVub2RkIgogICAgICBkPSJNOC41IDEzNS41SDh2MmguNVYxMzZIMTB2LS41SDguNVptMCAxMS41di0xLjZIOHYyLjFoMnYtLjVIOC41Wm0xODMtMTEuNWguNXYyaC0uNVYxMzZIMTkwdi0uNWgxLjVabTAgMTEuNXYtMS42aC41djIuMWgtMnYtLjVoMS41WiIKICAgICAgY2xpcC1ydWxlPSJldmVub2RkIiAvPgogIDwvZz4KICA8cGF0aCBmaWxsPSIjRkZBNjAwIgogICAgZD0iTTc4LjEgMTIwLjNoMS43Yy42IDAgMS4yIDAgMS42LS4zLjUtLjIuOC0uNSAxLTEgLjItLjMuMy0uOC4zLTEuMyAwLS42IDAtMS0uMy0xLjRhMiAyIDAgMCAwLS44LS45IDMgMyAwIDAgMC0xLjUtLjMgMyAzIDAgMCAwLTEuNC4zIDIuMyAyLjMgMCAwIDAtMSAuOGMtLjIuNC0uMy44LS4zIDEuM2gtMi44YzAtLjkuMi0xLjcuNy0yLjQuNS0uNyAxLjEtMS4yIDItMS42LjgtLjUgMS43LS43IDIuNy0uN2E3IDcgMCAwIDEgMyAuNmMuOC40IDEuNC45IDEuOSAxLjYuNC43LjYgMS42LjYgMi43IDAgLjUgMCAxLS4zIDEuNS0uMi41LS42IDEtMSAxLjMtLjQuNC0xIC44LTEuNyAxLS42LjMtMS40LjQtMi4zLjRoLTJ2LTEuNlptMCAyLjJWMTIxaDIuMWMxIDAgMS45LjEgMi42LjMuNy4zIDEuMy42IDEuNyAxIC41LjQuOC45IDEgMS40LjIuNS4zIDEgLjMgMS43YTUgNSAwIDAgMS0uNCAyLjEgNC4zIDQuMyAwIDAgMS0xLjIgMS42IDUuNCA1LjQgMCAwIDEtMS45IDEgNy41IDcuNSAwIDAgMS0yLjIuMyA3LjMgNy4zIDAgMCAxLTIuMi0uMyA1LjggNS44IDAgMCAxLTEuOC0xIDQuNSA0LjUgMCAwIDEtMS4yLTEuNGMtLjMtLjctLjUtMS40LS41LTIuMmgyLjhjMCAuNi4yIDEgLjQgMS40YTIuNSAyLjUgMCAwIDAgMSAuOWMuNC4yIDEgLjMgMS41LjMuNiAwIDEuMSAwIDEuNS0uM2EyLjIgMi4yIDAgMCAwIDEtMWMuMy0uMy40LS44LjQtMS40YTMgMyAwIDAgMC0uNC0xLjYgMi4zIDIuMyAwIDAgMC0xLjEtMWMtLjUtLjItMS0uMy0xLjctLjNINzhabTIxLjUtMi4zdjIuOGMwIDEuMy0uMiAyLjQtLjQgMy40YTYgNiAwIDAgMS0xLjIgMi4zIDQuNCA0LjQgMCAwIDEtMS43IDEuMyA2IDYgMCAwIDEtMi4zLjQgNi41IDYuNSAwIDAgMS0xLjktLjMgNC42IDQuNiAwIDAgMS0xLjUtLjggNC44IDQuOCAwIDAgMS0xLjItMS40IDggOCAwIDAgMS0uOC0yLjFjLS4yLS44LS4yLTEuOC0uMi0yLjh2LTIuOGMwLTEuNCAwLTIuNS40LTMuNGE2IDYgMCAwIDEgMS0yLjMgNC4zIDQuMyAwIDAgMSAxLjktMS4zIDYuNSA2LjUgMCAwIDEgNC4yLS4xIDQuNCA0LjQgMCAwIDEgMS41LjhjLjQuMy44LjggMS4xIDEuNC40LjYuNiAxLjMuOCAyIC4yLjkuMyAxLjguMyAyLjlabS0yLjkgMy4yVjExOGwtLjQtMS40YTIuNiAyLjYgMCAwIDAtLjYtLjkgMiAyIDAgMCAwLS44LS41IDIuNyAyLjcgMCAwIDAtMS0uMWMtLjQgMC0uOCAwLTEuMS4yLS40LjItLjcuNS0uOS44bC0uNSAxLjUtLjIgMi4ydjMuNmwuMSAxLjhjMCAuNS4yIDEgLjQgMS4zbC41IDEgLjguNSAxIC4xYy40IDAgLjggMCAxLjItLjIuMy0uMi42LS41LjgtLjkuMy0uMy40LS44LjYtMS40bC4xLTIuMlptMTAuMi40aDF2Ni4zaC0uOWwtLjEtMS40di00LjlabS4yIDMuNGguNGwtLjEgMS4yYTMuMSAzLjEgMCAwIDEtLjQgMSAxLjggMS44IDAgMCAxLS42LjZjLS4zLjItLjYuMy0xIC4zLS4zIDAtLjUgMC0uOC0uMmExLjUgMS41IDAgMCAxLS42LS4zIDIgMiAwIDAgMS0uNC0uN2wtLjItMS43aC43djFjMCAuMi4yLjUuNC42IDAgLjIuMi4yLjQuM2gxLjNsLjYtLjUuMi0uN2E0LjYgNC42IDAgMCAwIDAtMVptLTQuMS0zLjRoMXY4LjhoLTF2LTguOFptMTEuMiAwaDF2Ni4yYzAgLjYtLjEgMS0uMyAxLjRhMi4yIDIuMiAwIDAgMS0xIDEgMyAzIDAgMCAxLTIuMS4xIDMuMSAzLjEgMCAwIDEtMS0uMyAyLjIgMi4yIDAgMCAxLS42LS43bC42LS42LjguNy45LjIuOC0uMi42LS42Yy4yLS4zLjItLjYuMi0xdi00LjhsLjEtMS40Wk0xMTAgMTI3bC4xLTEuNC41LTFhMi4yIDIuMiAwIDAgMSAxLjktMWMuNCAwIC43LjIgMSAuMy4zLjEuNi4zLjguNmwuNSAxYTYgNiAwIDAgMSAuMiAxLjJ2LjVhNS41IDUuNSAwIDAgMS0uMiAxLjMgMyAzIDAgMCAxLS41IDEgMiAyIDAgMCAxLS44LjYgMi41IDIuNSAwIDAgMS0xIC4yYy0uNCAwLS44LS4xLTEtLjNhMi40IDIuNCAwIDAgMS0uOS0uNyAzLjIgMy4yIDAgMCAxLS41LTEgNC42IDQuNiAwIDAgMS0uMS0xLjNabTEgMCAuMSAxIC4zLjcuNi41LjcuMWMuNCAwIC43IDAgMS0uMmwuNS0uNi4zLS44di0xLjRhMi45IDIuOSAwIDAgMC0uMi0uNiAxLjkgMS45IDAgMCAwLS4zLS42IDEuNSAxLjUgMCAwIDAtLjYtLjQgMS43IDEuNyAwIDAgMC0uNy0uMWMtLjMgMC0uNSAwLS43LjJhMS41IDEuNSAwIDAgMC0uNi41IDIuNCAyLjQgMCAwIDAtLjMuNyA0IDQgMCAwIDAgMCAxWm0xMC4xLTUuNC0zLjUgOS4zaC0xbDMuNi05LjNoMVptMi43IDMuNXY1aC0xdi02LjNoMXYxLjNabS0uMiAxLjZoLS41bC4xLTEuMi41LTEgLjgtLjYgMS4xLS4yLjkuMWExLjcgMS43IDAgMCAxIDEgMS4xbC4yIDF2NC4yaC0xVjEyNmMwLS40LS4xLS42LS4zLS44YTEgMSAwIDAgMC0uNC0uNSAxLjcgMS43IDAgMCAwLS43LS4xYy0uNCAwLS42IDAtLjguMmExLjQgMS40IDAgMCAwLS42LjRsLS4yLjdhMy44IDMuOCAwIDAgMC0uMS44Wm00LS42LS42LjNhMy4yIDMuMiAwIDAgMSAuMS0xIDIuNyAyLjcgMCAwIDEgLjUtLjljLjItLjMuNS0uNC44LS42bDEtLjIgMSAuMWMuMi4xLjUuMi42LjRsLjUuNy4xIDF2NC4yaC0xVjEyNmMwLS40LS4xLS43LS4zLS45YS45LjkgMCAwIDAtLjQtLjQgMS45IDEuOSAwIDAgMC0uNy0uMWwtLjcuMWExLjMgMS4zIDAgMCAwLS40LjRjLS4yLjEtLjMuMy0uMy40YTEuOCAxLjggMCAwIDAtLjEuNlptNi44LTIuNmguOWEuNi42IDAgMCAwIC4zLS4zLjYuNiAwIDAgMCAuMS0uMy42LjYgMCAwIDAtLjItLjRsLS42LS4yYTEgMSAwIDAgMC0uNS4xLjUuNSAwIDAgMC0uMi40aC0xYzAtLjIuMS0uNS4zLS42LjEtLjIuMy0uNC42LS41bC44LS4yIDEgLjJjLjIgMCAuNC4yLjUuNC4yLjIuMi41LjIuOGExIDEgMCAwIDEtLjEuNWwtLjYuNC0uOS4yaC0uNnYtLjVabTAgLjd2LS40aC42bDEgLjFjLjIgMCAuNC4yLjUuNC4yLjIuMi40LjIuNiAwIC4zIDAgLjYtLjIuOGExLjUgMS41IDAgMCAxLS43LjRsLS45LjItLjgtLjFhMS41IDEuNSAwIDAgMS0uNy0uNWwtLjItLjhoMWwuMS41LjcuMS42LS4xYS42LjYgMCAwIDAgLjItLjUuNi42IDAgMCAwLS4xLS40LjYuNiAwIDAgMC0uMy0uMmgtMVoiIC8+CiAgPHBhdGggZmlsbD0idXJsKCNiKSIgZmlsbC1vcGFjaXR5PSIuMyIgZD0iTTE1OCAxMjAuN2g1LjR2Ni43bC0yLjcgMi0yLjctMnYtNi43WiIgLz4KICA8cGF0aCBmaWxsPSIjMDAwIiBmaWxsLW9wYWNpdHk9Ii45IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMTYwLjUgMTI5LjhhLjMuMyAwIDAgMCAuNSAwbDIuNC0yLjQtLjUtLjUtMS4zIDEuM2EuMy4zIDAgMCAxLS41LS4ydi0zLjNoLS43djMuM2EuMy4zIDAgMCAxLS41LjJsLTEuNC0xLjMtLjUuNSAyLjUgMi40WiIKICAgIGNsaXAtcnVsZT0iZXZlbm9kZCIgLz4KICA8cGF0aCBmaWxsPSIjMDAwIiBmaWxsLW9wYWNpdHk9Ii45IgogICAgZD0iTTE3MC4zIDEyNS4ydjFoLTMuMXYtMWgzWm01LTQuNHY4LjZIMTc0di03bC0yIC44VjEyMmwzLjMtMS4yaC4yWm03LjQgMGguMXYxLjFjLS42IDAtMSAuMS0xLjQuM2EyLjQgMi40IDAgMCAwLS45LjZsLS41IDEtLjIgMS4xdjEuNGwuMS45LjQuNi40LjQuNi4yYy4yIDAgLjQgMCAuNi0uMmwuNC0uMy4zLS42YTIuNSAyLjUgMCAwIDAgMC0xLjQgMS43IDEuNyAwIDAgMC0uMy0uNSAxLjIgMS4yIDAgMCAwLS40LS40IDEuMiAxLjIgMCAwIDAtLjYtLjJsLS44LjJhMS42IDEuNiAwIDAgMC0uNS42IDEuMyAxLjMgMCAwIDAtLjMuNmwtLjUtLjJhMyAzIDAgMCAxIC4yLTFsLjYtLjcuNy0uNCAxLS4yYy40IDAgLjcgMCAxIC4ybC44LjdjLjIuMi4zLjUuNC45YTMuNyAzLjcgMCAwIDEgMCAyLjJjLS4xLjQtLjMuNy0uNiAxYTIuNSAyLjUgMCAwIDEtLjguNmwtMS4yLjJjLS40IDAtLjktLjEtMS4yLS4zYTIuNyAyLjcgMCAwIDEtMS0uNyAzLjMgMy4zIDAgMCAxLS41LTEgNC4yIDQuMiAwIDAgMS0uMi0xLjR2LS41YzAtLjcuMS0xLjMuMy0xLjlhNC41IDQuNSAwIDAgMSAuNy0xLjUgMy41IDMuNSAwIDAgMSAxLjMtMWMuNi0uMyAxLjItLjQgMi0uNFptMi44IDMuN3YtLjNsLjEtLjcuNS0uNS43LS4xLjcuMS40LjUuMi43di4zYzAgLjMgMCAuNS0uMi43YTEuMiAxLjIgMCAwIDEtLjQuNWwtLjcuMS0uNy0uMWExLjIgMS4yIDAgMCAxLS41LS41IDEuNCAxLjQgMCAwIDEtLjEtLjdabS42LS4zdi43YS43LjcgMCAwIDAgLjMuMy43LjcgMCAwIDAgLjQuMS43LjcgMCAwIDAgLjQgMCAuNy43IDAgMCAwIC4yLS40IDEgMSAwIDAgMCAwLS40di0uM2ExIDEgMCAwIDAgMC0uNC42LjYgMCAwIDAtLjYtLjQuNy43IDAgMCAwLS40LjEuNy43IDAgMCAwLS4zLjMgMSAxIDAgMCAwIDAgLjRabTIuNSA0di0uNGwuMS0uNy41LS40LjctLjJjLjMgMCAuNSAwIC43LjIuMiAwIC4zLjIuNC40bC4yLjd2LjNjMCAuMyAwIC41LS4yLjdhMS4yIDEuMiAwIDAgMS0uNC41IDEuMyAxLjMgMCAwIDEtLjcuMmwtLjctLjJhMS4zIDEuMyAwIDAgMS0uNS0uNSAxLjQgMS40IDAgMCAxLS4xLS43Wm0uNi0uNHYuN2wuMy4zYS43LjcgMCAwIDAgLjQuMS43LjcgMCAwIDAgLjQgMCAuNy43IDAgMCAwIC4yLS40di0uN2ExIDEgMCAwIDAgMC0uNC42LjYgMCAwIDAtLjYtLjQuNi42IDAgMCAwLS42LjQgMSAxIDAgMCAwLS4xLjRabS45LTQtMy4xIDUtLjUtLjIgMy4xLTUgLjUuM1oiIC8+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImIiIHgxPSIxNjAuNSIgeDI9IjE2MC41IiB5MT0iMTI5LjkiIHkyPSIxMjAuNyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0eWxlPSJzdG9wLWNvbG9yOiNmZmY7c3RvcC1vcGFjaXR5OjEiIC8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3R5bGU9InN0b3AtY29sb3I6I2ZmZjtzdG9wLW9wYWNpdHk6MCIgLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICA8Y2xpcFBhdGggaWQ9ImEiPgogICAgICA8cGF0aCBmaWxsPSIjZmZmIiBkPSJNOCAxMzUuNWgxODR2MTJIOHoiIC8+CiAgICA8L2NsaXBQYXRoPgogIDwvZGVmcz4KPC9zdmc+", - "description": "Contains widgets displaying air quality telemetry.", - "order": 12600, - "externalId": null, - "name": "Air quality" - }, - "widgetTypeFqns": [ - "air_quality_card", - "air_quality_card_with_background", - "horizontal_air_quality_card", - "horizontal_air_quality_card_with_background", - "air_quality_chart_card", - "air_quality_chart_card_with_background", - "simple_air_quality_chart_card", - "simple_air_quality_chart_card_with_background", - "humidity_card", - "humidity_card_with_background", - "horizontal_humidity_card", - "horizontal_humidity_card_with_background", - "humidity_chart_card", - "humidity_chart_card_with_background", - "simple_humidity_chart_card", - "simple_humidity_chart_card_with_background", - "humidity_progress_bar", - "humidity_progress_bar_with_background", - "co2_card", - "co2_card_with_background", - "horizontal_co2_card", - "horizontal_co2_card_with_background", - "co2_chart_card", - "co2_chart_card_with_background", - "simple_co2_chart_card", - "simple_co2_chart_card_with_background", - "pm2_5_card", - "pm2_5_card_with_background", - "horizontal_pm2_5_card", - "horizontal_pm2_5_card_with_background", - "pm2_5_chart_card", - "pm2_5_chart_card_with_background", - "simple_pm2_5_chart_card", - "simple_pm2_5_chart_card_with_background", - "pm10_card", - "pm10_card_with_background", - "horizontal_pm10_card", - "horizontal_pm10_card_with_background", - "pm10_chart_card", - "pm10_chart_card_with_background", - "simple_pm10_chart_card", - "simple_pm10_chart_card_with_background", - "radon_level_card", - "radon_level_card_with_background", - "horizontal_radon_level_card", - "horizontal_radon_level_card_with_background", - "radon_level_chart_card", - "radon_level_chart_card_with_background", - "simple_radon_level_chart_card", - "simple_radon_level_chart_card_with_background", - "volatile_organic_compounds_card", - "volatile_organic_compounds_card_with_background", - "horizontal_volatile_organic_compounds_card", - "horizontal_volatile_organic_compounds_card_with_background", - "volatile_organic_compounds_chart_card", - "volatile_organic_compounds_chart_card_with_background", - "simple_volatile_organic_compounds_chart_card", - "simple_volatile_organic_compounds_chart_card_with_background", - "individual_allergy_index_card", - "individual_allergy_index_card_with_background", - "horizontal_individual_allergy_index_iai_card", - "horizontal_individual_allergy_index_iai_card_with_background", - "individual_allergy_index_iai_chart_card", - "individual_allergy_index_iai_chart_card_with_background", - "simple_individual_allergy_index_iai_chart_card", - "simple_individual_allergy_index_iai_chart_card_with_background2", - "ozone_o3_card", - "ozone_o3_card_with_background", - "horizontal_ozone_o3_card", - "horizontal_ozone_o3_card_with_background", - "ozone_o3_chart_card", - "ozone_o3_chart_card_with_background", - "simple_ozone_o3_chart_card", - "simple_ozone_o3_chart_card_with_background", - "nitrogen_dioxide_no2_card", - "nitrogen_dioxide_no2_card_with_background", - "horizontal_nitrogen_dioxide_no2_card", - "horizontal_nitrogen_dioxide_no2_card_with_background", - "nitrogen_dioxide_no2_chart_card", - "nitrogen_dioxide_no2_chart_card_with_background", - "simple_nitrogen_dioxide_no2_chart_card", - "simple_nitrogen_dioxide_no2_chart_card_with_background", - "sulfur_dioxide_so2_card", - "sulfur_dioxide_so2_card_with_background", - "horizontal_sulfur_dioxide_so2_card", - "horizontal_sulfur_dioxide_so2_card_with_background", - "sulfur_dioxide_so2_chart_card", - "sulfur_dioxide_so2_chart_card_with_background", - "simple_sulfur_dioxide_so2_chart_card", - "simple_sulfur_dioxide_so2_chart_card_with_background", - "carbon_monoxide_co_card", - "carbon_monoxide_co_card_with_background", - "horizontal_carbon_monoxide_co_card", - "horizontal_carbon_monoxide_co_card_with_background", - "carbon_monoxide_co_chart_card", - "carbon_monoxide_co_chart_card_with_background", - "simple_carbon_monoxide_co_chart_card", - "simple_carbon_monoxide_co_chart_card_with_background" - ] -} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json index 2701abcb84..2168ab7595 100644 --- a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json @@ -14,6 +14,7 @@ "home_page_widgets.documentation_links", "home_page_widgets.dashboards", "home_page_widgets.usage_info", - "api_usage" + "api_usage", + "home_page_widgets.iot_hub" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/indoor_environment.json b/application/src/main/data/json/system/widget_bundles/indoor_environment.json deleted file mode 100644 index d13fc5ba66..0000000000 --- a/application/src/main/data/json/system/widget_bundles/indoor_environment.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "widgetsBundle": { - "alias": "indoor_environment", - "title": "Indoor Environment", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMkAAAChCAMAAABav2sAAAABcVBMVEUAAAAAAADf39/09PTw8PD9/f3AwMDy8vL39/fy8vL19fXw8PDw8PD////39/fz8/MjTMf6+vrj4+P29vbx8fHn5+fHx8dYWFg/pxra2tp0dHSQkJDt7e3h4eFmZmbV1dWsrKz/pgBaedW5ubmCgoKf04z/3aDdNVGenp71xc3T09PCwsL64ubI0vHOzs4+Ys7wqLTIyMirq6uRpePYGDjk6fisvOr/9N91j9zri5v/6b9KSkr/+u8wV8u33qniUmnn9OODmt//78//46/z+vHW3fXgQ13/sSDnb4L/vkXx9PufsOa7u7v/2JD/04D/zW//x2DaJkXy9Pv98fPc3NxYszi6x+6esefPz8//6L/umqiHyHA9PT3/rBD309rP6ca2trYsLCzb79TLy8ur2ZrpfY91wFpLrCiDm99MbdHzt8HD47jtmaiTzX7aJkQxV8rtmqj/2Y+JiYmTzn7lYHZUVFQ8PDz/tzBohNnNzc2Pj4/UlCD8AAAADXRSTlMAByDvv/Eqr8W577Cvv6N3egAACkpJREFUeNrsms9v0zAUgBEgceHhxNiOnNhJ10RVlGqp0qpd1a2HtqqqwdjQDoNJTIIbnBBI/PzreQ5ts0FBK91GOvVbE7t2V/ub/ZzW2Z07d+/fe3RtPH549w5yI63cvQfXyj1s5GZaeQDXzH0UuZFWHsM1cw9FbqSVR3DNPEKRG2hlY7IxmXNbTFqD9jWaxCwIMWGeF0BOJYAZERREq5u87TfeH8MMIQS9SpPwTei9YQDUDxwQIWMVV7AKZaJCARwIQiYgxApwVjd51xpY2zCFVV7FAaPADIAZwIRSoIz5DJP86TImwotDMxrUqTgQx7GLJo7YpU4zQhPmiCiibsWLxBWYNFpjNJnjUOpEses5nsM+xw71feFEzcgPXgUOVu6GkecvFSeVCoAx8Tzq4+8yDzvtBy51sa1g12sKv+n53tWYPL1oIowJNT/YZOxHTQfCeBefGBMXPi9nInZ3cxMX8C18fOddEbke/WkCThTCGxo4UXAdY8KcMPxp8iZ0hF8RDrjeK+oLP3xFXZRrlnTtalQH503MnBYsAPPjCAbUZJgIgFIamBACwcpqYlnWCSykuVbXk5aFNI5hKZY3IVrj2bbBxkRh7ueDED7N29Ikq5g0LMPTeYvwK6oowo6A/jcTiV2VicrkAQ90CjqVGp9rqQIJGZeJraR8KckKJmMrpwtT6gBa2ZK8VqBsxbXNM641r2MRqNeEaHyBxEQRSZYx4djvTIKUkhOeAOdEa805PiTolzIBLH3JYQWTJ1bOoDCxk9c6ldjhlKR1ldQlT1+TNMvykkzhC9J6RrIDlZUsTr5aSL8NUw64TvHI6gQymR4kso4T4jWpp1rnbjgmWh6kPMt42UyedC3rXbEKc25zDhxDEQgndQWE2FyBwqI8iFR+qDQDXra1C2nDn1CwmNxrKZNiYbLzlGC6nt9PuOSJUpxwzBFC8BRwpYlS62eiucQ1imuCAtLAucQivnYmhCiOvVfERhWjQRRmuA7WzuQP8PWbXXM2OxIbk43JxmRjAr+wMdmY3DKTvT087RhMZu/0dG89TU6PzmoAvS3DPsAzTCY9WIj9+DGeqQdBDAErmcneBHuOJqOto1qtNoKdrcmwd3a2eFSYpDZQ5oHrsaZXMpOd571naIJDcQoIpiOAQzwtgn9PbCbAA0+ENBblMkFqxuRo67Q2wpF4vjU0Jc9gEckrFxABtAKMBeU02d9CJjtosmOC5hAWQlmZI/6nyehoCB+29guTK127jlstgE51xklxE68zrW/0+9XO8VWY5Ey2LmFCALHNcUmT4xdVy6qafe8ZJzORQTe/OTHuWzmN9hWYnA7BTDE4msfJcAeQvfyYY0sSaM0J15JfMk5OsJu5SXs756llTTvc6AL0B1jRtxov2mOsqF6BydkZwM7ZBJ8cTsP+WW2vNuz1Rr3RcFjsZnASSIRznpDLmbSs6ot35/rYtbrz+0Uo8RRLpm44aNsrm2CMTI6+YAavL8/3zRUSjo6eQa3X26nVcHTmEDshPFCEEHXJOBk/AagWJm3LGk+zT61+HyfUE2vqhpnBSiaj/Z7x2Z/s1/ILzJfJoZlPh7Ve7XA03BuNVo34CybdIgvjQQs1t+e3Wra3x9f1CbIHhqs0edKwOnCRDk6qtfksXJh0rAbcDpOG1b0dJtjr9gKTk+VMNNFKK0D+n0n19yGB8TziW63O5Uy41JwD8t9M2gsnUr+oHVzOREt8AHAN/0rAijxJljfpWo2F106rk9cue2VU8K/ErutGIZ2pLG3SzrtcUKzMVveF+XQ2uKGIDyI8sdB1xT/OroH1/hgW0EYVBEVWMFnwlYNh0SKoP8s0nQpdyqTT6hTJIk4G3e6LbVjJxHUoXEC4XuSIX+UoDc6/UODAlOybFnVpVIEC5sXm7DA4T+QhF41p2Uw8iod37m9N5yFRUInLud9lk4IgIsi3TwEx0I8xmfLxgMyhn8hC/rvJeTwxDeZwOiBzfFbMLVr+PUjmzpW8PEIKRARTaFTa3VTmuq5fAaQiYEbTp3CBaFbn0NKa5DSNiwt/hjmQEzdLucOtOQCfr1POXzsp3IrBgz/A/6sJ5wDFXoKAvyJyGCzG/s8mGi5yu+4EbUw2JhuTWRsq//9sk+Rok7UBEtskiL02Jhx4XaUqBc6llEEiX2Yqk5BKDkkqM635uphoLusqUQnIVGI+SV5KniqQAYc0SOpaq3UxOYeGX1FyjeLkPOsd8QUbkx/t3G9r2kAAx3HcZtdu/DYWjzu5lDOQhCQzD4L4RATNAxGfivWJDmXt1sG6lsH2/tklzLZZmfSW5NzGfQpXEfH8kvgX7kyJKfl1lqeoWUuGaJnlGWr2UoZomKX+vSpePJEhemZ58rz1qjZPn+UhGmYxjP9Co0badgySjo6bqM/JkbZ9iY6aqFXzSNe+RCeo2bGmfYkaqFuzoeGznZYSmJJHMyWK/s8SasHBLYcAnuUQgtKKJQQQBEDHhUWJ7RA4XgclFUtsCgYqg6hruXBJx2UW9TglnXI1xRLCbdsTAIgAp6LTsym3BKEuSimWMP4ODMzhHvccbjE6pHJWNxQWyiiWgIbwXEhyBjCAhQ5AmEtRUvGY5CU5TjkYtR2HCIFyiiUQtsdIlsQ5hrDkfHBIT1RdMrR3JZaw5TGhAl4P5RRLCIcgyLzvIBTCJczjRPCys+Qle5GSJ9ff8ypsUZTy95SUZUoUmRIFpkSRKVFgShSZEgWmRFWxJPXPgZHv+x9QmcOUrNsjYLm8vFyjMgcpGS2zkvYVqqW/ZLIcyZJJ+5s/QpX0l1yvIUvST5frT9fAyN+mqID+EpmQZiUZvz3B9dWHEaqgv2TdzvjpFhjJksm5f56iAvpLJmmatj9Mtu2ryeUSMmdbzYux/hIpP7v8dnt5DmA7+eajPL0lRZN0dwEVqL6E2ExwgqJDfFpZbADECyBKIiAfizbfsRenQIehSH/JZjwfIBn3IyTBx2CFZJ6NBVGAfSwOyaYo0F/yOZnKkkiWjFf4MsV4gM8BgC/RIMFiIEdZkgxi3EdcK/8nGLOI+HkZBfpLkD12QJYE2WPGVI59AMH0azCI+7Ov8yjqz2Y3C9xxmSN6ALwQlhAucqGN+8tGD18S7EoSeYTifozZTF6L6Qp3GAGGFijHfT3BhlwwB7niO+Pv+a0/8ErpmCRIgqxkIEvyG93KCzpsKIMesuzwQcmnPSXLVuu18l9rf8n3GT6O8XGG1TgrWWE2jvsbTAfRTbwIPuMWt7DXg5LU/730zR/A/pI4CII4G+eb/HkSzOO4P58HcXQznY9xy/bwCIf99hvfG4NkAciza3fdDmUhHueu5LR7ikqcAfKuLi6U3uODBHkJikJB8AjFki5cVMEKcXHqnp11Wyol8SIfoe5hiVtRSX5HsuTitFBSAw0l3ZZrvVU4JiXU/Dxpdc/w9i1y5pc7RaZEgSlRZEoUmBJFpkSBKVFkShSYEmUNPesZG03UraFpPeMxanaiYaWhppU02lYaHp2gPs1jGbLPv7Xi7AcB+P0LpI8HcAAAAABJRU5ErkJggg==", - "description": "Contains widgets displaying indoor environment telemetry.", - "order": 10000, - "externalId": null, - "name": "Indoor Environment" - }, - "widgetTypeFqns": [ - "indoor_temperature_card", - "indoor_temperature_card_with_background", - "indoor_horizontal_temperature_card", - "indoor_horizontal_temperature_card_with_background", - "indoor_temperature_chart_card", - "indoor_temperature_chart_card_with_background", - "indoor_simple_temperature_chart_card", - "indoor_simple_temperature_chart_card_with_background", - "indoor_temperature_progress_bar", - "indoor_temperature_progress_bar_with_background", - "indoor_temperature_range_chart", - "indoor_temperature_range_chart_with_background", - "indoor_temperature_gauge", - "indoor_humidity_card", - "indoor_humidity_card_with_background", - "indoor_horizontal_humidity_card", - "indoor_horizontal_humidity_card_with_background", - "indoor_humidity_chart_card", - "indoor_humidity_chart_card_with_background", - "indoor_simple_humidity_chart_card", - "indoor_simple_humidity_chart_card_with_background", - "indoor_humidity_progress_bar", - "indoor_humidity_progress_bar_with_background", - "indoor_co2_card", - "indoor_co2_card_with_background", - "indoor_horizontal_co2_card", - "indoor_horizontal_co2_card_with_background", - "indoor_co2_chart_card", - "indoor_co2_chart_card_with_background", - "indoor_simple_co2_chart_card", - "indoor_simple_co2_chart_card_with_background", - "indoor_illuminance_card", - "indoor_illuminance_card_with_background", - "indoor_horizontal_illuminance_card", - "indoor_horizontal_illuminance_card_with_background", - "indoor_illuminance_chart_card", - "indoor_illuminance_chart_card_with_background", - "indoor_simple_illuminance_chart_card", - "indoor_simple_illuminance_chart_card_with_background", - "indoor_illuminance_progress_bar", - "indoor_illuminance_progress_bar_with_background", - "noise_level_card", - "noise_level_card_with_background", - "horizontal_noise_level_card", - "horizontal_noise_level_card_with_background", - "noise_level_chart_card", - "noise_level_chart_card_with_background", - "simple_noise_level_chart_card", - "simple_noise_level_chart_card_with_background", - "indoor_pm2_5_card", - "indoor_pm2_5_card_with_background", - "indoor_horizontal_pm2_5_card", - "indoor_horizontal_pm2_5_card_with_background", - "indoor_pm2_5_chart_card", - "indoor_pm2_5_chart_card_with_background", - "indoor_simple_pm2_5_chart_card", - "indoor_simple_pm2_5_chart_card_with_background", - "indoor_pm10_card", - "indoor_pm10_card_with_background", - "indoor_horizontal_pm10_card", - "indoor_horizontal_pm10_card_with_background", - "indoor_pm10_chart_card", - "indoor_pm10_chart_card_with_background", - "indoor_simple_pm10_chart_card", - "indoor_simple_pm10_chart_card_with_background", - "radon_level_card", - "radon_level_card_with_background", - "horizontal_radon_level_card", - "horizontal_radon_level_card_with_background", - "radon_level_chart_card", - "radon_level_chart_card_with_background", - "simple_radon_level_chart_card", - "simple_radon_level_chart_card_with_background", - "volatile_organic_compounds_card", - "volatile_organic_compounds_card_with_background", - "horizontal_volatile_organic_compounds_card", - "horizontal_volatile_organic_compounds_card_with_background", - "volatile_organic_compounds_chart_card", - "volatile_organic_compounds_chart_card_with_background", - "simple_volatile_organic_compounds_chart_card", - "simple_volatile_organic_compounds_chart_card_with_background" - ] -} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/industial_widgets.json b/application/src/main/data/json/system/widget_bundles/industial_widgets.json deleted file mode 100644 index 940c4e0d31..0000000000 --- a/application/src/main/data/json/system/widget_bundles/industial_widgets.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "widgetsBundle": { - "alias": "industrial_widgets", - "title": "Industrial widgets", - "image": "tb-image:aW5kdXN0cmlhbC13aWRnZXRzLWJ1bmxkZS5zdmc=:aW5kdXN0cmlhbC13aWRnZXRzLWJ1bmxkZS5zdmc=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iNzYiIHJ4PSI0IiBmaWxsPSJ3aGl0ZSIvPgo8cmVjdCB4PSIwLjUiIHk9IjAuNSIgd2lkdGg9IjE5OSIgaGVpZ2h0PSI3NSIgcng9IjMuNSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiLz4KPHBhdGggZD0iTTEyLjMwMDggMTcuMjIyN1YxOEg5LjI4MTI1VjE3LjIyMjdIMTIuMzAwOFpNOS41NTg1OSAxMi4zMTI1VjE4SDguNTc4MTJWMTIuMzEyNUg5LjU1ODU5Wk0xMS45MDYyIDE0LjY4NzVWMTUuNDUzMUg5LjI4MTI1VjE0LjY4NzVIMTEuOTA2MlpNMTIuMjgxMiAxMi4zMTI1VjEzLjA5MzhIOS4yODEyNVYxMi4zMTI1SDEyLjI4MTJaTTE0LjUzMTIgMThIMTMuNTg5OFYxMy4zNjcyQzEzLjU4OTggMTMuMDUyMSAxMy42NDg0IDEyLjc4NzggMTMuNzY1NiAxMi41NzQyQzEzLjg4NTQgMTIuMzU4MSAxNC4wNTYgMTIuMTk1MyAxNC4yNzczIDEyLjA4NTlDMTQuNDk4NyAxMS45NzQgMTQuNzYwNCAxMS45MTggMTUuMDYyNSAxMS45MThDMTUuMTU2MiAxMS45MTggMTUuMjQ4NyAxMS45MjQ1IDE1LjMzOTggMTEuOTM3NUMxNS40MzEgMTEuOTQ3OSAxNS41MTk1IDExLjk2NDggMTUuNjA1NSAxMS45ODgzTDE1LjU4MiAxMi43MTQ4QzE1LjUyOTkgMTIuNzAxOCAxNS40NzI3IDEyLjY5MjcgMTUuNDEwMiAxMi42ODc1QzE1LjM1MDMgMTIuNjgyMyAxNS4yODUyIDEyLjY3OTcgMTUuMjE0OCAxMi42Nzk3QzE1LjA3MTYgMTIuNjc5NyAxNC45NDc5IDEyLjcwNyAxNC44NDM4IDEyLjc2MTdDMTQuNzQyMiAxMi44MTM4IDE0LjY2NDEgMTIuODkwNiAxNC42MDk0IDEyLjk5MjJDMTQuNTU3MyAxMy4wOTM4IDE0LjUzMTIgMTMuMjE4OCAxNC41MzEyIDEzLjM2NzJWMThaTTE1LjQyNTggMTMuNzczNFYxNC40NjA5SDEyLjk0MTRWMTMuNzczNEgxNS40MjU4Wk0xNi45NTMxIDE4SDE2LjAxMTdWMTMuNUMxNi4wMTE3IDEzLjE1ODkgMTYuMDc5NCAxMi44NzExIDE2LjIxNDggMTIuNjM2N0MxNi4zNTAzIDEyLjQwMjMgMTYuNTQ1NiAxMi4yMjQgMTYuODAwOCAxMi4xMDE2QzE3LjA1ODYgMTEuOTc5MiAxNy4zNjQ2IDExLjkxOCAxNy43MTg4IDExLjkxOEMxNy45MjcxIDExLjkxOCAxOC4xMjg5IDExLjk0MDEgMTguMzI0MiAxMS45ODQ0QzE4LjUxOTUgMTIuMDI4NiAxOC43MjAxIDEyLjA4NDYgMTguOTI1OCAxMi4xNTIzTDE4Ljc4MTIgMTIuOTE4QzE4LjY1MSAxMi44NzM3IDE4LjUwMzkgMTIuODMyIDE4LjMzOTggMTIuNzkzQzE4LjE3ODQgMTIuNzUxMyAxNy45OTM1IDEyLjczMDUgMTcuNzg1MiAxMi43MzA1QzE3LjUwMTMgMTIuNzMwNSAxNy4yOTE3IDEyLjc5NTYgMTcuMTU2MiAxMi45MjU4QzE3LjAyMDggMTMuMDUzNCAxNi45NTMxIDEzLjI0NDggMTYuOTUzMSAxMy41VjE4Wk0xNy44MDQ3IDEzLjc3MzRWMTQuNDYwOUgxNS4zNjMzVjEzLjc3MzRIMTcuODA0N1pNMTkuMzc1IDEzLjc3MzRWMThIMTguNDI5N1YxMy43NzM0SDE5LjM3NVpNMjIuMzkwNiAxNy4zMjgxQzIyLjU0NDMgMTcuMzI4MSAyMi42ODIzIDE3LjI5ODIgMjIuODA0NyAxNy4yMzgzQzIyLjkyOTcgMTcuMTc1OCAyMy4wMjk5IDE3LjA4OTggMjMuMTA1NSAxNi45ODA1QzIzLjE4MzYgMTYuODcxMSAyMy4yMjY2IDE2Ljc0NDggMjMuMjM0NCAxNi42MDE2SDI0LjEyMTFDMjQuMTE1OSAxNi44NzUgMjQuMDM1MiAxNy4xMjM3IDIzLjg3ODkgMTcuMzQ3N0MyMy43MjI3IDE3LjU3MTYgMjMuNTE1NiAxNy43NSAyMy4yNTc4IDE3Ljg4MjhDMjMgMTguMDEzIDIyLjcxNDggMTguMDc4MSAyMi40MDIzIDE4LjA3ODFDMjIuMDc5NCAxOC4wNzgxIDIxLjc5ODIgMTguMDIzNCAyMS41NTg2IDE3LjkxNDFDMjEuMzE5IDE3LjgwMjEgMjEuMTE5OCAxNy42NDg0IDIwLjk2MDkgMTcuNDUzMUMyMC44MDIxIDE3LjI1NzggMjAuNjgyMyAxNy4wMzI2IDIwLjYwMTYgMTYuNzc3M0MyMC41MjM0IDE2LjUyMjEgMjAuNDg0NCAxNi4yNDg3IDIwLjQ4NDQgMTUuOTU3VjE1LjgyMDNDMjAuNDg0NCAxNS41Mjg2IDIwLjUyMzQgMTUuMjU1MiAyMC42MDE2IDE1QzIwLjY4MjMgMTQuNzQyMiAyMC44MDIxIDE0LjUxNTYgMjAuOTYwOSAxNC4zMjAzQzIxLjExOTggMTQuMTI1IDIxLjMxOSAxMy45NzI3IDIxLjU1ODYgMTMuODYzM0MyMS43OTgyIDEzLjc1MTMgMjIuMDc4MSAxMy42OTUzIDIyLjM5ODQgMTMuNjk1M0MyMi43MzcgMTMuNjk1MyAyMy4wMzM5IDEzLjc2MyAyMy4yODkxIDEzLjg5ODRDMjMuNTQ0MyAxNC4wMzEyIDIzLjc0NDggMTQuMjE3NCAyMy44OTA2IDE0LjQ1N0MyNC4wMzkxIDE0LjY5NCAyNC4xMTU5IDE0Ljk3MDEgMjQuMTIxMSAxNS4yODUySDIzLjIzNDRDMjMuMjI2NiAxNS4xMjg5IDIzLjE4NzUgMTQuOTg4MyAyMy4xMTcyIDE0Ljg2MzNDMjMuMDQ5NSAxNC43MzU3IDIyLjk1MzEgMTQuNjM0MSAyMi44MjgxIDE0LjU1ODZDMjIuNzA1NyAxNC40ODMxIDIyLjU1ODYgMTQuNDQ1MyAyMi4zODY3IDE0LjQ0NTNDMjIuMTk2NiAxNC40NDUzIDIyLjAzOTEgMTQuNDg0NCAyMS45MTQxIDE0LjU2MjVDMjEuNzg5MSAxNC42MzggMjEuNjkxNCAxNC43NDIyIDIxLjYyMTEgMTQuODc1QzIxLjU1MDggMTUuMDA1MiAyMS41IDE1LjE1MjMgMjEuNDY4OCAxNS4zMTY0QzIxLjQ0MDEgMTUuNDc3OSAyMS40MjU4IDE1LjY0NTggMjEuNDI1OCAxNS44MjAzVjE1Ljk1N0MyMS40MjU4IDE2LjEzMTUgMjEuNDQwMSAxNi4zMDA4IDIxLjQ2ODggMTYuNDY0OEMyMS40OTc0IDE2LjYyODkgMjEuNTQ2OSAxNi43NzYgMjEuNjE3MiAxNi45MDYyQzIxLjY5MDEgMTcuMDMzOSAyMS43ODkxIDE3LjEzNjcgMjEuOTE0MSAxNy4yMTQ4QzIyLjAzOTEgMTcuMjkwNCAyMi4xOTc5IDE3LjMyODEgMjIuMzkwNiAxNy4zMjgxWk0yNi4xMDE2IDEzLjc3MzRWMThIMjUuMTU2MlYxMy43NzM0SDI2LjEwMTZaTTI1LjA5MzggMTIuNjY0MUMyNS4wOTM4IDEyLjUyMDggMjUuMTQwNiAxMi40MDIzIDI1LjIzNDQgMTIuMzA4NkMyNS4zMzA3IDEyLjIxMjIgMjUuNDYzNSAxMi4xNjQxIDI1LjYzMjggMTIuMTY0MUMyNS43OTk1IDEyLjE2NDEgMjUuOTMxIDEyLjIxMjIgMjYuMDI3MyAxMi4zMDg2QzI2LjEyMzcgMTIuNDAyMyAyNi4xNzE5IDEyLjUyMDggMjYuMTcxOSAxMi42NjQxQzI2LjE3MTkgMTIuODA0NyAyNi4xMjM3IDEyLjkyMTkgMjYuMDI3MyAxMy4wMTU2QzI1LjkzMSAxMy4xMDk0IDI1Ljc5OTUgMTMuMTU2MiAyNS42MzI4IDEzLjE1NjJDMjUuNDYzNSAxMy4xNTYyIDI1LjMzMDcgMTMuMTA5NCAyNS4yMzQ0IDEzLjAxNTZDMjUuMTQwNiAxMi45MjE5IDI1LjA5MzggMTIuODA0NyAyNS4wOTM4IDEyLjY2NDFaTTI5LjI0NjEgMTguMDc4MUMyOC45MzM2IDE4LjA3ODEgMjguNjUxIDE4LjAyNzMgMjguMzk4NCAxNy45MjU4QzI4LjE0ODQgMTcuODIxNiAyNy45MzQ5IDE3LjY3NzEgMjcuNzU3OCAxNy40OTIyQzI3LjU4MzMgMTcuMzA3MyAyNy40NDkyIDE3LjA4OTggMjcuMzU1NSAxNi44Mzk4QzI3LjI2MTcgMTYuNTg5OCAyNy4yMTQ4IDE2LjMyMDMgMjcuMjE0OCAxNi4wMzEyVjE1Ljg3NUMyNy4yMTQ4IDE1LjU0NDMgMjcuMjYzIDE1LjI0NDggMjcuMzU5NCAxNC45NzY2QzI3LjQ1NTcgMTQuNzA4MyAyNy41ODk4IDE0LjQ3OTIgMjcuNzYxNyAxNC4yODkxQzI3LjkzMzYgMTQuMDk2NCAyOC4xMzY3IDEzLjk0OTIgMjguMzcxMSAxMy44NDc3QzI4LjYwNTUgMTMuNzQ2MSAyOC44NTk0IDEzLjY5NTMgMjkuMTMyOCAxMy42OTUzQzI5LjQzNDkgMTMuNjk1MyAyOS42OTkyIDEzLjc0NjEgMjkuOTI1OCAxMy44NDc3QzMwLjE1MjMgMTMuOTQ5MiAzMC4zMzk4IDE0LjA5MjQgMzAuNDg4MyAxNC4yNzczQzMwLjYzOTMgMTQuNDU5NiAzMC43NTEzIDE0LjY3NzEgMzAuODI0MiAxNC45Mjk3QzMwLjg5OTcgMTUuMTgyMyAzMC45Mzc1IDE1LjQ2MDkgMzAuOTM3NSAxNS43NjU2VjE2LjE2OEgyNy42NzE5VjE1LjQ5MjJIMzAuMDA3OFYxNS40MThDMzAuMDAyNiAxNS4yNDg3IDI5Ljk2ODggMTUuMDg5OCAyOS45MDYyIDE0Ljk0MTRDMjkuODQ2NCAxNC43OTMgMjkuNzUzOSAxNC42NzMyIDI5LjYyODkgMTQuNTgyQzI5LjUwMzkgMTQuNDkwOSAyOS4zMzcyIDE0LjQ0NTMgMjkuMTI4OSAxNC40NDUzQzI4Ljk3MjcgMTQuNDQ1MyAyOC44MzMzIDE0LjQ3OTIgMjguNzEwOSAxNC41NDY5QzI4LjU5MTEgMTQuNjEyIDI4LjQ5MDkgMTQuNzA3IDI4LjQxMDIgMTQuODMyQzI4LjMyOTQgMTQuOTU3IDI4LjI2NjkgMTUuMTA4MSAyOC4yMjI3IDE1LjI4NTJDMjguMTgxIDE1LjQ1OTYgMjguMTYwMiAxNS42NTYyIDI4LjE2MDIgMTUuODc1VjE2LjAzMTJDMjguMTYwMiAxNi4yMTYxIDI4LjE4NDkgMTYuMzg4IDI4LjIzNDQgMTYuNTQ2OUMyOC4yODY1IDE2LjcwMzEgMjguMzYyIDE2LjgzOTggMjguNDYwOSAxNi45NTdDMjguNTU5OSAxNy4wNzQyIDI4LjY3OTcgMTcuMTY2NyAyOC44MjAzIDE3LjIzNDRDMjguOTYwOSAxNy4yOTk1IDI5LjEyMTEgMTcuMzMyIDI5LjMwMDggMTcuMzMyQzI5LjUyNzMgMTcuMzMyIDI5LjcyOTIgMTcuMjg2NSAyOS45MDYyIDE3LjE5NTNDMzAuMDgzMyAxNy4xMDQyIDMwLjIzNyAxNi45NzUzIDMwLjM2NzIgMTYuODA4NkwzMC44NjMzIDE3LjI4OTFDMzAuNzcyMSAxNy40MjE5IDMwLjY1MzYgMTcuNTQ5NSAzMC41MDc4IDE3LjY3MTlDMzAuMzYyIDE3Ljc5MTcgMzAuMTgzNiAxNy44ODkzIDI5Ljk3MjcgMTcuOTY0OEMyOS43NjQzIDE4LjA0MDQgMjkuNTIyMSAxOC4wNzgxIDI5LjI0NjEgMTguMDc4MVpNMzIuODU1NSAxNC42NzU4VjE4SDMxLjkxNDFWMTMuNzczNEgzMi44MDA4TDMyLjg1NTUgMTQuNjc1OFpNMzIuNjg3NSAxNS43MzA1TDMyLjM4MjggMTUuNzI2NkMzMi4zODU0IDE1LjQyNzEgMzIuNDI3MSAxNS4xNTIzIDMyLjUwNzggMTQuOTAyM0MzMi41OTExIDE0LjY1MjMgMzIuNzA1NyAxNC40Mzc1IDMyLjg1MTYgMTQuMjU3OEMzMyAxNC4wNzgxIDMzLjE3NzEgMTMuOTQwMSAzMy4zODI4IDEzLjg0MzhDMzMuNTg4NSAxMy43NDQ4IDMzLjgxNzcgMTMuNjk1MyAzNC4wNzAzIDEzLjY5NTNDMzQuMjczNCAxMy42OTUzIDM0LjQ1NyAxMy43MjQgMzQuNjIxMSAxMy43ODEyQzM0Ljc4NzggMTMuODM1OSAzNC45Mjk3IDEzLjkyNTggMzUuMDQ2OSAxNC4wNTA4QzM1LjE2NjcgMTQuMTc1OCAzNS4yNTc4IDE0LjMzODUgMzUuMzIwMyAxNC41MzkxQzM1LjM4MjggMTQuNzM3IDM1LjQxNDEgMTQuOTgwNSAzNS40MTQxIDE1LjI2OTVWMThIMzQuNDY4OFYxNS4yNjU2QzM0LjQ2ODggMTUuMDYyNSAzNC40Mzg4IDE0LjkwMjMgMzQuMzc4OSAxNC43ODUyQzM0LjMyMTYgMTQuNjY1NCAzNC4yMzcgMTQuNTgwNyAzNC4xMjUgMTQuNTMxMkMzNC4wMTU2IDE0LjQ3OTIgMzMuODc4OSAxNC40NTMxIDMzLjcxNDggMTQuNDUzMUMzMy41NTM0IDE0LjQ1MzEgMzMuNDA4OSAxNC40ODcgMzMuMjgxMiAxNC41NTQ3QzMzLjE1MzYgMTQuNjIyNCAzMy4wNDU2IDE0LjcxNDggMzIuOTU3IDE0LjgzMkMzMi44NzExIDE0Ljk0OTIgMzIuODA0NyAxNS4wODQ2IDMyLjc1NzggMTUuMjM4M0MzMi43MTA5IDE1LjM5MTkgMzIuNjg3NSAxNS41NTYgMzIuNjg3NSAxNS43MzA1Wk0zOC4zNTE2IDE3LjMyODFDMzguNTA1MiAxNy4zMjgxIDM4LjY0MzIgMTcuMjk4MiAzOC43NjU2IDE3LjIzODNDMzguODkwNiAxNy4xNzU4IDM4Ljk5MDkgMTcuMDg5OCAzOS4wNjY0IDE2Ljk4MDVDMzkuMTQ0NSAxNi44NzExIDM5LjE4NzUgMTYuNzQ0OCAzOS4xOTUzIDE2LjYwMTZINDAuMDgyQzQwLjA3NjggMTYuODc1IDM5Ljk5NjEgMTcuMTIzNyAzOS44Mzk4IDE3LjM0NzdDMzkuNjgzNiAxNy41NzE2IDM5LjQ3NjYgMTcuNzUgMzkuMjE4OCAxNy44ODI4QzM4Ljk2MDkgMTguMDEzIDM4LjY3NTggMTguMDc4MSAzOC4zNjMzIDE4LjA3ODFDMzguMDQwNCAxOC4wNzgxIDM3Ljc1OTEgMTguMDIzNCAzNy41MTk1IDE3LjkxNDFDMzcuMjc5OSAxNy44MDIxIDM3LjA4MDcgMTcuNjQ4NCAzNi45MjE5IDE3LjQ1MzFDMzYuNzYzIDE3LjI1NzggMzYuNjQzMiAxNy4wMzI2IDM2LjU2MjUgMTYuNzc3M0MzNi40ODQ0IDE2LjUyMjEgMzYuNDQ1MyAxNi4yNDg3IDM2LjQ0NTMgMTUuOTU3VjE1LjgyMDNDMzYuNDQ1MyAxNS41Mjg2IDM2LjQ4NDQgMTUuMjU1MiAzNi41NjI1IDE1QzM2LjY0MzIgMTQuNzQyMiAzNi43NjMgMTQuNTE1NiAzNi45MjE5IDE0LjMyMDNDMzcuMDgwNyAxNC4xMjUgMzcuMjc5OSAxMy45NzI3IDM3LjUxOTUgMTMuODYzM0MzNy43NTkxIDEzLjc1MTMgMzguMDM5MSAxMy42OTUzIDM4LjM1OTQgMTMuNjk1M0MzOC42OTc5IDEzLjY5NTMgMzguOTk0OCAxMy43NjMgMzkuMjUgMTMuODk4NEMzOS41MDUyIDE0LjAzMTIgMzkuNzA1NyAxNC4yMTc0IDM5Ljg1MTYgMTQuNDU3QzQwIDE0LjY5NCA0MC4wNzY4IDE0Ljk3MDEgNDAuMDgyIDE1LjI4NTJIMzkuMTk1M0MzOS4xODc1IDE1LjEyODkgMzkuMTQ4NCAxNC45ODgzIDM5LjA3ODEgMTQuODYzM0MzOS4wMTA0IDE0LjczNTcgMzguOTE0MSAxNC42MzQxIDM4Ljc4OTEgMTQuNTU4NkMzOC42NjY3IDE0LjQ4MzEgMzguNTE5NSAxNC40NDUzIDM4LjM0NzcgMTQuNDQ1M0MzOC4xNTc2IDE0LjQ0NTMgMzggMTQuNDg0NCAzNy44NzUgMTQuNTYyNUMzNy43NSAxNC42MzggMzcuNjUyMyAxNC43NDIyIDM3LjU4MiAxNC44NzVDMzcuNTExNyAxNS4wMDUyIDM3LjQ2MDkgMTUuMTUyMyAzNy40Mjk3IDE1LjMxNjRDMzcuNDAxIDE1LjQ3NzkgMzcuMzg2NyAxNS42NDU4IDM3LjM4NjcgMTUuODIwM1YxNS45NTdDMzcuMzg2NyAxNi4xMzE1IDM3LjQwMSAxNi4zMDA4IDM3LjQyOTcgMTYuNDY0OEMzNy40NTgzIDE2LjYyODkgMzcuNTA3OCAxNi43NzYgMzcuNTc4MSAxNi45MDYyQzM3LjY1MSAxNy4wMzM5IDM3Ljc1IDE3LjEzNjcgMzcuODc1IDE3LjIxNDhDMzggMTcuMjkwNCAzOC4xNTg5IDE3LjMyODEgMzguMzUxNiAxNy4zMjgxWk00Mi4yODEyIDE3LjUzOTFMNDMuNDI5NyAxMy43NzM0SDQ0LjQzNzVMNDIuNzQyMiAxOC42NDQ1QzQyLjcwMzEgMTguNzQ4NyA0Mi42NTIzIDE4Ljg2MiA0Mi41ODk4IDE4Ljk4NDRDNDIuNTI3MyAxOS4xMDY4IDQyLjQ0NTMgMTkuMjIyNyA0Mi4zNDM4IDE5LjMzMkM0Mi4yNDQ4IDE5LjQ0NCA0Mi4xMjExIDE5LjUzMzkgNDEuOTcyNyAxOS42MDE2QzQxLjgyNDIgMTkuNjcxOSA0MS42NDQ1IDE5LjcwNyA0MS40MzM2IDE5LjcwN0M0MS4zNTAzIDE5LjcwNyA0MS4yNjk1IDE5LjY5OTIgNDEuMTkxNCAxOS42ODM2QzQxLjExNTkgMTkuNjcwNiA0MS4wNDQzIDE5LjY1NjIgNDAuOTc2NiAxOS42NDA2TDQwLjk3MjcgMTguOTIxOUM0MC45OTg3IDE4LjkyNDUgNDEuMDI5OSAxOC45MjcxIDQxLjA2NjQgMTguOTI5N0M0MS4xMDU1IDE4LjkzMjMgNDEuMTM2NyAxOC45MzM2IDQxLjE2MDIgMTguOTMzNkM0MS4zMTY0IDE4LjkzMzYgNDEuNDQ2NiAxOC45MTQxIDQxLjU1MDggMTguODc1QzQxLjY1NDkgMTguODM4NSA0MS43Mzk2IDE4Ljc3ODYgNDEuODA0NyAxOC42OTUzQzQxLjg3MjQgMTguNjEyIDQxLjkyOTcgMTguNSA0MS45NzY2IDE4LjM1OTRMNDIuMjgxMiAxNy41MzkxWk00MS42MzI4IDEzLjc3MzRMNDIuNjM2NyAxNi45Mzc1TDQyLjgwNDcgMTcuOTI5N0w0Mi4xNTIzIDE4LjA5NzdMNDAuNjE3MiAxMy43NzM0SDQxLjYzMjhaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0yMC42MjExIDQ4LjkzNzVWNTAuNDg0NEwxMy44MDA4IDY2SDEwLjgyNDJMMTcuNjMyOCA1MS4xODc1SDguNzk2ODhWNDguOTM3NUgyMC42MjExWk0zNC4wNTA4IDU2LjAzOTFWNTguODI4MUMzNC4wNTA4IDYwLjE2NDEgMzMuOTE4IDYxLjMwNDcgMzMuNjUyMyA2Mi4yNUMzMy4zOTQ1IDYzLjE4NzUgMzMuMDE5NSA2My45NDkyIDMyLjUyNzMgNjQuNTM1MkMzMi4wMzUyIDY1LjEyMTEgMzEuNDQ1MyA2NS41NTA4IDMwLjc1NzggNjUuODI0MkMzMC4wNzgxIDY2LjA5NzcgMjkuMzE2NCA2Ni4yMzQ0IDI4LjQ3MjcgNjYuMjM0NEMyNy44MDA4IDY2LjIzNDQgMjcuMTc1OCA2Ni4xNDg0IDI2LjU5NzcgNjUuOTc2NkMyNi4wMjczIDY1LjgwNDcgMjUuNTExNyA2NS41MzUyIDI1LjA1MDggNjUuMTY4QzI0LjU4OTggNjQuODAwOCAyNC4xOTUzIDY0LjMyODEgMjMuODY3MiA2My43NUMyMy41NDY5IDYzLjE2NDEgMjMuMjk2OSA2Mi40NjQ4IDIzLjExNzIgNjEuNjUyM0MyMi45NDUzIDYwLjgzOTggMjIuODU5NCA1OS44OTg0IDIyLjg1OTQgNTguODI4MVY1Ni4wMzkxQzIyLjg1OTQgNTQuNjk1MyAyMi45OTIyIDUzLjU2MjUgMjMuMjU3OCA1Mi42NDA2QzIzLjUyMzQgNTEuNzEwOSAyMy45MDIzIDUwLjk1NyAyNC4zOTQ1IDUwLjM3ODlDMjQuODg2NyA0OS43OTMgMjUuNDcyNyA0OS4zNjcyIDI2LjE1MjMgNDkuMTAxNkMyNi44Mzk4IDQ4LjgzNTkgMjcuNjA1NSA0OC43MDMxIDI4LjQ0OTIgNDguNzAzMUMyOS4xMjg5IDQ4LjcwMzEgMjkuNzUzOSA0OC43ODkxIDMwLjMyNDIgNDguOTYwOUMzMC45MDIzIDQ5LjEyNSAzMS40MTggNDkuMzg2NyAzMS44NzExIDQ5Ljc0NjFDMzIuMzMyIDUwLjEwNTUgMzIuNzIyNyA1MC41NzQyIDMzLjA0MyA1MS4xNTIzQzMzLjM3MTEgNTEuNzIyNyAzMy42MjExIDUyLjQxNDEgMzMuNzkzIDUzLjIyNjZDMzMuOTY0OCA1NC4wMzEyIDM0LjA1MDggNTQuOTY4OCAzNC4wNTA4IDU2LjAzOTFaTTMxLjIyNjYgNTkuMjI2NlY1NS42MTcyQzMxLjIyNjYgNTQuOTM3NSAzMS4xODc1IDU0LjMzOTggMzEuMTA5NCA1My44MjQyQzMxLjAzMTIgNTMuMzAwOCAzMC45MTQxIDUyLjg1OTQgMzAuNzU3OCA1Mi41QzMwLjYwOTQgNTIuMTMyOCAzMC40MjE5IDUxLjgzNTkgMzAuMTk1MyA1MS42MDk0QzI5Ljk2ODggNTEuMzc1IDI5LjcxMDkgNTEuMjA3IDI5LjQyMTkgNTEuMTA1NUMyOS4xMzI4IDUwLjk5NjEgMjguODA4NiA1MC45NDE0IDI4LjQ0OTIgNTAuOTQxNEMyOC4wMTE3IDUwLjk0MTQgMjcuNjIxMSA1MS4wMjczIDI3LjI3NzMgNTEuMTk5MkMyNi45MzM2IDUxLjM2MzMgMjYuNjQ0NSA1MS42Mjg5IDI2LjQxMDIgNTEuOTk2MUMyNi4xNzU4IDUyLjM2MzMgMjUuOTk2MSA1Mi44NDc3IDI1Ljg3MTEgNTMuNDQ5MkMyNS43NTM5IDU0LjA0MyAyNS42OTUzIDU0Ljc2NTYgMjUuNjk1MyA1NS42MTcyVjU5LjIyNjZDMjUuNjk1MyA1OS45MTQxIDI1LjczNDQgNjAuNTE5NSAyNS44MTI1IDYxLjA0M0MyNS44OTA2IDYxLjU2NjQgMjYuMDA3OCA2Mi4wMTU2IDI2LjE2NDEgNjIuMzkwNkMyNi4zMjAzIDYyLjc1NzggMjYuNTA3OCA2My4wNjI1IDI2LjcyNjYgNjMuMzA0N0MyNi45NTMxIDYzLjUzOTEgMjcuMjEwOSA2My43MTA5IDI3LjUgNjMuODIwM0MyNy43OTY5IDYzLjkyOTcgMjguMTIxMSA2My45ODQ0IDI4LjQ3MjcgNjMuOTg0NEMyOC45MTggNjMuOTg0NCAyOS4zMTI1IDYzLjg5ODQgMjkuNjU2MiA2My43MjY2QzMwIDYzLjU1NDcgMzAuMjg5MSA2My4yODEyIDMwLjUyMzQgNjIuOTA2MkMzMC43NTc4IDYyLjUyMzQgMzAuOTMzNiA2Mi4wMjczIDMxLjA1MDggNjEuNDE4QzMxLjE2OCA2MC44MDg2IDMxLjIyNjYgNjAuMDc4MSAzMS4yMjY2IDU5LjIyNjZaIiBmaWxsPSIjM0ZBNzFBIi8+CjxwYXRoIGQ9Ik0zOS4xNTcyIDU4LjQ4NzNWNTcuOTYwOUMzOS4xNTcyIDU3LjU4MjcgMzkuMjM5MyA1Ny4yMzg2IDM5LjQwMzMgNTYuOTI4N0MzOS41Njc0IDU2LjYxODggMzkuODA2NiA1Ni4zNzA0IDQwLjEyMTEgNTYuMTgzNkM0MC40MzU1IDU1Ljk5NjcgNDAuODEzOCA1NS45MDMzIDQxLjI1NTkgNTUuOTAzM0M0MS43MTE2IDU1LjkwMzMgNDIuMDk0NCA1NS45OTY3IDQyLjQwNDMgNTYuMTgzNkM0Mi43MTg4IDU2LjM3MDQgNDIuOTU4IDU2LjYxODggNDMuMTIyMSA1Ni45Mjg3QzQzLjI4NjEgNTcuMjM4NiA0My4zNjgyIDU3LjU4MjcgNDMuMzY4MiA1Ny45NjA5VjU4LjQ4NzNDNDMuMzY4MiA1OC44NTY0IDQzLjI4NjEgNTkuMTk2IDQzLjEyMjEgNTkuNTA1OUM0Mi45NjI2IDU5LjgxNTggNDIuNzI1NiA2MC4wNjQxIDQyLjQxMTEgNjAuMjUxQzQyLjEwMTIgNjAuNDM3OCA0MS43MjA3IDYwLjUzMTIgNDEuMjY5NSA2MC41MzEyQzQwLjgyMjkgNjAuNTMxMiA0MC40NDAxIDYwLjQzNzggNDAuMTIxMSA2MC4yNTFDMzkuODA2NiA2MC4wNjQxIDM5LjU2NzQgNTkuODE1OCAzOS40MDMzIDU5LjUwNTlDMzkuMjM5MyA1OS4xOTYgMzkuMTU3MiA1OC44NTY0IDM5LjE1NzIgNTguNDg3M1pNNDAuMjk4OCA1Ny45NjA5VjU4LjQ4NzNDNDAuMjk4OCA1OC42Njk2IDQwLjMzMyA1OC44NDI4IDQwLjQwMTQgNTkuMDA2OEM0MC40NzQzIDU5LjE3MDkgNDAuNTgzNyA1OS4zMDMxIDQwLjcyOTUgNTkuNDAzM0M0MC44NzUzIDU5LjUwMzYgNDEuMDU1MyA1OS41NTM3IDQxLjI2OTUgNTkuNTUzN0M0MS40ODgzIDU5LjU1MzcgNDEuNjY2IDU5LjUwMzYgNDEuODAyNyA1OS40MDMzQzQxLjk0NCA1OS4zMDMxIDQyLjA0ODggNTkuMTcwOSA0Mi4xMTcyIDU5LjAwNjhDNDIuMTg1NSA1OC44NDI4IDQyLjIxOTcgNTguNjY5NiA0Mi4yMTk3IDU4LjQ4NzNWNTcuOTYwOUM0Mi4yMTk3IDU3Ljc3NDEgNDIuMTgzMyA1Ny41OTg2IDQyLjExMDQgNTcuNDM0NkM0Mi4wNDIgNTcuMjY2IDQxLjkzNzIgNTcuMTMxNSA0MS43OTU5IDU3LjAzMTJDNDEuNjU0NiA1Ni45MzEgNDEuNDc0NiA1Ni44ODA5IDQxLjI1NTkgNTYuODgwOUM0MS4wNDYyIDU2Ljg4MDkgNDAuODY4NSA1Ni45MzEgNDAuNzIyNyA1Ny4wMzEyQzQwLjU4MTQgNTcuMTMxNSA0MC40NzQzIDU3LjI2NiA0MC40MDE0IDU3LjQzNDZDNDAuMzMzIDU3LjU5ODYgNDAuMjk4OCA1Ny43NzQxIDQwLjI5ODggNTcuOTYwOVpNNDMuOTU2MSA2NC4wOTI4VjYzLjU1OTZDNDMuOTU2MSA2My4xODU5IDQ0LjAzODEgNjIuODQ0MSA0NC4yMDIxIDYyLjUzNDJDNDQuMzcwOCA2Mi4yMjQzIDQ0LjYxMjMgNjEuOTc1OSA0NC45MjY4IDYxLjc4OTFDNDUuMjQxMiA2MS42MDIyIDQ1LjYxOTUgNjEuNTA4OCA0Ni4wNjE1IDYxLjUwODhDNDYuNTE3MyA2MS41MDg4IDQ2LjkwMDEgNjEuNjAyMiA0Ny4yMSA2MS43ODkxQzQ3LjUyNDQgNjEuOTc1OSA0Ny43NjE0IDYyLjIyNDMgNDcuOTIwOSA2Mi41MzQyQzQ4LjA4NSA2Mi44NDQxIDQ4LjE2NyA2My4xODU5IDQ4LjE2NyA2My41NTk2VjY0LjA5MjhDNDguMTY3IDY0LjQ2NjUgNDguMDg1IDY0LjgwODMgNDcuOTIwOSA2NS4xMTgyQzQ3Ljc2MTQgNjUuNDI4MSA0Ny41MjY3IDY1LjY3NjQgNDcuMjE2OCA2NS44NjMzQzQ2LjkwNjkgNjYuMDUwMSA0Ni41Mjg2IDY2LjE0MzYgNDYuMDgyIDY2LjE0MzZDNDUuNjMwOSA2Ni4xNDM2IDQ1LjI0NTggNjYuMDUwMSA0NC45MjY4IDY1Ljg2MzNDNDQuNjEyMyA2NS42NzY0IDQ0LjM3MDggNjUuNDI4MSA0NC4yMDIxIDY1LjExODJDNDQuMDM4MSA2NC44MDgzIDQzLjk1NjEgNjQuNDY2NSA0My45NTYxIDY0LjA5MjhaTTQ1LjEwNDUgNjMuNTU5NlY2NC4wOTI4QzQ1LjEwNDUgNjQuMjc1MSA0NS4xNDEgNjQuNDQ4MiA0NS4yMTM5IDY0LjYxMjNDNDUuMjkxMyA2NC43NzY0IDQ1LjQwMyA2NC45MTA4IDQ1LjU0ODggNjUuMDE1NkM0NS42OTQ3IDY1LjExNTkgNDUuODcwMSA2NS4xNjYgNDYuMDc1MiA2NS4xNjZDNDYuMzA3NiA2NS4xNjYgNDYuNDk0NSA2NS4xMTU5IDQ2LjYzNTcgNjUuMDE1NkM0Ni43NzcgNjQuOTEwOCA0Ni44NzczIDY0Ljc3ODYgNDYuOTM2NSA2NC42MTkxQzQ3LjAwMDMgNjQuNDU1MSA0Ny4wMzIyIDY0LjI3OTYgNDcuMDMyMiA2NC4wOTI4VjYzLjU1OTZDNDcuMDMyMiA2My4zNzI3IDQ2Ljk5NTggNjMuMTk3MyA0Ni45MjI5IDYzLjAzMzJDNDYuODU0NSA2Mi44NjkxIDQ2Ljc0NzQgNjIuNzM3IDQ2LjYwMTYgNjIuNjM2N0M0Ni40NjAzIDYyLjUzNjUgNDYuMjgwMyA2Mi40ODYzIDQ2LjA2MTUgNjIuNDg2M0M0NS44NDczIDYyLjQ4NjMgNDUuNjY5NiA2Mi41MzY1IDQ1LjUyODMgNjIuNjM2N0M0NS4zODcgNjIuNzM3IDQ1LjI3OTkgNjIuODY5MSA0NS4yMDcgNjMuMDMzMkM0NS4xMzg3IDYzLjE5NzMgNDUuMTA0NSA2My4zNzI3IDQ1LjEwNDUgNjMuNTU5NlpNNDYuNDcxNyA1Ny40Njg4TDQxLjYxMTMgNjUuMjQ4TDQwLjc3MDUgNjQuNzYyN0w0NS42MzA5IDU2Ljk4MzRMNDYuNDcxNyA1Ny40Njg4WiIgZmlsbD0iIzNGQTcxQSIvPgo8cGF0aCBkPSJNMTg2LjM2OCAyN0MxNzAuNDM4IDI3IDE3Mi4zMjUgNTguMDk3OSAxNTUuNzU3IDU4LjA5NzlDMTQxLjczOCA1OC4wOTc5IDEzOS40OSA0NC44NzIzIDEyOC42NTcgNDQuODcyM0MxMTcuODI0IDQ0Ljg3MjMgMTEzLjA2MyA2MyA5OS4wNDM4IDYzQzg3LjU3MzYgNjMgODEuMjAxMyA1OS4zMjM0IDcwLjM2ODQgNTkuMzIzNCIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuODciIHN0cm9rZS13aWR0aD0iMS4zIi8+CjxyZWN0IHk9Ijg0IiB3aWR0aD0iMjAwIiBoZWlnaHQ9Ijc2IiByeD0iNCIgZmlsbD0id2hpdGUiLz4KPHJlY3QgeD0iMC41IiB5PSI4NC41IiB3aWR0aD0iMTk5IiBoZWlnaHQ9Ijc1IiByeD0iMy41IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC4xMiIvPgo8cGF0aCBkPSJNMTAuNTI3MyA5OC44MTY0TDEyLjAyMzQgOTQuMTY4SDEzLjEwNTVMMTEuMDY2NCA5OS44NTU1SDEwLjMzNTlMMTAuNTI3MyA5OC44MTY0Wk05LjE0NDUzIDk0LjE2OEwxMC42Mjg5IDk4LjgxNjRMMTAuODMyIDk5Ljg1NTVIMTAuMDk3N0w4LjA2NjQxIDk0LjE2OEg5LjE0NDUzWk0xNC45MDYyIDk1LjYyODlWOTkuODU1NUgxMy45NjA5Vjk1LjYyODlIMTQuOTA2MlpNMTMuODk4NCA5NC41MTk1QzEzLjg5ODQgOTQuMzc2MyAxMy45NDUzIDk0LjI1NzggMTQuMDM5MSA5NC4xNjQxQzE0LjEzNTQgOTQuMDY3NyAxNC4yNjgyIDk0LjAxOTUgMTQuNDM3NSA5NC4wMTk1QzE0LjYwNDIgOTQuMDE5NSAxNC43MzU3IDk0LjA2NzcgMTQuODMyIDk0LjE2NDFDMTQuOTI4NCA5NC4yNTc4IDE0Ljk3NjYgOTQuMzc2MyAxNC45NzY2IDk0LjUxOTVDMTQuOTc2NiA5NC42NjAyIDE0LjkyODQgOTQuNzc3MyAxNC44MzIgOTQuODcxMUMxNC43MzU3IDk0Ljk2NDggMTQuNjA0MiA5NS4wMTE3IDE0LjQzNzUgOTUuMDExN0MxNC4yNjgyIDk1LjAxMTcgMTQuMTM1NCA5NC45NjQ4IDE0LjAzOTEgOTQuODcxMUMxMy45NDUzIDk0Ljc3NzMgMTMuODk4NCA5NC42NjAyIDEzLjg5ODQgOTQuNTE5NVpNMTYuMTkxNCA5My44NTU1SDE3LjEzMjhWOTguOTUzMUwxNy4wNDMgOTkuODU1NUgxNi4xOTE0VjkzLjg1NTVaTTE5Ljg5MDYgOTcuNzAzMVY5Ny43ODUyQzE5Ljg5MDYgOTguMDk3NyAxOS44NTU1IDk4LjM4NTQgMTkuNzg1MiA5OC42NDg0QzE5LjcxNzQgOTguOTA4OSAxOS42MTMzIDk5LjEzNTQgMTkuNDcyNyA5OS4zMjgxQzE5LjMzNDYgOTkuNTIwOCAxOS4xNjI4IDk5LjY3MDYgMTguOTU3IDk5Ljc3NzNDMTguNzUzOSA5OS44ODE1IDE4LjUxODIgOTkuOTMzNiAxOC4yNSA5OS45MzM2QzE3Ljk4NyA5OS45MzM2IDE3Ljc1NzggOTkuODg0MSAxNy41NjI1IDk5Ljc4NTJDMTcuMzY3MiA5OS42ODYyIDE3LjIwMzEgOTkuNTQ1NiAxNy4wNzAzIDk5LjM2MzNDMTYuOTQwMSA5OS4xODEgMTYuODM0NiA5OC45NjM1IDE2Ljc1MzkgOTguNzEwOUMxNi42NzMyIDk4LjQ1ODMgMTYuNjE1OSA5OC4xNzk3IDE2LjU4MiA5Ny44NzVWOTcuNjEzM0MxNi42MTU5IDk3LjMwNiAxNi42NzMyIDk3LjAyNzMgMTYuNzUzOSA5Ni43NzczQzE2LjgzNDYgOTYuNTI0NyAxNi45NDAxIDk2LjMwNzMgMTcuMDcwMyA5Ni4xMjVDMTcuMjAzMSA5NS45NDAxIDE3LjM2NTkgOTUuNzk4MiAxNy41NTg2IDk1LjY5OTJDMTcuNzUzOSA5NS42MDAzIDE3Ljk4MTggOTUuNTUwOCAxOC4yNDIyIDk1LjU1MDhDMTguNTEzIDk1LjU1MDggMTguNzUxMyA5NS42MDI5IDE4Ljk1NyA5NS43MDdDMTkuMTY1NCA5NS44MTEyIDE5LjMzODUgOTUuOTU5NiAxOS40NzY2IDk2LjE1MjNDMTkuNjE0NiA5Ni4zNDI0IDE5LjcxNzQgOTYuNTY5IDE5Ljc4NTIgOTYuODMyQzE5Ljg1NTUgOTcuMDk1MSAxOS44OTA2IDk3LjM4NTQgMTkuODkwNiA5Ny43MDMxWk0xOC45NDkyIDk3Ljc4NTJWOTcuNzAzMUMxOC45NDkyIDk3LjUxMyAxOC45MzM2IDk3LjMzNDYgMTguOTAyMyA5Ny4xNjhDMTguODcxMSA5Ni45OTg3IDE4LjgxOSA5Ni44NTAzIDE4Ljc0NjEgOTYuNzIyN0MxOC42NzU4IDk2LjU5NTEgMTguNTc5NCA5Ni40OTQ4IDE4LjQ1NyA5Ni40MjE5QzE4LjMzNzIgOTYuMzQ2NCAxOC4xODc1IDk2LjMwODYgMTguMDA3OCA5Ni4zMDg2QzE3Ljg0MTEgOTYuMzA4NiAxNy42OTc5IDk2LjMzNzIgMTcuNTc4MSA5Ni4zOTQ1QzE3LjQ1ODMgOTYuNDUxOCAxNy4zNTgxIDk2LjUyOTkgMTcuMjc3MyA5Ni42Mjg5QzE3LjE5NjYgOTYuNzI3OSAxNy4xMzI4IDk2Ljg0MjQgMTcuMDg1OSA5Ni45NzI3QzE3LjA0MTcgOTcuMTAyOSAxNy4wMTE3IDk3LjI0MzUgMTYuOTk2MSA5Ny4zOTQ1Vjk4LjEwMTZDMTcuMDE5NSA5OC4yOTY5IDE3LjA2OSA5OC40NzY2IDE3LjE0NDUgOTguNjQwNkMxNy4yMjI3IDk4LjgwMjEgMTcuMzMyIDk4LjkzMjMgMTcuNDcyNyA5OS4wMzEyQzE3LjYxMzMgOTkuMTI3NiAxNy43OTQzIDk5LjE3NTggMTguMDE1NiA5OS4xNzU4QzE4LjE5MDEgOTkuMTc1OCAxOC4zMzcyIDk5LjE0MDYgMTguNDU3IDk5LjA3MDNDMTguNTc2OCA5OSAxOC42NzE5IDk4LjkwMjMgMTguNzQyMiA5OC43NzczQzE4LjgxNTEgOTguNjQ5NyAxOC44NjcyIDk4LjUwMTMgMTguODk4NCA5OC4zMzJDMTguOTMyMyA5OC4xNjI4IDE4Ljk0OTIgOTcuOTgwNSAxOC45NDkyIDk3Ljc4NTJaTTIxLjg4MjggOTYuNDMzNlY5OS44NTU1SDIwLjk0MTRWOTUuNjI4OUgyMS44Mzk4TDIxLjg4MjggOTYuNDMzNlpNMjMuMTc1OCA5NS42MDE2TDIzLjE2OCA5Ni40NzY2QzIzLjExMDcgOTYuNDY2MSAyMy4wNDgyIDk2LjQ1ODMgMjIuOTgwNSA5Ni40NTMxQzIyLjkxNTQgOTYuNDQ3OSAyMi44NTAzIDk2LjQ0NTMgMjIuNzg1MiA5Ni40NDUzQzIyLjYyMzcgOTYuNDQ1MyAyMi40ODE4IDk2LjQ2ODggMjIuMzU5NCA5Ni41MTU2QzIyLjIzNyA5Ni41NTk5IDIyLjEzNDEgOTYuNjI1IDIyLjA1MDggOTYuNzEwOUMyMS45NzAxIDk2Ljc5NDMgMjEuOTA3NiA5Ni44OTU4IDIxLjg2MzMgOTcuMDE1NkMyMS44MTkgOTcuMTM1NCAyMS43OTMgOTcuMjY5NSAyMS43ODUyIDk3LjQxOEwyMS41NzAzIDk3LjQzMzZDMjEuNTcwMyA5Ny4xNjggMjEuNTk2NCA5Ni45MjE5IDIxLjY0ODQgOTYuNjk1M0MyMS43MDA1IDk2LjQ2ODggMjEuNzc4NiA5Ni4yNjk1IDIxLjg4MjggOTYuMDk3N0MyMS45ODk2IDk1LjkyNTggMjIuMTIyNCA5NS43OTE3IDIyLjI4MTIgOTUuNjk1M0MyMi40NDI3IDk1LjU5OSAyMi42Mjg5IDk1LjU1MDggMjIuODM5OCA5NS41NTA4QzIyLjg5NzEgOTUuNTUwOCAyMi45NTgzIDk1LjU1NiAyMy4wMjM0IDk1LjU2NjRDMjMuMDkxMSA5NS41NzY4IDIzLjE0MTkgOTUuNTg4NSAyMy4xNzU4IDk1LjYwMTZaTTI2LjI1NzggOTkuMDA3OFY5Ni45OTIyQzI2LjI1NzggOTYuODQxMSAyNi4yMzA1IDk2LjcxMDkgMjYuMTc1OCA5Ni42MDE2QzI2LjEyMTEgOTYuNDkyMiAyNi4wMzc4IDk2LjQwNzYgMjUuOTI1OCA5Ni4zNDc3QzI1LjgxNjQgOTYuMjg3OCAyNS42Nzg0IDk2LjI1NzggMjUuNTExNyA5Ni4yNTc4QzI1LjM1ODEgOTYuMjU3OCAyNS4yMjUzIDk2LjI4MzkgMjUuMTEzMyA5Ni4zMzU5QzI1LjAwMTMgOTYuMzg4IDI0LjkxNDEgOTYuNDU4MyAyNC44NTE2IDk2LjU0NjlDMjQuNzg5MSA5Ni42MzU0IDI0Ljc1NzggOTYuNzM1NyAyNC43NTc4IDk2Ljg0NzdIMjMuODIwM0MyMy44MjAzIDk2LjY4MSAyMy44NjA3IDk2LjUxOTUgMjMuOTQxNCA5Ni4zNjMzQzI0LjAyMjEgOTYuMjA3IDI0LjEzOTMgOTYuMDY3NyAyNC4yOTMgOTUuOTQ1M0MyNC40NDY2IDk1LjgyMjkgMjQuNjMwMiA5NS43MjY2IDI0Ljg0MzggOTUuNjU2MkMyNS4wNTczIDk1LjU4NTkgMjUuMjk2OSA5NS41NTA4IDI1LjU2MjUgOTUuNTUwOEMyNS44ODAyIDk1LjU1MDggMjYuMTYxNSA5NS42MDQyIDI2LjQwNjIgOTUuNzEwOUMyNi42NTM2IDk1LjgxNzcgMjYuODQ3NyA5NS45NzkyIDI2Ljk4ODMgOTYuMTk1M0MyNy4xMzE1IDk2LjQwODkgMjcuMjAzMSA5Ni42NzcxIDI3LjIwMzEgOTdWOTguODc4OUMyNy4yMDMxIDk5LjA3MTYgMjcuMjE2MSA5OS4yNDQ4IDI3LjI0MjIgOTkuMzk4NEMyNy4yNzA4IDk5LjU0OTUgMjcuMzExMiA5OS42ODEgMjcuMzYzMyA5OS43OTNWOTkuODU1NUgyNi4zOTg0QzI2LjM1NDIgOTkuNzUzOSAyNi4zMTkgOTkuNjI1IDI2LjI5MyA5OS40Njg4QzI2LjI2OTUgOTkuMzA5OSAyNi4yNTc4IDk5LjE1NjIgMjYuMjU3OCA5OS4wMDc4Wk0yNi4zOTQ1IDk3LjI4NTJMMjYuNDAyMyA5Ny44NjcySDI1LjcyNjZDMjUuNTUyMSA5Ny44NjcyIDI1LjM5ODQgOTcuODg0MSAyNS4yNjU2IDk3LjkxOEMyNS4xMzI4IDk3Ljk0OTIgMjUuMDIyMSA5Ny45OTYxIDI0LjkzMzYgOTguMDU4NkMyNC44NDUxIDk4LjEyMTEgMjQuNzc4NiA5OC4xOTY2IDI0LjczNDQgOTguMjg1MkMyNC42OTAxIDk4LjM3MzcgMjQuNjY4IDk4LjQ3NCAyNC42NjggOTguNTg1OUMyNC42NjggOTguNjk3OSAyNC42OTQgOTguODAwOCAyNC43NDYxIDk4Ljg5NDVDMjQuNzk4MiA5OC45ODU3IDI0Ljg3MzcgOTkuMDU3MyAyNC45NzI3IDk5LjEwOTRDMjUuMDc0MiA5OS4xNjE1IDI1LjE5NjYgOTkuMTg3NSAyNS4zMzk4IDk5LjE4NzVDMjUuNTMyNiA5OS4xODc1IDI1LjcwMDUgOTkuMTQ4NCAyNS44NDM4IDk5LjA3MDNDMjUuOTg5NiA5OC45ODk2IDI2LjEwNDIgOTguODkxOSAyNi4xODc1IDk4Ljc3NzNDMjYuMjcwOCA5OC42NjAyIDI2LjMxNTEgOTguNTQ5NSAyNi4zMjAzIDk4LjQ0NTNMMjYuNjI1IDk4Ljg2MzNDMjYuNTkzOCA5OC45NzAxIDI2LjU0MDQgOTkuMDg0NiAyNi40NjQ4IDk5LjIwN0MyNi4zODkzIDk5LjMyOTQgMjYuMjkwNCA5OS40NDY2IDI2LjE2OCA5OS41NTg2QzI2LjA0ODIgOTkuNjY4IDI1LjkwMzYgOTkuNzU3OCAyNS43MzQ0IDk5LjgyODFDMjUuNTY3NyA5OS44OTg0IDI1LjM3NSA5OS45MzM2IDI1LjE1NjIgOTkuOTMzNkMyNC44ODAyIDk5LjkzMzYgMjQuNjM0MSA5OS44Nzg5IDI0LjQxOCA5OS43Njk1QzI0LjIwMTggOTkuNjU3NiAyNC4wMzI2IDk5LjUwNzggMjMuOTEwMiA5OS4zMjAzQzIzLjc4NzggOTkuMTMwMiAyMy43MjY2IDk4LjkxNTQgMjMuNzI2NiA5OC42NzU4QzIzLjcyNjYgOTguNDUxOCAyMy43NjgyIDk4LjI1MzkgMjMuODUxNiA5OC4wODJDMjMuOTM3NSA5Ny45MDc2IDI0LjA2MjUgOTcuNzYxNyAyNC4yMjY2IDk3LjY0NDVDMjQuMzkzMiA5Ny41MjczIDI0LjU5NjQgOTcuNDM4OCAyNC44MzU5IDk3LjM3ODlDMjUuMDc1NSA5Ny4zMTY0IDI1LjM0OSA5Ny4yODUyIDI1LjY1NjIgOTcuMjg1MkgyNi4zOTQ1Wk0zMC4zNzUgOTUuNjI4OVY5Ni4zMTY0SDI3Ljk5MjJWOTUuNjI4OUgzMC4zNzVaTTI4LjY3OTcgOTQuNTkzOEgyOS42MjExVjk4LjY4NzVDMjkuNjIxMSA5OC44MTc3IDI5LjYzOTMgOTguOTE4IDI5LjY3NTggOTguOTg4M0MyOS43MTQ4IDk5LjA1NiAyOS43NjgyIDk5LjEwMTYgMjkuODM1OSA5OS4xMjVDMjkuOTAzNiA5OS4xNDg0IDI5Ljk4MzEgOTkuMTYwMiAzMC4wNzQyIDk5LjE2MDJDMzAuMTM5MyA5OS4xNjAyIDMwLjIwMTggOTkuMTU2MiAzMC4yNjE3IDk5LjE0ODRDMzAuMzIxNiA5OS4xNDA2IDMwLjM2OTggOTkuMTMyOCAzMC40MDYyIDk5LjEyNUwzMC40MTAyIDk5Ljg0MzhDMzAuMzMyIDk5Ljg2NzIgMzAuMjQwOSA5OS44ODggMzAuMTM2NyA5OS45MDYyQzMwLjAzNTIgOTkuOTI0NSAyOS45MTggOTkuOTMzNiAyOS43ODUyIDk5LjkzMzZDMjkuNTY5IDk5LjkzMzYgMjkuMzc3NiA5OS44OTU4IDI5LjIxMDkgOTkuODIwM0MyOS4wNDQzIDk5Ljc0MjIgMjguOTE0MSA5OS42MTU5IDI4LjgyMDMgOTkuNDQxNEMyOC43MjY2IDk5LjI2NjkgMjguNjc5NyA5OS4wMzUyIDI4LjY3OTcgOTguNzQ2MVY5NC41OTM4Wk0zMi4zNTk0IDk1LjYyODlWOTkuODU1NUgzMS40MTQxVjk1LjYyODlIMzIuMzU5NFpNMzEuMzUxNiA5NC41MTk1QzMxLjM1MTYgOTQuMzc2MyAzMS4zOTg0IDk0LjI1NzggMzEuNDkyMiA5NC4xNjQxQzMxLjU4ODUgOTQuMDY3NyAzMS43MjE0IDk0LjAxOTUgMzEuODkwNiA5NC4wMTk1QzMyLjA1NzMgOTQuMDE5NSAzMi4xODg4IDk0LjA2NzcgMzIuMjg1MiA5NC4xNjQxQzMyLjM4MTUgOTQuMjU3OCAzMi40Mjk3IDk0LjM3NjMgMzIuNDI5NyA5NC41MTk1QzMyLjQyOTcgOTQuNjYwMiAzMi4zODE1IDk0Ljc3NzMgMzIuMjg1MiA5NC44NzExQzMyLjE4ODggOTQuOTY0OCAzMi4wNTczIDk1LjAxMTcgMzEuODkwNiA5NS4wMTE3QzMxLjcyMTQgOTUuMDExNyAzMS41ODg1IDk0Ljk2NDggMzEuNDkyMiA5NC44NzExQzMxLjM5ODQgOTQuNzc3MyAzMS4zNTE2IDk0LjY2MDIgMzEuMzUxNiA5NC41MTk1Wk0zMy40NjA5IDk3Ljc4OTFWOTcuNjk5MkMzMy40NjA5IDk3LjM5NDUgMzMuNTA1MiA5Ny4xMTIgMzMuNTkzOCA5Ni44NTE2QzMzLjY4MjMgOTYuNTg4NSAzMy44MDk5IDk2LjM2MDcgMzMuOTc2NiA5Ni4xNjhDMzQuMTQ1OCA5NS45NzI3IDM0LjM1MTYgOTUuODIxNiAzNC41OTM4IDk1LjcxNDhDMzQuODM4NSA5NS42MDU1IDM1LjExNDYgOTUuNTUwOCAzNS40MjE5IDk1LjU1MDhDMzUuNzMxOCA5NS41NTA4IDM2LjAwNzggOTUuNjA1NSAzNi4yNSA5NS43MTQ4QzM2LjQ5NDggOTUuODIxNiAzNi43MDE4IDk1Ljk3MjcgMzYuODcxMSA5Ni4xNjhDMzcuMDQwNCA5Ni4zNjA3IDM3LjE2OTMgOTYuNTg4NSAzNy4yNTc4IDk2Ljg1MTZDMzcuMzQ2NCA5Ny4xMTIgMzcuMzkwNiA5Ny4zOTQ1IDM3LjM5MDYgOTcuNjk5MlY5Ny43ODkxQzM3LjM5MDYgOTguMDkzOCAzNy4zNDY0IDk4LjM3NjMgMzcuMjU3OCA5OC42MzY3QzM3LjE2OTMgOTguODk3MSAzNy4wNDA0IDk5LjEyNSAzNi44NzExIDk5LjMyMDNDMzYuNzAxOCA5OS41MTMgMzYuNDk2MSA5OS42NjQxIDM2LjI1MzkgOTkuNzczNEMzNi4wMTE3IDk5Ljg4MDIgMzUuNzM3IDk5LjkzMzYgMzUuNDI5NyA5OS45MzM2QzM1LjExOTggOTkuOTMzNiAzNC44NDI0IDk5Ljg4MDIgMzQuNTk3NyA5OS43NzM0QzM0LjM1NTUgOTkuNjY0MSAzNC4xNDk3IDk5LjUxMyAzMy45ODA1IDk5LjMyMDNDMzMuODExMiA5OS4xMjUgMzMuNjgyMyA5OC44OTcxIDMzLjU5MzggOTguNjM2N0MzMy41MDUyIDk4LjM3NjMgMzMuNDYwOSA5OC4wOTM4IDMzLjQ2MDkgOTcuNzg5MVpNMzQuNDAyMyA5Ny42OTkyVjk3Ljc4OTFDMzQuNDAyMyA5Ny45NzkyIDM0LjQyMTkgOTguMTU4OSAzNC40NjA5IDk4LjMyODFDMzQuNSA5OC40OTc0IDM0LjU2MTIgOTguNjQ1OCAzNC42NDQ1IDk4Ljc3MzRDMzQuNzI3OSA5OC45MDEgMzQuODM0NiA5OS4wMDEzIDM0Ljk2NDggOTkuMDc0MkMzNS4wOTUxIDk5LjE0NzEgMzUuMjUgOTkuMTgzNiAzNS40Mjk3IDk5LjE4MzZDMzUuNjA0MiA5OS4xODM2IDM1Ljc1NTIgOTkuMTQ3MSAzNS44ODI4IDk5LjA3NDJDMzYuMDEzIDk5LjAwMTMgMzYuMTE5OCA5OC45MDEgMzYuMjAzMSA5OC43NzM0QzM2LjI4NjUgOTguNjQ1OCAzNi4zNDc3IDk4LjQ5NzQgMzYuMzg2NyA5OC4zMjgxQzM2LjQyODQgOTguMTU4OSAzNi40NDkyIDk3Ljk3OTIgMzYuNDQ5MiA5Ny43ODkxVjk3LjY5OTJDMzYuNDQ5MiA5Ny41MTE3IDM2LjQyODQgOTcuMzM0NiAzNi4zODY3IDk3LjE2OEMzNi4zNDc3IDk2Ljk5ODcgMzYuMjg1MiA5Ni44NDkgMzYuMTk5MiA5Ni43MTg4QzM2LjExNTkgOTYuNTg4NSAzNi4wMDkxIDk2LjQ4NyAzNS44Nzg5IDk2LjQxNDFDMzUuNzUxMyA5Ni4zMzg1IDM1LjU5OSA5Ni4zMDA4IDM1LjQyMTkgOTYuMzAwOEMzNS4yNDQ4IDk2LjMwMDggMzUuMDkxMSA5Ni4zMzg1IDM0Ljk2MDkgOTYuNDE0MUMzNC44MzMzIDk2LjQ4NyAzNC43Mjc5IDk2LjU4ODUgMzQuNjQ0NSA5Ni43MTg4QzM0LjU2MTIgOTYuODQ5IDM0LjUgOTYuOTk4NyAzNC40NjA5IDk3LjE2OEMzNC40MjE5IDk3LjMzNDYgMzQuNDAyMyA5Ny41MTE3IDM0LjQwMjMgOTcuNjk5MlpNMzkuMzYzMyA5Ni41MzEyVjk5Ljg1NTVIMzguNDIxOVY5NS42Mjg5SDM5LjMwODZMMzkuMzYzMyA5Ni41MzEyWk0zOS4xOTUzIDk3LjU4NTlMMzguODkwNiA5Ny41ODJDMzguODkzMiA5Ny4yODI2IDM4LjkzNDkgOTcuMDA3OCAzOS4wMTU2IDk2Ljc1NzhDMzkuMDk5IDk2LjUwNzggMzkuMjEzNSA5Ni4yOTMgMzkuMzU5NCA5Ni4xMTMzQzM5LjUwNzggOTUuOTMzNiAzOS42ODQ5IDk1Ljc5NTYgMzkuODkwNiA5NS42OTkyQzQwLjA5NjQgOTUuNjAwMyA0MC4zMjU1IDk1LjU1MDggNDAuNTc4MSA5NS41NTA4QzQwLjc4MTIgOTUuNTUwOCA0MC45NjQ4IDk1LjU3OTQgNDEuMTI4OSA5NS42MzY3QzQxLjI5NTYgOTUuNjkxNCA0MS40Mzc1IDk1Ljc4MTIgNDEuNTU0NyA5NS45MDYyQzQxLjY3NDUgOTYuMDMxMiA0MS43NjU2IDk2LjE5NCA0MS44MjgxIDk2LjM5NDVDNDEuODkwNiA5Ni41OTI0IDQxLjkyMTkgOTYuODM1OSA0MS45MjE5IDk3LjEyNVY5OS44NTU1SDQwLjk3NjZWOTcuMTIxMUM0MC45NzY2IDk2LjkxOCA0MC45NDY2IDk2Ljc1NzggNDAuODg2NyA5Ni42NDA2QzQwLjgyOTQgOTYuNTIwOCA0MC43NDQ4IDk2LjQzNjIgNDAuNjMyOCA5Ni4zODY3QzQwLjUyMzQgOTYuMzM0NiA0MC4zODY3IDk2LjMwODYgNDAuMjIyNyA5Ni4zMDg2QzQwLjA2MTIgOTYuMzA4NiAzOS45MTY3IDk2LjM0MjQgMzkuNzg5MSA5Ni40MTAyQzM5LjY2MTUgOTYuNDc3OSAzOS41NTM0IDk2LjU3MDMgMzkuNDY0OCA5Ni42ODc1QzM5LjM3ODkgOTYuODA0NyAzOS4zMTI1IDk2Ljk0MDEgMzkuMjY1NiA5Ny4wOTM4QzM5LjIxODggOTcuMjQ3NCAzOS4xOTUzIDk3LjQxMTUgMzkuMTk1MyA5Ny41ODU5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDBfNDY3M185Nzc0KSI+CjxwYXRoIGQ9Ik00NC44ODgzIDEyMi41NzdMMTcuNDk2MSAxNDIuMzExTDggMTQ4LjM3MVYxNTUuMTQ1SDE5MlYxNDYuMjM3VjEzNi45ODdMMTc4LjM4OSAxNTEuMDI3TDE3MC44MDMgMTQyLjI0NkwxNjcuNTExIDE0NC44MjhMMTUzLjQ0MSAxNTQuNTE1QzE0NC4wNjIgMTQzLjk3OSAxMzYuMjQ2IDE0MC4wNDggMTI2LjAzOCAxMjkuNjk2TDExNS41MDUgMTQ1LjE1NkgxMDEuOTU5TDk5LjM0MjYgMTIwLjAzNEw5NC4wNzI4IDExMC4xNDVMNjguOTk3OSAxMzQuODE4TDYzLjAzNTQgMTM0LjA0M0w0NC44ODgzIDEyMi41NzdaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfNDY3M185Nzc0KSIvPgo8cGF0aCBkPSJNOCAxNDguMDI3TDE3LjU4NTQgMTQyLjAyMkw0NS4yMzUzIDEyMi40NjVMNjMuNTUzMSAxMzMuODI5TDY5LjU3MTcgMTM0LjU5Nkw5NCAxMTFMOTkuNSAxMTkuOTY2QzEwMC41MzEgMTI5LjY4OSAxMDAuOTY5IDEzNS4xMTkgMTAyIDE0NC44NDFIMTE1LjVMMTI2IDEyOS4zOTVMMTUzLjUgMTU0LjE0NUwxNzAuOTUyIDE0MS45OTRMMTc4LjUgMTUwLjYwOUwxOTIgMTM2LjgzOSIgc3Ryb2tlPSJ1cmwoI3BhaW50MV9saW5lYXJfNDY3M185Nzc0KSIgc3Ryb2tlLXdpZHRoPSIwLjkwMTMxOCIvPgo8L2c+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMV80NjczXzk3NzQpIj4KPHBhdGggZD0iTTggMTEwLjY0NUwxOTIgMTEwLjY0NSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiIHN0cm9rZS13aWR0aD0iMC4yOTQ5OTUiLz4KPHBhdGggZD0iTTggMTIwLjM5NUwxOTIgMTIwLjM5NSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiIHN0cm9rZS13aWR0aD0iMC4yOTQ5OTUiLz4KPHBhdGggZD0iTTggMTMwLjE0NUwxOTIgMTMwLjE0NSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiIHN0cm9rZS13aWR0aD0iMC4yOTQ5OTUiLz4KPHBhdGggZD0iTTggMTM5Ljg5NUwxOTIgMTM5Ljg5NSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiIHN0cm9rZS13aWR0aD0iMC4yOTQ5OTUiLz4KPHBhdGggZD0iTTggMTQ5LjY0NUwxOTIgMTQ5LjY0NSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiIHN0cm9rZS13aWR0aD0iMC4yOTQ5OTUiLz4KPC9nPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzQ2NzNfOTc3NCIgeDE9Ijk1LjU4ODkiIHkxPSIxNjAuMzI1IiB4Mj0iOTUuNTg4OSIgeTI9Ijk4LjMzODciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agb2Zmc2V0PSIwLjE3MzM4OSIgc3RvcC1jb2xvcj0iIzZGQ0M0QyIvPgo8c3RvcCBvZmZzZXQ9IjAuMzI3MDc0IiBzdG9wLWNvbG9yPSIjNzBDQzRDIi8+CjxzdG9wIG9mZnNldD0iMC4zMzE2NTYiIHN0b3AtY29sb3I9IiNGRUJBM0QiLz4KPHN0b3Agb2Zmc2V0PSIwLjQ4MDg3NiIgc3RvcC1jb2xvcj0iI0ZFQkEzRCIvPgo8c3RvcCBvZmZzZXQ9IjAuNDkxMjQyIiBzdG9wLWNvbG9yPSIjRkQ4RjNDIi8+CjxzdG9wIG9mZnNldD0iMC42Mzc0MzEiIHN0b3AtY29sb3I9IiNGRDhGM0MiLz4KPHN0b3Agb2Zmc2V0PSIwLjY0NTY0OSIgc3RvcC1jb2xvcj0iI0Y1NjE0OCIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MV9saW5lYXJfNDY3M185Nzc0IiB4MT0iOTQuNzg2MiIgeTE9IjE1NC41MzciIHgyPSI5NC43NzY0IiB5Mj0iOTguNDQ0OSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBvZmZzZXQ9IjAuMjU4NDUyIiBzdG9wLWNvbG9yPSIjM0ZBNzFBIi8+CjxzdG9wIG9mZnNldD0iMC4yNjk2MDEiIHN0b3AtY29sb3I9IiNGRkE2MDAiLz4KPHN0b3Agb2Zmc2V0PSIwLjQyNzI1OSIgc3RvcC1jb2xvcj0iI0ZGQTYwMCIvPgo8c3RvcCBvZmZzZXQ9IjAuNDQzODQ3IiBzdG9wLWNvbG9yPSIjRjU2RTA4Ii8+CjxzdG9wIG9mZnNldD0iMC42MDM1NDciIHN0b3AtY29sb3I9IiNGNTZFMDgiLz4KPHN0b3Agb2Zmc2V0PSIwLjYwNjQ2NyIgc3RvcC1jb2xvcj0iI0ZGQTYwMCIvPgo8c3RvcCBvZmZzZXQ9IjAuNjExNTkxIiBzdG9wLWNvbG9yPSIjRjA0MDIyIi8+CjwvbGluZWFyR3JhZGllbnQ+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNDY3M185Nzc0Ij4KPHJlY3Qgd2lkdGg9IjE4NCIgaGVpZ2h0PSI0NSIgZmlsbD0id2hpdGUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDggMTEwLjE0NSkiLz4KPC9jbGlwUGF0aD4KPGNsaXBQYXRoIGlkPSJjbGlwMV80NjczXzk3NzQiPgo8cmVjdCB3aWR0aD0iMTg0IiBoZWlnaHQ9IjQ1IiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOCAxMTAuMTQ1KSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=", - "description": "Contains widgets displaying industrial telemetry.", - "order": 9500, - "externalId": null, - "name": "Industrial widgets" - }, - "widgetTypeFqns": [ - "flow_rate_card", - "flow_rate_card_with_background", - "horizontal_flow_rate_card", - "horizontal_flow_rate_card_with_background", - "flow_rate_chart_card", - "flow_rate_chart_card_with_background", - "simple_flow_rate_chart_card", - "simple_flow_rate_chart_card_with_background", - "flow_rate_progress_bar", - "flow_rate_progress_bar_with_background", - "flow_rate_range_chart", - "flow_rate_range_chart_with_background", - "flow_rate_gauge", - "fluid_pressure_card", - "fluid_pressure_card_with_background", - "horizontal_fluid_pressure_card", - "horizontal_fluid_pressure_card_with_background", - "fluid_pressure_chart_card", - "fluid_pressure_chart_card_with_background", - "simple_fluid_pressure_chart_card", - "simple_fluid_pressure_chart_card_with_background", - "fluid_pressure_progress_bar", - "fluid_pressure_progress_bar_with_background", - "fluid_pressure_range_chart", - "fluid_pressure_range_chart_with_background", - "fluid_pressure_gauge", - "pump_vibration_card", - "pump_vibration_card_with_background", - "horizontal_pump_vibration_card", - "horizontal_pump_vibration_card_with_background", - "pump_vibration_chart_card", - "pump_vibration_chart_card_with_background", - "simple_pump_vibration_chart_card", - "simple_pump_vibration_chart_card_with_background", - "pump_vibration_range_chart", - "pump_vibration_range_chart_with_background", - "power_consumption_card", - "power_consumption_card_with_background", - "horizontal_power_consumption_card", - "horizontal_power_consumption_card_with_background", - "power_consumption_chart_card", - "power_consumption_chart_card_with_background", - "simple_power_consumption_chart_card", - "simple_power_consumption_chart_card_with_background", - "power_consumption_range_chart", - "power_consumption_range_chart_with_background", - "rotational_speed_card", - "rotational_speed_card_with_background", - "horizontal_rotational_speed_card", - "horizontal_rotational_speed_card_with_background", - "rotational_speed_chart_card", - "rotational_speed_chart_card_with_background", - "simple_rotational_speed_chart_card", - "simple_rotational_speed_chart_card_with_background", - "rotational_speed_progress_bar", - "rotational_speed_progress_bar_with_background", - "rotational_speed_range_chart", - "rotational_speed_range_chart_with_background", - "rotational_speed_gauge", - "efficiency_card", - "efficiency_card_with_background", - "horizontal_efficiency_card", - "horizontal_efficiency_card_with_background", - "efficiency_chart_card", - "efficiency_chart_card_with_background", - "simple_efficiency_chart_card", - "simple_efficiency_chart_card_with_background", - "efficiency_progress_bar", - "efficiency_progress_bar_with_background", - "efficiency_range_chart", - "efficiency_range_chart_with_background" - ] -} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/outdoor_environment.json b/application/src/main/data/json/system/widget_bundles/outdoor_environment.json deleted file mode 100644 index c7705c5e91..0000000000 --- a/application/src/main/data/json/system/widget_bundles/outdoor_environment.json +++ /dev/null @@ -1,190 +0,0 @@ -{ - "widgetsBundle": { - "alias": "outdoor_environment", - "title": "Outdoor Environment", - "image": "data:image/png;base64,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", - "description": "Contains widgets displaying outdoor environment telemetry.", - "order": 11000, - "externalId": null, - "name": "Outdoor Environment" - }, - "widgetTypeFqns": [ - "temperature_card", - "temperature_card_with_background", - "horizontal_temperature_card", - "horizontal_temperature_card_with_background", - "temperature_chart_card", - "temperature_chart_card_with_background", - "simple_temperature_chart_card", - "simple_temperature_chart_card_with_background", - "temperature_range_chart", - "temperature_range_chart_with_background", - "temperature_gauge", - "humidity_card", - "humidity_card_with_background", - "horizontal_humidity_card", - "horizontal_humidity_card_with_background", - "humidity_chart_card", - "humidity_chart_card_with_background", - "simple_humidity_chart_card", - "simple_humidity_chart_card_with_background", - "humidity_progress_bar", - "humidity_progress_bar_with_background", - "pressure_card", - "pressure_card_with_background", - "horizontal_pressure_card", - "horizontal_pressure_card_with_background", - "pressure_chart_card", - "pressure_chart_card_with_background", - "simple_pressure_chart_card", - "simple_pressure_chart_card_with_background", - "pressure_progress_bar", - "pressure_progress_bar_with_background", - "wind_speed_card", - "wind_speed_card_with_background", - "horizontal_wind_speed_card", - "horizontal_wind_speed_card_with_background", - "wind_speed_chart_card", - "wind_speed_chart_card_with_background", - "simple_wind_speed_chart_card", - "simple_wind_speed_chart_card_with_background", - "wind_speed_and_direction", - "wind_speed_and_direction_with_background", - "rainfall_card", - "rainfall_card_with_background", - "horizontal_rainfall_card", - "horizontal_rainfall_card_with_background", - "rainfall_chart_card", - "rainfall_chart_card_with_background", - "simple_rainfall_chart_card", - "simple_rainfall_chart_card_with_background", - "solar_radiation_card", - "solar_radiation_card_with_background", - "horizontal_solar_radiation_card", - "horizontal_solar_radiation_card_with_background", - "solar_radiation_chart_card", - "solar_radiation_chart_card_with_background", - "simple_solar_radiation_chart_card", - "simple_solar_radiation_chart_card_with_background", - "uv_index_card", - "uv_index_card_with_background", - "horizontal_uv_index_card", - "horizontal_uv_index_card_with_background", - "uv_index_chart_card", - "uv_index_chart_card_with_background", - "simple_uv_index_chart_card", - "simple_uv_index_chart_card_with_background", - "air_quality_card", - "air_quality_card_with_background", - "horizontal_air_quality_card", - "horizontal_air_quality_card_with_background", - "air_quality_chart_card", - "air_quality_chart_card_with_background", - "simple_air_quality_chart_card", - "simple_air_quality_chart_card_with_background", - "visibility_card", - "visibility_card_with_background", - "horizontal_visibility_card", - "horizontal_visibility_card_with_background", - "visibility_chart_card", - "visibility_chart_card_with_background", - "simple_visibility_chart_card", - "simple_visibility_chart_card_with_background", - "ground_temperature_card", - "ground_temperature_card_with_background", - "horizontal_ground_temperature_card", - "horizontal_ground_temperature_card_with_background", - "ground_temperature_chart_card", - "ground_temperature_chart_card_with_background", - "simple_ground_temperature_chart_card", - "simple_ground_temperature_chart_card_with_background", - "soil_moisture_card", - "soil_moisture_card_with_background", - "horizontal_soil_moisture_card", - "horizontal_soil_moisture_card_with_background", - "soil_moisture_chart_card", - "soil_moisture_chart_card_with_background", - "simple_soil_moisture_chart_card", - "simple_soil_moisture_chart_card_with_background", - "soil_moisture_progress_bar", - "soil_moisture_progress_bar_with_background", - "noise_level_card", - "noise_level_card_with_background", - "horizontal_noise_level_card", - "horizontal_noise_level_card_with_background", - "noise_level_chart_card", - "noise_level_chart_card_with_background", - "simple_noise_level_chart_card", - "simple_noise_level_chart_card_with_background", - "vibration_card", - "vibration_card_with_background", - "horizontal_vibration_card", - "horizontal_vibration_card_with_background", - "vibration_chart_card", - "vibration_chart_card_with_background", - "simple_vibration_chart_card", - "simple_vibration_chart_card_with_background", - "leaf_wetness_card", - "leaf_wetness_card_with_background", - "horizontal_leaf_wetness_card", - "horizontal_leaf_wetness_card_with_background", - "leaf_wetness_chart_card", - "leaf_wetness_chart_card_with_background", - "simple_leaf_wetness_chart_card", - "simple_leaf_wetness_chart_card_with_background", - "leaf_wetness_progress_bar", - "leaf_wetness_progress_bar_with_background", - "snow_depth_card", - "snow_depth_card_with_background", - "horizontal_snow_depth_card", - "horizontal_snow_depth_card_with_background", - "snow_depth_chart_card", - "snow_depth_chart_card_with_background", - "simple_snow_depth_chart_card", - "simple_snow_depth_chart_card_with_background", - "co2_card", - "co2_card_with_background", - "horizontal_co2_card", - "horizontal_co2_card_with_background", - "co2_chart_card", - "co2_chart_card_with_background", - "simple_co2_chart_card", - "simple_co2_chart_card_with_background", - "flooding_level_card", - "flooding_level_card_with_background", - "horizontal_flooding_level_card", - "horizontal_flooding_level_card_with_background", - "flooding_level_chart_card", - "flooding_level_chart_card_with_background", - "simple_flooding_level_chart_card", - "simple_flooding_level_chart_card_with_background", - "flooding_level_progress_bar", - "flooding_level_progress_bar_with_background", - "illuminance_card", - "illuminance_card_with_background", - "horizontal_illuminance_card", - "horizontal_illuminance_card_with_background", - "illuminance_chart_card", - "illuminance_chart_card_with_background", - "simple_illuminance_chart_card", - "simple_illuminance_chart_card_with_background", - "illuminance_progress_bar", - "illuminance_progress_bar_with_background", - "pm2_5_card", - "pm2_5_card_with_background", - "horizontal_pm2_5_card", - "horizontal_pm2_5_card_with_background", - "pm2_5_chart_card", - "pm2_5_chart_card_with_background", - "simple_pm2_5_chart_card", - "simple_pm2_5_chart_card_with_background", - "pm10_card", - "pm10_card_with_background", - "horizontal_pm10_card", - "horizontal_pm10_card_with_background", - "pm10_chart_card", - "pm10_chart_card_with_background", - "simple_pm10_chart_card", - "simple_pm10_chart_card_with_background" - ] -} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/air_quality_index_card.json b/application/src/main/data/json/system/widget_types/air_quality_index_card.json index a12fb7a5c3..5ef070ec8d 100644 --- a/application/src/main/data/json/system/widget_types/air_quality_index_card.json +++ b/application/src/main/data/json/system/widget_types/air_quality_index_card.json @@ -1,7 +1,7 @@ { "fqn": "air_quality_card", "name": "Air quality index card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/air_quality_index_card_system_widget_image.png", "description": "Displays the latest air quality index telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/air_quality_index_card_with_background.json b/application/src/main/data/json/system/widget_types/air_quality_index_card_with_background.json index 801b91330b..faecf351f3 100644 --- a/application/src/main/data/json/system/widget_types/air_quality_index_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/air_quality_index_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "air_quality_card_with_background", "name": "Air quality index card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/air_quality_index_card_with_background_system_widget_image.png", "description": "Displays the latest air quality index telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/air_quality_index_chart_card.json b/application/src/main/data/json/system/widget_types/air_quality_index_chart_card.json index 58ba002255..95058ecf29 100644 --- a/application/src/main/data/json/system/widget_types/air_quality_index_chart_card.json +++ b/application/src/main/data/json/system/widget_types/air_quality_index_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "air_quality_chart_card", "name": "Air quality index chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/air_quality_index_chart_card_system_widget_image.png", "description": "Displays air quality index data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/air_quality_index_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/air_quality_index_chart_card_with_background.json index 43ddb147ac..50bf9e06ec 100644 --- a/application/src/main/data/json/system/widget_types/air_quality_index_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/air_quality_index_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "air_quality_chart_card_with_background", "name": "Air quality index chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/air_quality_index_chart_card_with_background_system_widget_image.png", "description": "Displays air quality index data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card.json b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card.json index 7fdb133b93..b0bdb30d42 100644 --- a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card.json +++ b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card.json @@ -1,7 +1,7 @@ { "fqn": "carbon_monoxide_co_card", "name": "Carbon monoxide (CO) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/CO-value-card.svg", "description": "A colorless and odorless gas resulting from incomplete combustion, particularly in motor vehicles.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card_with_background.json b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card_with_background.json index ed9e2203b4..3f570f3ded 100644 --- a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "carbon_monoxide_co_card_with_background", "name": "Carbon monoxide (CO) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/CO-value-card-with-background.svg", "description": "A colorless and odorless gas resulting from incomplete combustion, particularly in motor vehicles.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card.json b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card.json index f1ac79ea11..dd712fcf75 100644 --- a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card.json +++ b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "carbon_monoxide_co_chart_card", "name": "Carbon monoxide (CO) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/CO-value-and-chart-card.svg", "description": "A colorless and odorless gas resulting from incomplete combustion, particularly in motor vehicles. Results displayed by combining the latest and aggregated values and optional simplified chart. ", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card_with_background.json index cddc48b6e4..d3c334f7b2 100644 --- a/application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "carbon_monoxide_co_chart_card_with_background", "name": "Carbon monoxide (CO) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/CO-value-chart-card-with-background.svg", "description": "A colorless and odorless gas resulting from incomplete combustion, particularly in motor vehicles. Results displayed by combining the latest and aggregated values and optional simplified chart. ", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/co2_card.json b/application/src/main/data/json/system/widget_types/co2_card.json index 5d48dbb64f..bf95ee5cee 100644 --- a/application/src/main/data/json/system/widget_types/co2_card.json +++ b/application/src/main/data/json/system/widget_types/co2_card.json @@ -1,7 +1,7 @@ { "fqn": "co2_card", "name": "CO2 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/co2_card_system_widget_image.png", "description": "Displays the latest CO2 level telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/co2_card_with_background.json b/application/src/main/data/json/system/widget_types/co2_card_with_background.json index ddee660eb3..5de534ff69 100644 --- a/application/src/main/data/json/system/widget_types/co2_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/co2_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "co2_card_with_background", "name": "CO2 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/co2_card_with_background_system_widget_image.png", "description": "Displays the latest CO2 level telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/co2_chart_card.json b/application/src/main/data/json/system/widget_types/co2_chart_card.json index 70d06ffe37..4aff9d8f84 100644 --- a/application/src/main/data/json/system/widget_types/co2_chart_card.json +++ b/application/src/main/data/json/system/widget_types/co2_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "co2_chart_card", "name": "CO2 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/co2_chart_card_system_widget_image.png", "description": "Displays a CO2 level data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/co2_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/co2_chart_card_with_background.json index 3de7fadf01..4f0b248af9 100644 --- a/application/src/main/data/json/system/widget_types/co2_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/co2_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "co2_chart_card_with_background", "name": "CO2 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/co2_chart_card_with_background_system_widget_image.png", "description": "Displays a CO2 level data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/efficiency_card.json b/application/src/main/data/json/system/widget_types/efficiency_card.json index 92cb86ab35..24c1a5f900 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_card.json +++ b/application/src/main/data/json/system/widget_types/efficiency_card.json @@ -1,7 +1,7 @@ { "fqn": "efficiency_card", "name": "Efficiency card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/efficiency_card.svg", "description": "Displays the latest efficiency telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/efficiency_card_with_background.json b/application/src/main/data/json/system/widget_types/efficiency_card_with_background.json index f8bef025dd..15929bcf7d 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/efficiency_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "efficiency_card_with_background", "name": "Efficiency card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/efficiency_card_with_background.svg", "description": "Displays the latest efficiency telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/efficiency_chart_card.json b/application/src/main/data/json/system/widget_types/efficiency_chart_card.json index 298eddb21c..6a546bfd5e 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_chart_card.json +++ b/application/src/main/data/json/system/widget_types/efficiency_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "efficiency_chart_card", "name": "Efficiency chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/efficiency_chart_card.svg", "description": "Displays efficiency data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/efficiency_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/efficiency_chart_card_with_background.json index bd3b02b991..98391712de 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/efficiency_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "efficiency_chart_card_with_background", "name": "Efficiency chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/efficiency_chart_card_with_background.svg", "description": "Displays efficiency data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/efficiency_progress_bar.json b/application/src/main/data/json/system/widget_types/efficiency_progress_bar.json index a2090db66d..de57673e9a 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/efficiency_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "efficiency_progress_bar", "name": "Efficiency progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/efficiency_progress_bar.svg", "description": "Displays efficiency reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/efficiency_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/efficiency_progress_bar_with_background.json index b337d40439..d398afa2d8 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/efficiency_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "efficiency_progress_bar_with_background", "name": "Efficiency progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/efficiency_progress_bar_with_background.svg", "description": "Displays efficiency reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/efficiency_range_chart.json b/application/src/main/data/json/system/widget_types/efficiency_range_chart.json index ce08ee7c5b..f6451ec2bc 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_range_chart.json +++ b/application/src/main/data/json/system/widget_types/efficiency_range_chart.json @@ -1,7 +1,7 @@ { "fqn": "efficiency_range_chart", "name": "Efficiency range chart", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/efficiency_range_chart.svg", "description": "Displays changes to efficiency data over time visualized with color ranges.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/efficiency_range_chart_with_background.json b/application/src/main/data/json/system/widget_types/efficiency_range_chart_with_background.json index df6e41242c..5cf9870734 100644 --- a/application/src/main/data/json/system/widget_types/efficiency_range_chart_with_background.json +++ b/application/src/main/data/json/system/widget_types/efficiency_range_chart_with_background.json @@ -1,7 +1,7 @@ { "fqn": "efficiency_range_chart_with_background", "name": "Efficiency range chart with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/efficiency_range_chart_with_background.svg", "description": "Displays changes to efficiency data over time visualized with color ranges and background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flooding_level_card.json b/application/src/main/data/json/system/widget_types/flooding_level_card.json index a6f6e610b9..1834e77cb6 100644 --- a/application/src/main/data/json/system/widget_types/flooding_level_card.json +++ b/application/src/main/data/json/system/widget_types/flooding_level_card.json @@ -1,7 +1,7 @@ { "fqn": "flooding_level_card", "name": "Flooding level card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flooding_level_card_system_widget_image.png", "description": "Displays the latest flooding level telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flooding_level_card_with_background.json b/application/src/main/data/json/system/widget_types/flooding_level_card_with_background.json index 727122f32b..6b6d84dbd4 100644 --- a/application/src/main/data/json/system/widget_types/flooding_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/flooding_level_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "flooding_level_card_with_background", "name": "Flooding level card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flooding_level_card_with_background_system_widget_image.png", "description": "Displays the latest flooding level telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flooding_level_chart_card.json b/application/src/main/data/json/system/widget_types/flooding_level_chart_card.json index 5592656dc4..5317ceac0a 100644 --- a/application/src/main/data/json/system/widget_types/flooding_level_chart_card.json +++ b/application/src/main/data/json/system/widget_types/flooding_level_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "flooding_level_chart_card", "name": "Flooding level chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flooding_level_chart_card_system_widget_image.png", "description": "Displays flooding level data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flooding_level_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/flooding_level_chart_card_with_background.json index 5b7dfda3da..21cc83c7a1 100644 --- a/application/src/main/data/json/system/widget_types/flooding_level_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/flooding_level_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "flooding_level_chart_card_with_background", "name": "Flooding level chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flooding_level_chart_card_with_background_system_widget_image.png", "description": "Displays a flooding level data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flooding_level_progress_bar.json b/application/src/main/data/json/system/widget_types/flooding_level_progress_bar.json index e14d49d98b..b761c7af4e 100644 --- a/application/src/main/data/json/system/widget_types/flooding_level_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/flooding_level_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "flooding_level_progress_bar", "name": "Flooding level progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flooding_level_progress_bar_system_widget_image.png", "description": "Displays flooding level reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flooding_level_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/flooding_level_progress_bar_with_background.json index 4c8cd2808b..674336221f 100644 --- a/application/src/main/data/json/system/widget_types/flooding_level_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/flooding_level_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "flooding_level_progress_bar_with_background", "name": "Flooding level progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flooding_level_progress_bar_with_background_system_widget_image.png", "description": "Displays flooding level reading as a horizontal progress bar with the background image. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_card.json b/application/src/main/data/json/system/widget_types/flow_rate_card.json index c78fcb704b..494bea6bc6 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_card.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_card.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_card", "name": "Flow rate card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_card.svg", "description": "Displays the latest flow rate telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_card_with_background.json b/application/src/main/data/json/system/widget_types/flow_rate_card_with_background.json index 689b6fc5e9..c471193b51 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_card_with_background", "name": "Flow rate card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_card_with_background.svg", "description": "Displays the latest flow rate telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_chart_card.json b/application/src/main/data/json/system/widget_types/flow_rate_chart_card.json index df5c63b14e..4beefb2187 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_chart_card.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_chart_card", "name": "Flow rate chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_chart_card.svg", "description": "Displays flow rate data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/flow_rate_chart_card_with_background.json index 0ffb5a7122..5a8ad8064e 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_chart_card_with_background", "name": "Flow rate chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_chart_card_with_background.svg", "description": "Displays flow rate data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_gauge.json b/application/src/main/data/json/system/widget_types/flow_rate_gauge.json index d67ef8ad90..73a8106a67 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_gauge.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_gauge.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_gauge", "name": "Flow rate gauge", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_gauge.svg", "description": "Flow rate gauge.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_progress_bar.json b/application/src/main/data/json/system/widget_types/flow_rate_progress_bar.json index a7bdcaeeb9..cb0291b054 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_progress_bar", "name": "Flow rate progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_progress_bar_(1).svg", "description": "Displays flow rate reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/flow_rate_progress_bar_with_background.json index 69192e5682..81072ef64d 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_progress_bar_with_background", "name": "Flow rate progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_progress_bar_with_background.svg", "description": "Displays flow rate reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_range_chart.json b/application/src/main/data/json/system/widget_types/flow_rate_range_chart.json index c19becc363..90b936ad95 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_range_chart.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_range_chart.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_range_chart", "name": "Flow rate range chart", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_range_chart.svg", "description": "Displays changes to flow rate data over time visualized with color ranges.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/flow_rate_range_chart_with_background.json b/application/src/main/data/json/system/widget_types/flow_rate_range_chart_with_background.json index f5b7d437b2..7383538dee 100644 --- a/application/src/main/data/json/system/widget_types/flow_rate_range_chart_with_background.json +++ b/application/src/main/data/json/system/widget_types/flow_rate_range_chart_with_background.json @@ -1,7 +1,7 @@ { "fqn": "flow_rate_range_chart_with_background", "name": "Flow rate range chart with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/flow_rate_range_chart_with_background.svg", "description": "Displays changes to flow rate data over time visualized with color ranges and background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_card.json b/application/src/main/data/json/system/widget_types/fluid_pressure_card.json index 32724202b5..6a073d43c0 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_card.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_card.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_card", "name": "Pressure card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_card.svg", "description": "Displays the latest fluid pressure telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_card_with_background.json b/application/src/main/data/json/system/widget_types/fluid_pressure_card_with_background.json index 6e1175f4f2..85a44eac62 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_card_with_background", "name": "Pressure card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_card_with_background.svg", "description": "Displays the latest fluid pressure telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_chart_card.json b/application/src/main/data/json/system/widget_types/fluid_pressure_chart_card.json index ee27a6b376..c6de6acc35 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_chart_card.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_chart_card", "name": "Pressure chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_chart_card.svg", "description": "Displays fluid pressure data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/fluid_pressure_chart_card_with_background.json index e9e9fede9a..122b553773 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_chart_card_with_background", "name": "Pressure chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_chart_card_with_background.svg", "description": "Displays fluid pressure data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_gauge.json b/application/src/main/data/json/system/widget_types/fluid_pressure_gauge.json index c555fae338..e17d605f08 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_gauge.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_gauge.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_gauge", "name": "Pressure gauge", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_gauge.svg", "description": "Fluid pressure gauge.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar.json b/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar.json index cd1c1571ac..89c2d9be23 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_progress_bar", "name": "Pressure progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_progress_bar_(1).svg", "description": "Displays fluid pressure reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar_with_background.json index f97347d1e5..1a60df16ff 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_progress_bar_with_background", "name": "Pressure progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/progress_bar_with_background.svg", "description": "Displays fluid pressure reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_range_chart.json b/application/src/main/data/json/system/widget_types/fluid_pressure_range_chart.json index fd948984c9..2f4c231e92 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_range_chart.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_range_chart.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_range_chart", "name": "Pressure range chart", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_range_chart_(1).svg", "description": "Displays changes to fluid pressure data over time visualized with color ranges.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/fluid_pressure_range_chart_with_background.json b/application/src/main/data/json/system/widget_types/fluid_pressure_range_chart_with_background.json index a8e3c8df2c..efb0aea62e 100644 --- a/application/src/main/data/json/system/widget_types/fluid_pressure_range_chart_with_background.json +++ b/application/src/main/data/json/system/widget_types/fluid_pressure_range_chart_with_background.json @@ -1,7 +1,7 @@ { "fqn": "fluid_pressure_range_chart_with_background", "name": "Pressure range chart with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_range_chart_with_background.svg", "description": "Displays changes to fluid pressure data over time visualized with color ranges and background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/ground_temperature_card.json b/application/src/main/data/json/system/widget_types/ground_temperature_card.json index d93653fb8d..e4b704a432 100644 --- a/application/src/main/data/json/system/widget_types/ground_temperature_card.json +++ b/application/src/main/data/json/system/widget_types/ground_temperature_card.json @@ -1,7 +1,7 @@ { "fqn": "ground_temperature_card", "name": "Ground temperature card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ground_temperature_card_system_widget_image.png", "description": "Displays the latest ground temperature telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/ground_temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/ground_temperature_card_with_background.json index 3b59b417d5..0805f706af 100644 --- a/application/src/main/data/json/system/widget_types/ground_temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/ground_temperature_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "ground_temperature_card_with_background", "name": "Ground temperature card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ground_temperature_card_with_background_system_widget_image.png", "description": "Displays the latest ground temperature telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/ground_temperature_chart_card.json b/application/src/main/data/json/system/widget_types/ground_temperature_chart_card.json index 949e48f9de..bee76a8789 100644 --- a/application/src/main/data/json/system/widget_types/ground_temperature_chart_card.json +++ b/application/src/main/data/json/system/widget_types/ground_temperature_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "ground_temperature_chart_card", "name": "Ground temperature chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ground_temperature_chart_card_system_widget_image.png", "description": "Displays a ground temperature data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/ground_temperature_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/ground_temperature_chart_card_with_background.json index 06bf06bd98..acef304a20 100644 --- a/application/src/main/data/json/system/widget_types/ground_temperature_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/ground_temperature_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "ground_temperature_chart_card_with_background", "name": "Ground temperature chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ground_temperature_chart_card_with_background_system_widget_image.png", "description": "Displays a ground temperature data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card.json b/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card.json index 3dc899c2d8..ddc4d2e9d1 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_air_quality_card", "name": "Horizontal air quality index card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_air_quality_index_card_system_widget_image.png", "description": "Displays the latest air quality index telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card_with_background.json index 989512f7e1..45c47227e0 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_air_quality_index_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_air_quality_card_with_background", "name": "Horizontal air quality index card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_air_quality_index_card_with_background_system_widget_image.png", "description": "Displays the latest air quality index telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card.json b/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card.json index ad9a952d30..3b105e8063 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_carbon_monoxide_co_card", "name": "Horizontal carbon monoxide (CO) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/CO-value-card-horizontal.svg", "description": "A colorless and odorless gas resulting from incomplete combustion, particularly in motor vehicles.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card_with_background.json index f6f003894a..1bfff05acf 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_carbon_monoxide__co__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_carbon_monoxide_co_card_with_background", "name": "Horizontal carbon monoxide (CO) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/CO-horizontal-value-card-with-background.svg", "description": "A colorless and odorless gas resulting from incomplete combustion, particularly in motor vehicles.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_co2_card.json b/application/src/main/data/json/system/widget_types/horizontal_co2_card.json index 300686a0d1..7605e3fb7a 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_co2_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_co2_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_co2_card", "name": "Horizontal CO2 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_co2_card_system_widget_image.png", "description": "Displays the latest CO2 level telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_co2_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_co2_card_with_background.json index 78868b1a7e..dbeb2e16b2 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_co2_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_co2_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_co2_card_with_background", "name": "Horizontal CO2 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_co2_card_with_background_system_widget_image.png", "description": "Displays the latest CO2 level telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_efficiency_card.json b/application/src/main/data/json/system/widget_types/horizontal_efficiency_card.json index 2d30185a5e..44540674f9 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_efficiency_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_efficiency_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_efficiency_card", "name": "Horizontal efficiency card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_efficiency_card.svg", "description": "Displays the latest efficiency telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_efficiency_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_efficiency_card_with_background.json index 44d80a7b96..2620208556 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_efficiency_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_efficiency_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_efficiency_card_with_background", "name": "Horizontal efficiency card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_efficiency_card_with_background.svg", "description": "Displays the latest efficiency telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card.json b/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card.json index 94179c9507..b0957b576b 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_flooding_level_card", "name": "Horizontal flooding level card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_flooding_level_card_system_widget_image.png", "description": "Displays the latest flooding level telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card_with_background.json index c65691497e..72d957b37f 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_flooding_level_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_flooding_level_card_with_background", "name": "Horizontal flooding level card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_flooding_level_card_with_background_system_widget_image.png", "description": "Displays the latest flooding level telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card.json b/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card.json index b0e2626fbb..f5a7c2e698 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_flow_rate_card", "name": "Horizontal flow rate card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_flow_rate_card.svg", "description": "Displays the latest flow rate telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card_with_background.json index 7b2c4ceb51..a684e71333 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_flow_rate_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_flow_rate_card_with_background", "name": "Horizontal flow rate card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_flow_rate_card_with_background.svg", "description": "Displays the latest flow rate telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card.json b/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card.json index ebeec0fe51..7556ff5cf0 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_fluid_pressure_card", "name": "Horizontal pressure card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_pressure_card.svg", "description": "Displays the latest fluid pressure telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card_with_background.json index e15bd24d4d..77532e81f2 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_fluid_pressure_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_fluid_pressure_card_with_background", "name": "Horizontal pressure card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_pressure_card_with_background.svg", "description": "Displays the latest fluid pressure telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card.json b/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card.json index ac87fdd54f..3432f84b29 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_ground_temperature_card", "name": "Horizontal ground temperature card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_ground_temperature_card_system_widget_image.png", "description": "Displays the latest ground temperature telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card_with_background.json index f3e44ba702..8d5351a819 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_ground_temperature_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_ground_temperature_card_with_background", "name": "Horizontal ground temperature card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_ground_temperature_card_with_background_system_widget_image.png", "description": "Displays the latest ground temperature telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_humidity_card.json b/application/src/main/data/json/system/widget_types/horizontal_humidity_card.json index 220a94e9e7..b1c8d0aebf 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_humidity_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_humidity_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_humidity_card", "name": "Horizontal humidity card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_humidity_card_system_widget_image.png", "description": "Displays the latest humidity telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_humidity_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_humidity_card_with_background.json index 05ad28b2d9..db557f449b 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_humidity_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_humidity_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_humidity_card_with_background", "name": "Horizontal humidity card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_humidity_card_with_background_system_widget_image.png", "description": "Displays the latest humidity telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_illuminance_card.json b/application/src/main/data/json/system/widget_types/horizontal_illuminance_card.json index 01d6d22c96..0f06bf4fd2 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_illuminance_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_illuminance_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_illuminance_card", "name": "Horizontal illuminance card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_illuminance_card_system_widget_image.png", "description": "Displays the latest illuminance telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_illuminance_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_illuminance_card_with_background.json index 232a07701b..7c8846e9ac 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_illuminance_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_illuminance_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_illuminance_card_with_background", "name": "Horizontal illuminance card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_illuminance_card_with_background_system_widget_image.png", "description": "Displays the latest illuminance telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card.json b/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card.json index 07db7700b8..cd9d7f2b2b 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_individual_allergy_index_iai_card", "name": "Horizontal individual allergy index (IAI) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/IAI-value-card-horizontal.svg", "description": "Indicates the concentration of airborne allergens, including pollen and mold spores, which can trigger allergic reactions in sensitive individuals.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card_with_background.json index 2498e25b89..5f5400d5f9 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_individual_allergy_index__iai__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_individual_allergy_index_iai_card_with_background", "name": "Horizontal individual allergy index (IAI) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/IAI-value-card-horizontal-with-background.svg", "description": "Indicates the concentration of airborne allergens, including pollen and mold spores, which can trigger allergic reactions in sensitive individuals.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card.json b/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card.json index aeeae67d47..9f47f98028 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_leaf_wetness_card", "name": "Horizontal leaf wetness card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_leaf_wetness_card_system_widget_image.png", "description": "Displays the latest leaf wetness telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card_with_background.json index 8d2cb283ab..406ebda5e2 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_leaf_wetness_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_leaf_wetness_card_with_background", "name": "Horizontal leaf wetness card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_leaf_wetness_card_with_background_system_widget_image.png", "description": "Displays the latest leaf wetness telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card.json b/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card.json index aaa8170eef..05da23db5f 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_nitrogen_dioxide_no2_card", "name": "Horizontal nitrogen dioxide (NO2) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/NO2-value-card-horizontal.svg", "description": "A gas produced from vehicle emissions and the burning of fossil fuels.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card_with_background.json index 369d7de0ec..e4231b4128 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_nitrogen_dioxide__no2__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_nitrogen_dioxide_no2_card_with_background", "name": "Horizontal nitrogen dioxide (NO2) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/NO2-horizontal-value-card-with-background.svg", "description": "A gas produced from vehicle emissions and the burning of fossil fuels.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_noise_level_card.json b/application/src/main/data/json/system/widget_types/horizontal_noise_level_card.json index 39ba22f0e7..e6f99e1dde 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_noise_level_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_noise_level_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_noise_level_card", "name": "Horizontal noise level card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_noise_level_card_system_widget_image.png", "description": "Displays the latest noise level telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_noise_level_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_noise_level_card_with_background.json index c45812b66e..8ec72aa98c 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_noise_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_noise_level_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_noise_level_card_with_background", "name": "Horizontal noise level card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_noise_level_card_with_background_system_widget_image.png", "description": "Displays the latest noise level telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card.json b/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card.json index bf75f97846..f1e4c41803 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_ozone_o3_card", "name": "Horizontal ozone (O3) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ozone-value-card-horizontal.svg", "description": "A beneficial layer in the upper atmosphere, but harmful when present near ground level. Results mainly from vehicle exhaust and industrial emissions.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card_with_background.json index 5b7c3464bc..3c4db38b7d 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_ozone__o3__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_ozone_o3_card_with_background", "name": "Horizontal ozone (O3) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ozone-value-card-horizontal-with-background.svg", "description": "A beneficial layer in the upper atmosphere, but harmful when present near ground level. Results mainly from vehicle exhaust and industrial emissions.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_pm10_card.json b/application/src/main/data/json/system/widget_types/horizontal_pm10_card.json index a50435db0e..9c25ad8daf 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pm10_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pm10_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_pm10_card", "name": "Horizontal PM10 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_pm10_card_system_widget_image.png", "description": "Displays the latest fine and coarse particulate matter (PM10) telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_pm10_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_pm10_card_with_background.json index 12e6a9904f..8ee48f8846 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pm10_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pm10_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_pm10_card_with_background", "name": "Horizontal PM10 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_pm10_card_with_background_system_widget_image.png", "description": "Displays the latest fine and coarse particulate matter (PM10) telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card.json b/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card.json index 658c51a0b9..f77c3bbfd6 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_pm2_5_card", "name": "Horizontal PM2.5 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_pm2_5_card_system_widget_image.png", "description": "Displays the latest fine particulate matter (PM2.5) telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card_with_background.json index 3aec6a698f..e0bd353212 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pm2_5_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_pm2_5_card_with_background", "name": "Horizontal PM2.5 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_pm2_5_card_with_background_system_widget_image.png", "description": "Displays the latest fine particulate matter (PM2.5) telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card.json b/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card.json index e56f1d307b..a31cee2a2c 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_power_consumption_card", "name": "Horizontal power consumption card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_power_consumption_card.svg", "description": "Displays the latest power consumption telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card_with_background.json index fc2c20d623..423a25e244 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_power_consumption_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_power_consumption_card_with_background", "name": "Horizontal power consumption card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_power_consumption_card_with_background.svg", "description": "Displays the latest power consumption telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_pressure_card.json b/application/src/main/data/json/system/widget_types/horizontal_pressure_card.json index 197ebc2488..f969d0ba8f 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pressure_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pressure_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_pressure_card", "name": "Horizontal pressure card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_pressure_card_system_widget_image.png", "description": "Displays the latest pressure telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_pressure_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_pressure_card_with_background.json index 8872f4e0e1..0651633c3f 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pressure_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pressure_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_pressure_card_with_background", "name": "Horizontal pressure card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_pressure_card_with_background_system_widget_image.png", "description": "Displays the latest pressure telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card.json b/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card.json index cc522255e1..96ad4dbae6 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_pump_vibration_card", "name": "Horizontal vibration card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_vibration_card.svg", "description": "Displays the latest vibration telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card_with_background.json index 9e4f306b0d..7147fa8d54 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_pump_vibration_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_pump_vibration_card_with_background", "name": "Horizontal vibration card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_vibration_card_with_background.svg", "description": "Displays the latest vibration telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_radon_level_card.json b/application/src/main/data/json/system/widget_types/horizontal_radon_level_card.json index 3d27d7a9a2..f9c8a7940a 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_radon_level_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_radon_level_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_radon_level_card", "name": "Horizontal radon level card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_radon_level_card_system_widget_image.png", "description": "Displays the latest radon level telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_radon_level_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_radon_level_card_with_background.json index 9ace2ca876..87ba073f50 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_radon_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_radon_level_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_radon_level_card_with_background", "name": "Horizontal radon level card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_radon_level_card_with_background_system_widget_image.png", "description": "Displays the latest radon level telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_rainfall_card.json b/application/src/main/data/json/system/widget_types/horizontal_rainfall_card.json index 7608e5b765..c26b4f2a5b 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_rainfall_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_rainfall_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_rainfall_card", "name": "Horizontal rainfall card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_rainfall_card_system_widget_image.png", "description": "Displays the latest rainfall telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_rainfall_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_rainfall_card_with_background.json index 86adcd593f..22efbe6b4f 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_rainfall_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_rainfall_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_rainfall_card_with_background", "name": "Horizontal rainfall card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_rainfall_card_with_background_system_widget_image.png", "description": "Displays the latest rainfall telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card.json b/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card.json index 59733ff38f..3059d76ed5 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_rotational_speed_card", "name": "Horizontal rotational speed card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_rotational_speed_card.svg", "description": "Displays the latest rotational speed telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card_with_background.json index 6e2cccf0a2..1e2cf858a0 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_rotational_speed_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_rotational_speed_card_with_background", "name": "Horizontal rotational speed card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_rotational_speed_card_with_background.svg", "description": "Displays the latest rotational speed telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card.json b/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card.json index b222b915de..ae3853baa9 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_snow_depth_card", "name": "Horizontal snow depth card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_snow_depth_card_system_widget_image.png", "description": "Displays the latest snow depth telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card_with_background.json index 6f271d3b67..d47d37adf0 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_snow_depth_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_snow_depth_card_with_background", "name": "Horizontal snow depth card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_snow_depth_card_with_background_system_widget_image.png", "description": "Displays the latest snow depth telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card.json b/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card.json index 5ac1793475..eae3336b46 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_soil_moisture_card", "name": "Horizontal soil moisture card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_soil_moisture_card_system_widget_image.png", "description": "Displays the latest soil moisture telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card_with_background.json index c53214bc37..9a4786eaf6 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_soil_moisture_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_soil_moisture_card_with_background", "name": "Horizontal soil moisture card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_soil_moisture_card_with_background_system_widget_image.png", "description": "Displays the latest soil moisture telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card.json b/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card.json index bf5b30bba8..ac7bb334c4 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_solar_radiation_card", "name": "Horizontal solar radiation card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_solar_radiation_card_system_widget_image.png", "description": "Displays the latest solar radiation telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card_with_background.json index 341003da53..5f339e6f15 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_solar_radiation_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_solar_radiation_card_with_background", "name": "Horizontal solar radiation card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_solar_radiation_card_with_background_system_widget_image.png", "description": "Displays the latest solar radiation telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card.json b/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card.json index 7869a8d0a9..8705ca3e42 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_sulfur_dioxide_so2_card", "name": "Horizontal sulfur dioxide (SO2) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/SO2-value-card-horizontal.svg", "description": "Produced by volcanoes and industrial processes, particularly the burning of coal and oil.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card_with_background.json index 197f62850c..e2660b613c 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_sulfur_dioxide__so2__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_sulfur_dioxide_so2_card_with_background", "name": "Horizontal sulfur dioxide (SO2) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/SO2-horizontal-value-card-with-background.svg", "description": "Produced by volcanoes and industrial processes, particularly the burning of coal and oil.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_temperature_card.json b/application/src/main/data/json/system/widget_types/horizontal_temperature_card.json index 8685ab39d6..c23564b3c4 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_temperature_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_temperature_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_temperature_card", "name": "Horizontal temperature card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_temperature_card_system_widget_image.png", "description": "Displays the latest temperature telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_temperature_card_with_background.json index 795daa25de..8d7b2a4919 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_temperature_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_temperature_card_with_background", "name": "Horizontal temperature card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_temperature_card_with_background_system_widget_image.png", "description": "Displays the latest temperature telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_uv_index_card.json b/application/src/main/data/json/system/widget_types/horizontal_uv_index_card.json index e25029b492..9f30ae466c 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_uv_index_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_uv_index_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_uv_index_card", "name": "Horizontal UV Index card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_uv_index_card_system_widget_image.png", "description": "Displays the latest UV index telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_uv_index_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_uv_index_card_with_background.json index 12cb3b8dca..905db88d3e 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_uv_index_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_uv_index_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_uv_index_card_with_background", "name": "Horizontal UV Index card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_uv_index_card_with_background_system_widget_image.png", "description": "Displays the latest UV index telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_vibration_card.json b/application/src/main/data/json/system/widget_types/horizontal_vibration_card.json index b43b513437..dc338970f8 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_vibration_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_vibration_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_vibration_card", "name": "Horizontal vibration card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_vibration_card_system_widget_image.png", "description": "Displays the latest vibration telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_vibration_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_vibration_card_with_background.json index eafcdbea5b..219b867f95 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_vibration_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_vibration_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_vibration_card_with_background", "name": "Horizontal vibration card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_vibration_card_with_background_system_widget_image.png", "description": "Displays the latest vibration telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_visibility_card.json b/application/src/main/data/json/system/widget_types/horizontal_visibility_card.json index 3f82e76027..77ae44ab0e 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_visibility_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_visibility_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_visibility_card", "name": "Horizontal visibility card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_visibility_card_system_widget_image.png", "description": "Displays the latest visibility telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_visibility_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_visibility_card_with_background.json index df818339b2..e7303c9a7b 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_visibility_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_visibility_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_visibility_card_with_background", "name": "Horizontal visibility card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_visibility_card_with_background_system_widget_image.png", "description": "Displays the latest visibility telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card.json b/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card.json index 8adb9110e5..bed74c67dc 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_volatile_organic_compounds_card", "name": "Horizontal volatile organic compounds card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_volatile_organic_compounds_card_system_widget_image.png", "description": "Displays the latest volatile organic compounds (VOCs) telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card_with_background.json index d864241952..a75a1a57c6 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_volatile_organic_compounds_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_volatile_organic_compounds_card_with_background", "name": "Horizontal volatile organic compounds card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_volatile_organic_compounds_card_with_background_system_widget_image.png", "description": "Displays the latest volatile organic compounds (VOCs) telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card.json b/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card.json index 257374763f..51491edb4a 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card.json +++ b/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_wind_speed_card", "name": "Horizontal wind speed card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_wind_speed_card_system_widget_image.png", "description": "Displays the latest wind speed telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card_with_background.json b/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card_with_background.json index d228e88c17..fd9373e66e 100644 --- a/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/horizontal_wind_speed_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "horizontal_wind_speed_card_with_background", "name": "Horizontal wind speed card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/horizontal_wind_speed_card_with_background_system_widget_image.png", "description": "Displays the latest wind speed telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/humidity_card.json b/application/src/main/data/json/system/widget_types/humidity_card.json index 9d4f1cc177..682513cf1a 100644 --- a/application/src/main/data/json/system/widget_types/humidity_card.json +++ b/application/src/main/data/json/system/widget_types/humidity_card.json @@ -1,7 +1,7 @@ { "fqn": "humidity_card", "name": "Humidity card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/humidity_card_system_widget_image.png", "description": "Displays the latest humidity telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/humidity_card_with_background.json b/application/src/main/data/json/system/widget_types/humidity_card_with_background.json index 4dd3d87fcc..75d1da1ba0 100644 --- a/application/src/main/data/json/system/widget_types/humidity_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/humidity_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "humidity_card_with_background", "name": "Humidity card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/humidity_card_with_background_system_widget_image.png", "description": "Displays the latest humidity telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/humidity_chart_card.json b/application/src/main/data/json/system/widget_types/humidity_chart_card.json index dad80f9797..2bbfa4c92b 100644 --- a/application/src/main/data/json/system/widget_types/humidity_chart_card.json +++ b/application/src/main/data/json/system/widget_types/humidity_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "humidity_chart_card", "name": "Humidity chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/humidity_chart_card_system_widget_image.png", "description": "Displays a humidity data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/humidity_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/humidity_chart_card_with_background.json index be13ef3285..a625836f1c 100644 --- a/application/src/main/data/json/system/widget_types/humidity_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/humidity_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "humidity_chart_card_with_background", "name": "Humidity chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/humidity_chart_card_with_background_system_widget_image.png", "description": "Displays a humidity data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/humidity_progress_bar.json b/application/src/main/data/json/system/widget_types/humidity_progress_bar.json index 9ba12ab343..782c430ac2 100644 --- a/application/src/main/data/json/system/widget_types/humidity_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/humidity_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "humidity_progress_bar", "name": "Humidity progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/humidity_progress_bar_system_widget_image.png", "description": "Displays humidity reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/humidity_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/humidity_progress_bar_with_background.json index 818012bd4c..f0afe6e268 100644 --- a/application/src/main/data/json/system/widget_types/humidity_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/humidity_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "humidity_progress_bar_with_background", "name": "Humidity progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/humidity_progress_bar_with_background_system_widget_image.png", "description": "Displays humidity reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/illuminance_card.json b/application/src/main/data/json/system/widget_types/illuminance_card.json index b3cb88702c..9805fb7a56 100644 --- a/application/src/main/data/json/system/widget_types/illuminance_card.json +++ b/application/src/main/data/json/system/widget_types/illuminance_card.json @@ -1,7 +1,7 @@ { "fqn": "illuminance_card", "name": "Illuminance card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/illuminance_card_system_widget_image.png", "description": "Displays the latest illuminance telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/illuminance_card_with_background.json b/application/src/main/data/json/system/widget_types/illuminance_card_with_background.json index 3242efe285..5127e13d9f 100644 --- a/application/src/main/data/json/system/widget_types/illuminance_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/illuminance_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "illuminance_card_with_background", "name": "Illuminance card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/illuminance_card_with_background_system_widget_image.png", "description": "Displays the latest illuminance telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/illuminance_chart_card.json b/application/src/main/data/json/system/widget_types/illuminance_chart_card.json index f664cc0d27..68d4da8c02 100644 --- a/application/src/main/data/json/system/widget_types/illuminance_chart_card.json +++ b/application/src/main/data/json/system/widget_types/illuminance_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "illuminance_chart_card", "name": "Illuminance chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/illuminance_chart_card_system_widget_image.png", "description": "Displays a illuminance data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/illuminance_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/illuminance_chart_card_with_background.json index 7849cad5e5..1e3e0a6545 100644 --- a/application/src/main/data/json/system/widget_types/illuminance_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/illuminance_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "illuminance_chart_card_with_background", "name": "Illuminance chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/illuminance_chart_card_with_background_system_widget_image.png", "description": "Displays a illuminance data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/illuminance_progress_bar.json b/application/src/main/data/json/system/widget_types/illuminance_progress_bar.json index 3743fd8eb7..c4677da9ca 100644 --- a/application/src/main/data/json/system/widget_types/illuminance_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/illuminance_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "illuminance_progress_bar", "name": "Illuminance progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/illuminance_progress_bar_system_widget_image.png", "description": "Displays illuminance reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/illuminance_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/illuminance_progress_bar_with_background.json index 90011a21ef..5a4d14c766 100644 --- a/application/src/main/data/json/system/widget_types/illuminance_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/illuminance_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "illuminance_progress_bar_with_background", "name": "Illuminance progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/illuminance_progress_bar_with_background_system_widget_image.png", "description": "Displays illuminance reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card.json b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card.json index 5e5b722af2..37cc70ea20 100644 --- a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card.json +++ b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card.json @@ -1,7 +1,7 @@ { "fqn": "individual_allergy_index_card", "name": "Individual allergy index (IAI) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/IAI-value-card.svg", "description": "Indicates the concentration of airborne allergens, including pollen and mold spores, which can trigger allergic reactions in sensitive individuals.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card_with_background.json b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card_with_background.json index 1e9df7e26b..5bc1705bc0 100644 --- a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "individual_allergy_index_card_with_background", "name": "Individual allergy index (IAI) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/IAI-value-card-with-background.svg", "description": "Indicates the concentration of airborne allergens, including pollen and mold spores, which can trigger allergic reactions in sensitive individuals.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card.json b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card.json index 13d72efffe..1face8a5e2 100644 --- a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card.json +++ b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "individual_allergy_index_iai_chart_card", "name": "Individual allergy index (IAI) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/IAI-value-chart-card.svg", "description": "Indicates the concentration of airborne allergens, including pollen and mold spores, which can trigger allergic reactions in sensitive individuals by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card_with_background.json index 2fcbe2bed8..39aa442b03 100644 --- a/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "individual_allergy_index_iai_chart_card_with_background", "name": "Individual allergy index (IAI) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/IAI-value-chart-card-with-background.svg", "description": "Indicates the concentration of airborne allergens, including pollen and mold spores, which can trigger allergic reactions in sensitive individuals by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_co2_card.json b/application/src/main/data/json/system/widget_types/indoor_co2_card.json index 21a4ca75cb..8e52cc602b 100644 --- a/application/src/main/data/json/system/widget_types/indoor_co2_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_co2_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_co2_card", "name": "Indoor CO2 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_co2_card_system_widget_image.png", "description": "Displays the latest indoor CO2 level telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_co2_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_co2_card_with_background.json index c624a97f49..8b75a81c8d 100644 --- a/application/src/main/data/json/system/widget_types/indoor_co2_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_co2_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_co2_card_with_background", "name": "Indoor CO2 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_co2_card_with_background_system_widget_image.png", "description": "Displays the latest indoor CO2 level telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_co2_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_co2_chart_card.json index ba1df65908..f4776e2601 100644 --- a/application/src/main/data/json/system/widget_types/indoor_co2_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_co2_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_co2_chart_card", "name": "Indoor CO2 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_co2_chart_card_system_widget_image.png", "description": "Displays a indoor CO2 level data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_co2_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_co2_chart_card_with_background.json index c4abd4a0b6..a4adcfbe14 100644 --- a/application/src/main/data/json/system/widget_types/indoor_co2_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_co2_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_co2_chart_card_with_background", "name": "Indoor CO2 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_co2_chart_card_with_background_system_widget_image.png", "description": "Displays a indoor CO2 level data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card.json index 0707b87752..ae484c3d18 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_co2_card", "name": "Indoor horizontal CO2 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_co2_card_system_widget_image.png", "description": "Displays the latest indoor CO2 level telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card_with_background.json index 66a3cfa70b..f0deb740fd 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_co2_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_co2_card_with_background", "name": "Indoor horizontal CO2 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_co2_card_with_background_system_widget_image.png", "description": "Displays the latest indoor CO2 level telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card.json index 3e76013cfa..d49a7e5422 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_humidity_card", "name": "Indoor horizontal humidity card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_humidity_card_system_widget_image.png", "description": "Displays the latest indoor humidity telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card_with_background.json index c5e7071561..153b1368e3 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_humidity_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_humidity_card_with_background", "name": "Indoor horizontal humidity card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_humidity_card_with_background_system_widget_image.png", "description": "Displays the latest indoor humidity telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card.json index 7810e9003e..a786b79236 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_illuminance_card", "name": "Indoor horizontal illuminance card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_illuminance_card_system_widget_image.png", "description": "Displays the latest indoor illuminance telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card_with_background.json index e2c832cb52..895dd62caf 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_illuminance_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_illuminance_card_with_background", "name": "Indoor horizontal illuminance card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_illuminance_card_with_background_system_widget_image.png", "description": "Displays the latest indoor illuminance telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card.json index 2aead4005e..3152cf323b 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_pm10_card", "name": "Indoor horizontal PM10 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_pm10_card_system_widget_image.png", "description": "Displays the latest indoor fine and coarse particulate matter (PM10) telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card_with_background.json index b8bb16de8e..f7bb81a443 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm10_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_pm10_card_with_background", "name": "Indoor horizontal PM10 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_pm10_card_with_background_system_widget_image.png", "description": "Displays the latest indoor fine and coarse particulate matter (PM10) telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card.json index 19fbff1e3b..9156d6c095 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_pm2_5_card", "name": "Indoor horizontal PM2.5 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_pm2_5_card_system_widget_image.png", "description": "Displays the latest indoor fine particulate matter (PM2.5) telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card_with_background.json index 8af6ab208f..26e6d0f91f 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_pm2_5_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_pm2_5_card_with_background", "name": "Indoor horizontal PM2.5 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_pm2_5_card_with_background_system_widget_image.png", "description": "Displays the latest indoor fine particulate matter (PM2.5) telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card.json index 2a63e7432c..caf9e461a5 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_temperature_card", "name": "Indoor horizontal temperature card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_temperature_card_system_widget_image.png", "description": "Displays the latest indoor temperature telemetry in a scalable horizontal layout.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card_with_background.json index eb3d2c0319..df0d57befb 100644 --- a/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_horizontal_temperature_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_horizontal_temperature_card_with_background", "name": "Indoor horizontal temperature card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_horizontal_temperature_card_with_background_system_widget_image.png", "description": "Displays the latest indoor temperature telemetry in a scalable horizontal layout with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_humidity_card.json b/application/src/main/data/json/system/widget_types/indoor_humidity_card.json index 9e489f5d80..8d7ce72ac3 100644 --- a/application/src/main/data/json/system/widget_types/indoor_humidity_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_humidity_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_humidity_card", "name": "Indoor humidity card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_humidity_card_system_widget_image.png", "description": "Displays the latest indoor humidity telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_humidity_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_humidity_card_with_background.json index 24a8689d70..75c9fcab3c 100644 --- a/application/src/main/data/json/system/widget_types/indoor_humidity_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_humidity_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_humidity_card_with_background", "name": "Indoor humidity card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_humidity_card_with_background_system_widget_image.png", "description": "Displays the latest indoor humidity telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_humidity_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_humidity_chart_card.json index 267cc65a5f..b3f0909bc0 100644 --- a/application/src/main/data/json/system/widget_types/indoor_humidity_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_humidity_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_humidity_chart_card", "name": "Indoor humidity chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_humidity_chart_card_system_widget_image.png", "description": "Displays a indoor humidity data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_humidity_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_humidity_chart_card_with_background.json index 82c9bbeaf6..2517b1b1d0 100644 --- a/application/src/main/data/json/system/widget_types/indoor_humidity_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_humidity_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_humidity_chart_card_with_background", "name": "Indoor humidity chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_humidity_chart_card_with_background_system_widget_image.png", "description": "Displays a indoor humidity data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar.json b/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar.json index 2b7df3ea28..07488e8086 100644 --- a/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "indoor_humidity_progress_bar", "name": "Indoor humidity progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_humidity_progress_bar_system_widget_image.png", "description": "Displays indoor humidity reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar_with_background.json index 612fc9a0ce..622ffa7054 100644 --- a/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_humidity_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_humidity_progress_bar_with_background", "name": "Indoor humidity progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_humidity_progress_bar_with_background_system_widget_image.png", "description": "Displays indoor humidity reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_illuminance_card.json b/application/src/main/data/json/system/widget_types/indoor_illuminance_card.json index d6627cf344..19e6014e3d 100644 --- a/application/src/main/data/json/system/widget_types/indoor_illuminance_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_illuminance_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_illuminance_card", "name": "Indoor illuminance card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_illuminance_card_system_widget_image.png", "description": "Displays the latest indoor illuminance telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_illuminance_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_illuminance_card_with_background.json index 0e0acf7698..4b939d4860 100644 --- a/application/src/main/data/json/system/widget_types/indoor_illuminance_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_illuminance_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_illuminance_card_with_background", "name": "Indoor illuminance card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_illuminance_card_with_background_system_widget_image.png", "description": "Displays the latest indoor illuminance telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_illuminance_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_illuminance_chart_card.json index 9ea21c112d..bad4adb97d 100644 --- a/application/src/main/data/json/system/widget_types/indoor_illuminance_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_illuminance_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_illuminance_chart_card", "name": "Indoor illuminance chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_illuminance_chart_card_system_widget_image.png", "description": "Displays a indoor illuminance data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_illuminance_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_illuminance_chart_card_with_background.json index 6badbfbfa0..5df2e86c84 100644 --- a/application/src/main/data/json/system/widget_types/indoor_illuminance_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_illuminance_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_illuminance_chart_card_with_background", "name": "Indoor illuminance chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_illuminance_chart_card_with_background_system_widget_image.png", "description": "Displays a indoor illuminance data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar.json b/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar.json index 5067c90a8d..631a5338af 100644 --- a/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "indoor_illuminance_progress_bar", "name": "Indoor illuminance progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_illuminance_progress_bar_system_widget_image.png", "description": "Displays indoor illuminance reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar_with_background.json index bd150ccfaf..16be23ac0d 100644 --- a/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_illuminance_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_illuminance_progress_bar_with_background", "name": "Indoor illuminance progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_illuminance_progress_bar_with_background_system_widget_image.png", "description": "Displays indoor illuminance reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_pm10_card.json b/application/src/main/data/json/system/widget_types/indoor_pm10_card.json index 8e28ea4d3c..522d2003f0 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm10_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm10_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_pm10_card", "name": "Indoor PM10 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_pm10_card_system_widget_image.png", "description": "Displays the latest indoor fine and coarse particulate matter (PM10) telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_pm10_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_pm10_card_with_background.json index 88fb98770d..4782c400c9 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm10_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm10_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_pm10_card_with_background", "name": "Indoor PM10 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_pm10_card_with_background_system_widget_image.png", "description": "Displays the latest indoor fine and coarse particulate matter (PM10) telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_pm10_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_pm10_chart_card.json index b04b7063cf..2f88f05151 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm10_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm10_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_pm10_chart_card", "name": "Indoor PM10 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_pm10_chart_card_system_widget_image.png", "description": "Displays a indoor fine and coarse particulate matter (PM10) data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_pm10_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_pm10_chart_card_with_background.json index 9eb69e6a26..d29a9d09d4 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm10_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm10_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_pm10_chart_card_with_background", "name": "Indoor PM10 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_pm10_chart_card_with_background_system_widget_image.png", "description": "Displays a indoor fine and coarse particulate matter (PM10) data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_pm2_5_card.json b/application/src/main/data/json/system/widget_types/indoor_pm2_5_card.json index fcb7017d57..1dc0413bdb 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm2_5_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm2_5_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_pm2_5_card", "name": "Indoor PM2.5 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_pm2_5_card_system_widget_image.png", "description": "Displays the latest indoor fine particulate matter (PM2.5) telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_pm2_5_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_pm2_5_card_with_background.json index 8d7cdf1681..9278e28ceb 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm2_5_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm2_5_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_pm2_5_card_with_background", "name": "Indoor PM2.5 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_pm2_5_card_with_background_system_widget_image.png", "description": "Displays the latest indoor fine particulate matter (PM2.5) telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_pm2_5_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_pm2_5_chart_card.json index 82d4e3af9a..d2507c6942 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm2_5_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm2_5_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_pm2_5_chart_card", "name": "Indoor PM2.5 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_pm2_5_chart_card_system_widget_image.png", "description": "Displays a indoor fine particulate matter (PM2.5) data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_pm2_5_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_pm2_5_chart_card_with_background.json index 9ee11386e6..32b78f75a6 100644 --- a/application/src/main/data/json/system/widget_types/indoor_pm2_5_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_pm2_5_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_pm2_5_chart_card_with_background", "name": "Indoor PM2.5 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_pm2_5_chart_card_with_background_system_widget_image.png", "description": "Displays a indoor fine particulate matter (PM2.5) data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card.json index 728abc34fd..584f43c440 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_co2_chart_card", "name": "Indoor simple CO2 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_co2_chart_card_system_widget_image.png", "description": "Displays historical indoor CO2 level values as a simplified chart. Optionally may display the corresponding latest indoor CO2 level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card_with_background.json index 41146249d0..1dd7217c11 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_co2_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_co2_chart_card_with_background", "name": "Indoor simple CO2 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_co2_chart_card_with_background_system_widget_image.png", "description": "Displays historical indoor CO2 level values as a simplified chart with background. Optionally may display the corresponding latest indoor CO2 level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card.json index 8f9c801dd5..32d7f96a85 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_humidity_chart_card", "name": "Indoor simple humidity chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_humidity_chart_card_system_widget_image.png", "description": "Displays historical indoor humidity values as a simplified chart. Optionally may display the corresponding latest indoor humidity value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card_with_background.json index 7c881e9347..d0db23ef0a 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_humidity_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_humidity_chart_card_with_background", "name": "Indoor simple humidity chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_humidity_chart_card_with_background_system_widget_image.png", "description": "Displays historical indoor humidity values as a simplified chart with background. Optionally may display the corresponding latest indoor humidity value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card.json index 2eeca64aca..d747767887 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_illuminance_chart_card", "name": "Indoor simple illuminance chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_illuminance_chart_card_system_widget_image.png", "description": "Displays historical indoor illuminance values as a simplified chart. Optionally may display the corresponding latest indoor illuminance value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card_with_background.json index 7f47b543ac..c5659ed145 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_illuminance_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_illuminance_chart_card_with_background", "name": "Indoor simple illuminance chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_illuminance_chart_card_with_background_system_widget_image.png", "description": "Displays historical indoor illuminance values as a simplified chart with background. Optionally may display the corresponding latest indoor illuminance value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card.json index 85e703440c..54903bafda 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_pm10_chart_card", "name": "Indoor simple PM10 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_pm10_chart_card_system_widget_image.png", "description": "Displays historical indoor fine and coarse particulate matter (PM10) values as a simplified chart. Optionally may display the corresponding latest indoor PM10 value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card_with_background.json index 5006398499..42eb7bc00e 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_pm10_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_pm10_chart_card_with_background", "name": "Indoor simple PM10 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_pm10_chart_card_with_background_system_widget_image.png", "description": "Displays historical indoor fine and coarse particulate matter (PM10) values as a simplified chart with background. Optionally may display the corresponding latest indoor PM10 value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card.json index 35a6f30a81..90441293c0 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_pm2_5_chart_card", "name": "Indoor simple PM2.5 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_pm2_5_chart_card_system_widget_image.png", "description": "Displays historical indoor fine particulate matter (PM2.5) values as a simplified chart. Optionally may display the corresponding latest indoor PM2.5 value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card_with_background.json index 05f3b8591f..703135169d 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_pm2_5_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_pm2_5_chart_card_with_background", "name": "Indoor simple PM2.5 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_pm2_5_chart_card_with_background_system_widget_image.png", "description": "Displays historical indoor fine particulate matter (PM2.5) values as a simplified chart with background. Optionally may display the corresponding latest indoor PM2.5 value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card.json index e6101557f0..569aed6cbc 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_temperature_chart_card", "name": "Indoor simple temperature chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_temperature_chart_card_system_widget_image.png", "description": "Displays historical indoor temperature values as a simplified chart. Optionally may display the corresponding latest indoor temperature value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card_with_background.json index e9b50ba2b8..a1f0c3f7c0 100644 --- a/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_simple_temperature_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_simple_temperature_chart_card_with_background", "name": "Indoor simple temperature chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_simple_temperature_chart_card_with_background_system_widget_image.png", "description": "Displays historical indoor temperature values as a simplified chart with background. Optionally may display the corresponding latest indoor temperature value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_card.json b/application/src/main/data/json/system/widget_types/indoor_temperature_card.json index 1058baae12..90f60c60ba 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_card", "name": "Indoor temperature card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_card_system_widget_image.png", "description": "Displays the latest indoor temperature telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_temperature_card_with_background.json index 3deb1c409a..f26491f3ea 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_card_with_background", "name": "Indoor temperature card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_card_with_background_system_widget_image.png", "description": "Displays the latest indoor temperature telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_chart_card.json b/application/src/main/data/json/system/widget_types/indoor_temperature_chart_card.json index c24cb6e5eb..750842be0e 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_chart_card.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_chart_card", "name": "Indoor temperature chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_chart_card_system_widget_image.png", "description": "Displays a indoor temperature data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/indoor_temperature_chart_card_with_background.json index 96e606d3b7..69d97ebbf3 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_chart_card_with_background", "name": "Indoor temperature chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_chart_card_with_background_system_widget_image.png", "description": "Displays a indoor temperature data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_gauge.json b/application/src/main/data/json/system/widget_types/indoor_temperature_gauge.json index f0726e05f3..7c49c241ca 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_gauge.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_gauge.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_gauge", "name": "Indoor temperature gauge", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_gauge.svg", "description": "Indoor temperatue gauge.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar.json b/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar.json index 3c392c9d26..caf2dd1fdb 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_progress_bar", "name": "Indoor temperature progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_progress_bar_system_widget_image.png", "description": "Displays indoor temperature reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar_with_background.json index d6bb4a992e..e4dce7b2f7 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_progress_bar_with_background", "name": "Indoor temperature progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_progress_bar_with_background_system_widget_image.png", "description": "Displays indoor temperature reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_range_chart.json b/application/src/main/data/json/system/widget_types/indoor_temperature_range_chart.json index 4dcf322cb7..84cd1c9c7e 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_range_chart.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_range_chart.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_range_chart", "name": "Indoor temperature range chart", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_range_chart.svg", "description": "Displays changes to indoor temperature data over time visualized with color ranges.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/indoor_temperature_range_chart_with_background.json b/application/src/main/data/json/system/widget_types/indoor_temperature_range_chart_with_background.json index e8a0752aeb..07477b9c5e 100644 --- a/application/src/main/data/json/system/widget_types/indoor_temperature_range_chart_with_background.json +++ b/application/src/main/data/json/system/widget_types/indoor_temperature_range_chart_with_background.json @@ -1,7 +1,7 @@ { "fqn": "indoor_temperature_range_chart_with_background", "name": "Indoor temperature range chart with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/indoor_temperature_range_chart_with_background.svg", "description": "Displays changes to indoor temperature data over time visualized with color ranges and background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/iot_hub.json b/application/src/main/data/json/system/widget_types/iot_hub.json new file mode 100644 index 0000000000..b6cfb28f60 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/iot_hub.json @@ -0,0 +1,39 @@ +{ + "fqn": "home_page_widgets.iot_hub", + "name": "IoT Hub", + "deprecated": false, + "image": "tb-image;/api/images/system/iot_hub_system_widget_image.png", + "description": "Display featured solution templates and device library from IoT Hub.", + "descriptor": { + "type": "static", + "sizeX": 7.5, + "sizeY": 6.5, + "resources": [], + "templateHtml": "\n\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}", + "settingsDirective": "", + "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"IoT Hub\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}" + }, + "resources": [ + { + "link": "/api/images/system/iot_hub_system_widget_image.png", + "title": "\"IoT Hub\" system widget image", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "iot_hub_system_widget_image.png", + "publicResourceKey": "XqwXyzWV9EsIEzcq7967Xrru0fycYRlS", + "mediaType": "image/png", + "data": "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", + "public": true + } + ], + "scada": false, + "tags": [ + "iot hub", + "widgets", + "solution templates", + "device library", + "marketplace" + ] +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/leaf_wetness_card.json b/application/src/main/data/json/system/widget_types/leaf_wetness_card.json index 2e9efa3027..cfb920e2ef 100644 --- a/application/src/main/data/json/system/widget_types/leaf_wetness_card.json +++ b/application/src/main/data/json/system/widget_types/leaf_wetness_card.json @@ -1,7 +1,7 @@ { "fqn": "leaf_wetness_card", "name": "Leaf wetness card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/leaf_wetness_card_system_widget_image.png", "description": "Displays the latest leaf wetness telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/leaf_wetness_card_with_background.json b/application/src/main/data/json/system/widget_types/leaf_wetness_card_with_background.json index b7d3b0edec..135b8c3799 100644 --- a/application/src/main/data/json/system/widget_types/leaf_wetness_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/leaf_wetness_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "leaf_wetness_card_with_background", "name": "Leaf wetness card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/leaf_wetness_card_with_background_system_widget_image.png", "description": "Displays the latest leaf wetness telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/leaf_wetness_chart_card.json b/application/src/main/data/json/system/widget_types/leaf_wetness_chart_card.json index 235a468a74..a6efbe6cac 100644 --- a/application/src/main/data/json/system/widget_types/leaf_wetness_chart_card.json +++ b/application/src/main/data/json/system/widget_types/leaf_wetness_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "leaf_wetness_chart_card", "name": "Leaf wetness chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/leaf_wetness_chart_card_system_widget_image.png", "description": "Displays a leaf wetness data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/leaf_wetness_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/leaf_wetness_chart_card_with_background.json index 3c9f15b0b2..e62ad0cc5f 100644 --- a/application/src/main/data/json/system/widget_types/leaf_wetness_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/leaf_wetness_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "leaf_wetness_chart_card_with_background", "name": "Leaf wetness chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/leaf_wetness_chart_card_with_background_system_widget_image.png", "description": "Displays a leaf wetness data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar.json b/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar.json index 750d2846a7..ffa0a09742 100644 --- a/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "leaf_wetness_progress_bar", "name": "Leaf wetness progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/leaf_wetness_progress_bar_system_widget_image.png", "description": "Displays leaf wetness reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar_with_background.json index 1af8f7de7b..86275b2d86 100644 --- a/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/leaf_wetness_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "leaf_wetness_progress_bar_with_background", "name": "Leaf wetness progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/leaf_wetness_progress_bar_with_background_system_widget_image.png", "description": "Displays leaf wetness reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card.json b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card.json index c47e0977d2..621b7f9314 100644 --- a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card.json +++ b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card.json @@ -1,7 +1,7 @@ { "fqn": "nitrogen_dioxide_no2_card", "name": "Nitrogen dioxide (NO2) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/NO2-value-card.svg", "description": "A gas produced from vehicle emissions and the burning of fossil fuels.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card_with_background.json b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card_with_background.json index 37671527dd..17df2dd697 100644 --- a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "nitrogen_dioxide_no2_card_with_background", "name": "Nitrogen dioxide (NO2) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/NO2-value-card-with-background.svg", "description": "A gas produced from vehicle emissions and the burning of fossil fuels.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card.json b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card.json index 39def6e43b..b4089e045c 100644 --- a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card.json +++ b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "nitrogen_dioxide_no2_chart_card", "name": "Nitrogen dioxide (NO2) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/NO2-value-chart-card.svg", "description": "A gas produced from vehicle emissions and the burning of fossil fuels. Results displayed by combining the latest and aggregated values and optional simplified chart. ", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card_with_background.json index e48517f3bb..362ff731bb 100644 --- a/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "nitrogen_dioxide_no2_chart_card_with_background", "name": "Nitrogen dioxide (NO2) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/NO2-value-chart-card-with-background.svg", "description": "A gas produced from vehicle emissions and the burning of fossil fuels. Results displayed by combining the latest and aggregated values and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/noise_level_card.json b/application/src/main/data/json/system/widget_types/noise_level_card.json index 4a1da677ea..13bb9f2713 100644 --- a/application/src/main/data/json/system/widget_types/noise_level_card.json +++ b/application/src/main/data/json/system/widget_types/noise_level_card.json @@ -1,7 +1,7 @@ { "fqn": "noise_level_card", "name": "Noise level card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/noise_level_card_system_widget_image.png", "description": "Displays the latest noise level telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/noise_level_card_with_background.json b/application/src/main/data/json/system/widget_types/noise_level_card_with_background.json index 44475dc2ce..ee77e6f857 100644 --- a/application/src/main/data/json/system/widget_types/noise_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/noise_level_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "noise_level_card_with_background", "name": "Noise level card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/noise_level_card_with_background_system_widget_image.png", "description": "Displays the latest noise level telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/noise_level_chart_card.json b/application/src/main/data/json/system/widget_types/noise_level_chart_card.json index 00da830302..bfd06d8760 100644 --- a/application/src/main/data/json/system/widget_types/noise_level_chart_card.json +++ b/application/src/main/data/json/system/widget_types/noise_level_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "noise_level_chart_card", "name": "Noise level chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/noise_level_chart_card_system_widget_image.png", "description": "Displays a noise level data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/noise_level_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/noise_level_chart_card_with_background.json index 032e34c994..dd617a1764 100644 --- a/application/src/main/data/json/system/widget_types/noise_level_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/noise_level_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "noise_level_chart_card_with_background", "name": "Noise level chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/noise_level_chart_card_with_background_system_widget_image.png", "description": "Displays a noise level data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/ozone__o3__card.json b/application/src/main/data/json/system/widget_types/ozone__o3__card.json index f8a49252c5..419e934407 100644 --- a/application/src/main/data/json/system/widget_types/ozone__o3__card.json +++ b/application/src/main/data/json/system/widget_types/ozone__o3__card.json @@ -1,7 +1,7 @@ { "fqn": "ozone_o3_card", "name": "Ozone (O3) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ozone-value-card.svg", "description": "A beneficial layer in the upper atmosphere, but harmful when present near ground level. Results mainly from vehicle exhaust and industrial emissions.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/ozone__o3__card_with_background.json b/application/src/main/data/json/system/widget_types/ozone__o3__card_with_background.json index 4af0a7b712..134ad947d5 100644 --- a/application/src/main/data/json/system/widget_types/ozone__o3__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/ozone__o3__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "ozone_o3_card_with_background", "name": "Ozone (O3) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ozone-value-card-with-background.svg", "description": "A beneficial layer in the upper atmosphere, but harmful when present near ground level. Results mainly from vehicle exhaust and industrial emissions.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/ozone__o3__chart_card.json b/application/src/main/data/json/system/widget_types/ozone__o3__chart_card.json index c4a3e47767..cac726d689 100644 --- a/application/src/main/data/json/system/widget_types/ozone__o3__chart_card.json +++ b/application/src/main/data/json/system/widget_types/ozone__o3__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "ozone_o3_chart_card", "name": "Ozone (O3) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ozone-value-chart-card.svg", "description": "A beneficial layer in the upper atmosphere, but harmful when present near ground level. Results displays mainly from vehicle exhaust and industrial emissions by combining the latest and aggregated values and optional simplified chart. ", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/ozone__o3__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/ozone__o3__chart_card_with_background.json index 6037c4454c..e7b46fa7ee 100644 --- a/application/src/main/data/json/system/widget_types/ozone__o3__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/ozone__o3__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "ozone_o3_chart_card_with_background", "name": "Ozone (O3) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ozone-value-chart-card-with-background.svg", "description": "A beneficial layer in the upper atmosphere, but harmful when present near ground level. Results displays mainly from vehicle exhaust and industrial emissions by combining the latest and aggregated values and optional simplified chart. ", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pm10_card.json b/application/src/main/data/json/system/widget_types/pm10_card.json index 05556452a2..4f1dfb035f 100644 --- a/application/src/main/data/json/system/widget_types/pm10_card.json +++ b/application/src/main/data/json/system/widget_types/pm10_card.json @@ -1,7 +1,7 @@ { "fqn": "pm10_card", "name": "PM10 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pm10_card_system_widget_image.png", "description": "Displays the latest fine and coarse particulate matter (PM10) telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pm10_card_with_background.json b/application/src/main/data/json/system/widget_types/pm10_card_with_background.json index 0af84a9fc3..d8af7818ad 100644 --- a/application/src/main/data/json/system/widget_types/pm10_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pm10_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pm10_card_with_background", "name": "PM10 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pm10_card_with_background_system_widget_image.png", "description": "Displays the latest fine and coarse particulate matter (PM10) telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pm10_chart_card.json b/application/src/main/data/json/system/widget_types/pm10_chart_card.json index 5014d27f7c..e4b30f0a91 100644 --- a/application/src/main/data/json/system/widget_types/pm10_chart_card.json +++ b/application/src/main/data/json/system/widget_types/pm10_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "pm10_chart_card", "name": "PM10 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pm10_chart_card_system_widget_image.png", "description": "Displays a fine and coarse particulate matter (PM10) data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pm10_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/pm10_chart_card_with_background.json index 8f2ba1d14e..23ae1523e1 100644 --- a/application/src/main/data/json/system/widget_types/pm10_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pm10_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pm10_chart_card_with_background", "name": "PM10 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pm10_chart_card_with_background_system_widget_image.png", "description": "Displays a fine and coarse particulate matter (PM10) data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pm2_5_card.json b/application/src/main/data/json/system/widget_types/pm2_5_card.json index b9be83e86d..7b086e8778 100644 --- a/application/src/main/data/json/system/widget_types/pm2_5_card.json +++ b/application/src/main/data/json/system/widget_types/pm2_5_card.json @@ -1,7 +1,7 @@ { "fqn": "pm2_5_card", "name": "PM2.5 card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pm2_5_card_system_widget_image.png", "description": "Displays the latest fine particulate matter (PM2.5) telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pm2_5_card_with_background.json b/application/src/main/data/json/system/widget_types/pm2_5_card_with_background.json index 41ef55b0ac..f09c7dec14 100644 --- a/application/src/main/data/json/system/widget_types/pm2_5_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pm2_5_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pm2_5_card_with_background", "name": "PM2.5 card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pm2_5_card_with_background_system_widget_image.png", "description": "Displays the latest fine particulate matter (PM2.5) telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pm2_5_chart_card.json b/application/src/main/data/json/system/widget_types/pm2_5_chart_card.json index eff5191922..b23e40011e 100644 --- a/application/src/main/data/json/system/widget_types/pm2_5_chart_card.json +++ b/application/src/main/data/json/system/widget_types/pm2_5_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "pm2_5_chart_card", "name": "PM2.5 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pm2_5_chart_card_system_widget_image.png", "description": "Displays a fine particulate matter (PM2.5) data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pm2_5_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/pm2_5_chart_card_with_background.json index cdfac469eb..3a6303d581 100644 --- a/application/src/main/data/json/system/widget_types/pm2_5_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pm2_5_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pm2_5_chart_card_with_background", "name": "PM2.5 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pm2_5_chart_card_with_background_system_widget_image.png", "description": "Displays a fine particulate matter (PM2.5) data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/power_consumption_card.json b/application/src/main/data/json/system/widget_types/power_consumption_card.json index 1b568491c6..469fed0dad 100644 --- a/application/src/main/data/json/system/widget_types/power_consumption_card.json +++ b/application/src/main/data/json/system/widget_types/power_consumption_card.json @@ -1,7 +1,7 @@ { "fqn": "power_consumption_card", "name": "Power consumption card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/power_consumption_card.svg", "description": "Displays the latest power consumption telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/power_consumption_card_with_background.json b/application/src/main/data/json/system/widget_types/power_consumption_card_with_background.json index 4f9fb7fbb4..2ee31333bb 100644 --- a/application/src/main/data/json/system/widget_types/power_consumption_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/power_consumption_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "power_consumption_card_with_background", "name": "Power consumption card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/power_consumption_card_with_background.svg", "description": "Displays the latest power consumption telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/power_consumption_chart_card.json b/application/src/main/data/json/system/widget_types/power_consumption_chart_card.json index 1009371fe6..13106bdf7e 100644 --- a/application/src/main/data/json/system/widget_types/power_consumption_chart_card.json +++ b/application/src/main/data/json/system/widget_types/power_consumption_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "power_consumption_chart_card", "name": "Power consumption chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/power_consumption_chart_card.svg", "description": "Displays power consumption data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/power_consumption_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/power_consumption_chart_card_with_background.json index c006f600fc..5df45ff182 100644 --- a/application/src/main/data/json/system/widget_types/power_consumption_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/power_consumption_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "power_consumption_chart_card_with_background", "name": "Power consumption chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/power_consumption_chart_card_with_background.svg", "description": "Displays power consumption data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/power_consumption_range_chart.json b/application/src/main/data/json/system/widget_types/power_consumption_range_chart.json index bc295c570f..878f0c5083 100644 --- a/application/src/main/data/json/system/widget_types/power_consumption_range_chart.json +++ b/application/src/main/data/json/system/widget_types/power_consumption_range_chart.json @@ -1,7 +1,7 @@ { "fqn": "power_consumption_range_chart", "name": "Power consumption range chart", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/power_consumption_range_chart.svg", "description": "Displays changes to power consumption data over time visualized with color ranges.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/power_consumption_range_chart_with_background.json b/application/src/main/data/json/system/widget_types/power_consumption_range_chart_with_background.json index 3ebcbb96c9..347938d831 100644 --- a/application/src/main/data/json/system/widget_types/power_consumption_range_chart_with_background.json +++ b/application/src/main/data/json/system/widget_types/power_consumption_range_chart_with_background.json @@ -1,7 +1,7 @@ { "fqn": "power_consumption_range_chart_with_background", "name": "Power consumption range chart with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/power_consumption_range_chart_with_background.svg", "description": "Displays changes to power consumption data over time visualized with color ranges and background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pressure_card.json b/application/src/main/data/json/system/widget_types/pressure_card.json index 49c4b6478f..be3e156bb9 100644 --- a/application/src/main/data/json/system/widget_types/pressure_card.json +++ b/application/src/main/data/json/system/widget_types/pressure_card.json @@ -1,7 +1,7 @@ { "fqn": "pressure_card", "name": "Pressure card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_card_system_widget_image.png", "description": "Displays the latest pressure telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pressure_card_with_background.json b/application/src/main/data/json/system/widget_types/pressure_card_with_background.json index bc35530963..058cfd4f4f 100644 --- a/application/src/main/data/json/system/widget_types/pressure_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pressure_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pressure_card_with_background", "name": "Pressure card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_card_with_background_system_widget_image.png", "description": "Displays the latest pressure telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pressure_chart_card.json b/application/src/main/data/json/system/widget_types/pressure_chart_card.json index e0ea0e9895..0ec733b6de 100644 --- a/application/src/main/data/json/system/widget_types/pressure_chart_card.json +++ b/application/src/main/data/json/system/widget_types/pressure_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "pressure_chart_card", "name": "Pressure chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_chart_card_system_widget_image.png", "description": "Displays a pressure data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pressure_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/pressure_chart_card_with_background.json index 58c5859931..d5aefcba58 100644 --- a/application/src/main/data/json/system/widget_types/pressure_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pressure_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pressure_chart_card_with_background", "name": "Pressure chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_chart_card_with_background_system_widget_image.png", "description": "Displays a pressure data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pressure_progress_bar.json b/application/src/main/data/json/system/widget_types/pressure_progress_bar.json index 12fd5b917e..43ffbe3a6e 100644 --- a/application/src/main/data/json/system/widget_types/pressure_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/pressure_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "pressure_progress_bar", "name": "Pressure progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_progress_bar_system_widget_image.png", "description": "Displays pressure reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pressure_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/pressure_progress_bar_with_background.json index 57fa40f2d5..3e9ba6f9ed 100644 --- a/application/src/main/data/json/system/widget_types/pressure_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/pressure_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pressure_progress_bar_with_background", "name": "Pressure progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/pressure_progress_bar_with_background_system_widget_image.png", "description": "Displays pressure reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pump_vibration_card.json b/application/src/main/data/json/system/widget_types/pump_vibration_card.json index 688c8b03df..a6794a91f4 100644 --- a/application/src/main/data/json/system/widget_types/pump_vibration_card.json +++ b/application/src/main/data/json/system/widget_types/pump_vibration_card.json @@ -1,7 +1,7 @@ { "fqn": "pump_vibration_card", "name": "Vibration card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_card.svg", "description": "Displays the latest vibration telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pump_vibration_card_with_background.json b/application/src/main/data/json/system/widget_types/pump_vibration_card_with_background.json index 05114909c1..d2d31ff352 100644 --- a/application/src/main/data/json/system/widget_types/pump_vibration_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pump_vibration_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pump_vibration_card_with_background", "name": "Vibration card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_card_with_background.svg", "description": "Displays the latest vibration telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pump_vibration_chart_card.json b/application/src/main/data/json/system/widget_types/pump_vibration_chart_card.json index acbe3524e0..c887086f5a 100644 --- a/application/src/main/data/json/system/widget_types/pump_vibration_chart_card.json +++ b/application/src/main/data/json/system/widget_types/pump_vibration_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "pump_vibration_chart_card", "name": "Vibration chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_chart_card.svg", "description": "Displays vibration data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pump_vibration_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/pump_vibration_chart_card_with_background.json index 46c249f200..78813ec690 100644 --- a/application/src/main/data/json/system/widget_types/pump_vibration_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/pump_vibration_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pump_vibration_chart_card_with_background", "name": "Vibration chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_chart_card_with_background.svg", "description": "Displays vibrarion data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pump_vibration_range_chart.json b/application/src/main/data/json/system/widget_types/pump_vibration_range_chart.json index 561130c14e..735484b0eb 100644 --- a/application/src/main/data/json/system/widget_types/pump_vibration_range_chart.json +++ b/application/src/main/data/json/system/widget_types/pump_vibration_range_chart.json @@ -1,7 +1,7 @@ { "fqn": "pump_vibration_range_chart", "name": "Vibration range chart", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_range_chart.svg", "description": "Displays changes to vibration data over time visualized with color ranges.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/pump_vibration_range_chart_with_background.json b/application/src/main/data/json/system/widget_types/pump_vibration_range_chart_with_background.json index 27ba578048..86667b8b5a 100644 --- a/application/src/main/data/json/system/widget_types/pump_vibration_range_chart_with_background.json +++ b/application/src/main/data/json/system/widget_types/pump_vibration_range_chart_with_background.json @@ -1,7 +1,7 @@ { "fqn": "pump_vibration_range_chart_with_background", "name": "Vibration range chart with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_range_chart_with_background.svg", "description": "Displays changes to vibration data over time visualized with color ranges and background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/radon_level_card.json b/application/src/main/data/json/system/widget_types/radon_level_card.json index ad789adea4..25aded46b1 100644 --- a/application/src/main/data/json/system/widget_types/radon_level_card.json +++ b/application/src/main/data/json/system/widget_types/radon_level_card.json @@ -1,7 +1,7 @@ { "fqn": "radon_level_card", "name": "Radon level card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/radon_level_card_system_widget_image.png", "description": "Displays the latest radon level telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/radon_level_card_with_background.json b/application/src/main/data/json/system/widget_types/radon_level_card_with_background.json index 4560fdb5eb..2b5a9aa30b 100644 --- a/application/src/main/data/json/system/widget_types/radon_level_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/radon_level_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "radon_level_card_with_background", "name": "Radon level card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/radon_level_card_with_background_system_widget_image.png", "description": "Displays the latest radon level telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/radon_level_chart_card.json b/application/src/main/data/json/system/widget_types/radon_level_chart_card.json index d76f5d2d9f..7a3a2a4c74 100644 --- a/application/src/main/data/json/system/widget_types/radon_level_chart_card.json +++ b/application/src/main/data/json/system/widget_types/radon_level_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "radon_level_chart_card", "name": "Radon level chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/radon_level_chart_card_system_widget_image.png", "description": "Displays a radon level data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/radon_level_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/radon_level_chart_card_with_background.json index aded058cd6..807cf86888 100644 --- a/application/src/main/data/json/system/widget_types/radon_level_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/radon_level_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "radon_level_chart_card_with_background", "name": "Radon level chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/radon_level_chart_card_with_background_system_widget_image.png", "description": "Displays a radon level data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rainfall_card.json b/application/src/main/data/json/system/widget_types/rainfall_card.json index ca4eb9ad21..8fa9885538 100644 --- a/application/src/main/data/json/system/widget_types/rainfall_card.json +++ b/application/src/main/data/json/system/widget_types/rainfall_card.json @@ -1,7 +1,7 @@ { "fqn": "rainfall_card", "name": "Rainfall card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rainfall_card_system_widget_image.png", "description": "Displays the latest rainfall telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rainfall_card_with_background.json b/application/src/main/data/json/system/widget_types/rainfall_card_with_background.json index 6a31586919..3424f50d65 100644 --- a/application/src/main/data/json/system/widget_types/rainfall_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/rainfall_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "rainfall_card_with_background", "name": "Rainfall card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rainfall_card_with_background_system_widget_image.png", "description": "Displays the latest rainfall telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rainfall_chart_card.json b/application/src/main/data/json/system/widget_types/rainfall_chart_card.json index b5bd51ad00..9aa17966d6 100644 --- a/application/src/main/data/json/system/widget_types/rainfall_chart_card.json +++ b/application/src/main/data/json/system/widget_types/rainfall_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "rainfall_chart_card", "name": "Rainfall chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rainfall_chart_card_system_widget_image.png", "description": "Displays a rainfall data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rainfall_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/rainfall_chart_card_with_background.json index f25ba166f7..d70028dff0 100644 --- a/application/src/main/data/json/system/widget_types/rainfall_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/rainfall_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "rainfall_chart_card_with_background", "name": "Rainfall chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rainfall_chart_card_with_background_system_widget_image.png", "description": "Displays a rainfall data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_card.json b/application/src/main/data/json/system/widget_types/rotational_speed_card.json index c4597d3a84..a8126c25af 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_card.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_card.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_card", "name": "Rotational speed card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_card.svg", "description": "Displays the latest rotational speed telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_card_with_background.json b/application/src/main/data/json/system/widget_types/rotational_speed_card_with_background.json index e51158d89d..24e3b89ebd 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_card_with_background", "name": "Rotational speed card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_card_with_background.svg", "description": "Displays the latest rotational speed telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_chart_card.json b/application/src/main/data/json/system/widget_types/rotational_speed_chart_card.json index d5c48138f2..13781dcc3b 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_chart_card.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_chart_card", "name": "Rotational speed chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_chart_card.svg", "description": "Displays rotational speed data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/rotational_speed_chart_card_with_background.json index 1e2a0c2397..b99138d7eb 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_chart_card_with_background", "name": "Rotational speed chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_chart_card_with_background.svg", "description": "Displays rotational speed data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_gauge.json b/application/src/main/data/json/system/widget_types/rotational_speed_gauge.json index cf8f0189d5..f70221c5f7 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_gauge.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_gauge.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_gauge", "name": "Rotational speed gauge", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_gauge.svg", "description": "Rotational speed gauge.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar.json b/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar.json index 96c8889e11..03ad1181d4 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_progress_bar", "name": "Rotational speed progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_progress_bar.svg", "description": "Displays rotational speed reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar_with_background.json index b9e5834f01..23ebc4bae8 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_progress_bar_with_background", "name": "Rotational speed progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_progress_bar_with_background.svg", "description": "Displays rotational speed reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_range_chart.json b/application/src/main/data/json/system/widget_types/rotational_speed_range_chart.json index bcbeb8c9a8..02b7279d0b 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_range_chart.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_range_chart.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_range_chart", "name": "Rotational speed range chart", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_range_chart.svg", "description": "Displays changes to rotational speed data over time visualized with color ranges.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/rotational_speed_range_chart_with_background.json b/application/src/main/data/json/system/widget_types/rotational_speed_range_chart_with_background.json index 537314bd0d..c4db34c71f 100644 --- a/application/src/main/data/json/system/widget_types/rotational_speed_range_chart_with_background.json +++ b/application/src/main/data/json/system/widget_types/rotational_speed_range_chart_with_background.json @@ -1,7 +1,7 @@ { "fqn": "rotational_speed_range_chart_with_background", "name": "Rotational speed range chart with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/rotational_speed_range_chart_with_background.svg", "description": "Displays changes to rotational speed data over time visualized with color ranges and background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card.json b/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card.json index 130fea3cc9..34a4590b20 100644 --- a/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_air_quality_chart_card", "name": "Simple air quality index chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_air_quality_index_chart_card_system_widget_image.png", "description": "Displays historical air quality index values as a simplified chart. Optionally may display the corresponding latest air quality index value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card_with_background.json index e20970e9a8..9437f8940d 100644 --- a/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_air_quality_index_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_air_quality_chart_card_with_background", "name": "Simple air quality index chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_air_quality_index_chart_card_with_background_system_widget_image.png", "description": "Displays historical air quality index values as a simplified chart with background. Optionally may display the corresponding latest air quality index value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card.json b/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card.json index 8174b77794..825ad767bd 100644 --- a/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_carbon_monoxide_co_chart_card", "name": "Simple carbon monoxide (CO) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/CO-simple-value-and-chart-card.svg", "description": "A colorless and odorless gas resulting from incomplete combustion, particularly in motor vehicles. Results displayed by combining the latest and aggregated values and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card_with_background.json index 25a13d1989..deedb58791 100644 --- a/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_carbon_monoxide__co__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_carbon_monoxide_co_chart_card_with_background", "name": "Simple carbon monoxide (CO) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/CO-simple-chart-value-card-with-background.svg", "description": "A colorless and odorless gas resulting from incomplete combustion, particularly in motor vehicles. Results displayed by combining the latest and aggregated values and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_co2_chart_card.json b/application/src/main/data/json/system/widget_types/simple_co2_chart_card.json index 4a50cd60a7..81973ca148 100644 --- a/application/src/main/data/json/system/widget_types/simple_co2_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_co2_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_co2_chart_card", "name": "Simple CO2 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_co2_chart_card_system_widget_image.png", "description": "Displays historical CO2 level values as a simplified chart. Optionally may display the corresponding latest CO2 level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_co2_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_co2_chart_card_with_background.json index e3ff71d64c..ef7b72e7c6 100644 --- a/application/src/main/data/json/system/widget_types/simple_co2_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_co2_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_co2_chart_card_with_background", "name": "Simple CO2 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_co2_chart_card_with_background_system_widget_image.png", "description": "Displays historical CO2 level values as a simplified chart with background. Optionally may display the corresponding latest CO2 level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card.json b/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card.json index 80079e409f..88e66dd41a 100644 --- a/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_efficiency_chart_card", "name": "Simple efficiency chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_efficiency_chart_card.svg", "description": "Displays historical efficiency values as a simplified chart. Optionally may display the corresponding efficiency value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card_with_background.json index f1b9f961fe..3df69c526d 100644 --- a/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_efficiency_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_efficiency_chart_card_with_background", "name": "Simple efficiency chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_efficiency_chart_card_with_background.svg", "description": "Displays historical efficiency values as a simplified chart with background. Optionally may display the corresponding efficiency value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card.json b/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card.json index a808a4c930..964a229f3c 100644 --- a/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_flooding_level_chart_card", "name": "Simple flooding level chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_flooding_level_chart_card_system_widget_image.png", "description": "Displays historical flooding level values as a simplified chart. Optionally may display the corresponding latest flooding level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card_with_background.json index 07976f61a4..28b8f84cc8 100644 --- a/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_flooding_level_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_flooding_level_chart_card_with_background", "name": "Simple flooding level chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_flooding_level_chart_card_with_background_system_widget_image.png", "description": "Displays historical flooding level values as a simplified chart with background. Optionally may display the corresponding latest flooding level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card.json b/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card.json index 8061c87659..5c66680bca 100644 --- a/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_flow_rate_chart_card", "name": "Simple flow rate chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_flow_rate_chart_card_(1).svg", "description": "Displays historical flow rate values as a simplified chart. Optionally may display the corresponding latest flow rate value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card_with_background.json index 436d45b9c4..518d363711 100644 --- a/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_flow_rate_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_flow_rate_chart_card_with_background", "name": "Simple flow rate chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_flow_rate_chart_card_with_background.svg", "description": "Displays historical flow rate values as a simplified chart with background. Optionally may display the corresponding latest flow rate value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card.json b/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card.json index 647c5fd971..93c39ae139 100644 --- a/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_fluid_pressure_chart_card", "name": "Simple pressure chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_pressure_chart_card_(1).svg", "description": "Displays historical fluid pressure values as a simplified chart. Optionally may display the corresponding latest fluid pressure value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card_with_background.json index b6b8fdd8f6..5060b1050d 100644 --- a/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_fluid_pressure_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_fluid_pressure_chart_card_with_background", "name": "Simple pressure chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_pressure_chart_card_with_background.svg", "description": "Displays historical fluid pressure values as a simplified chart with background. Optionally may display the corresponding latest fluid pressure value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card.json b/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card.json index 28a8c2e5d9..126cfe4497 100644 --- a/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_ground_temperature_chart_card", "name": "Simple ground temperature chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_ground_temperature_chart_card_system_widget_image.png", "description": "Displays historical ground temperature values as a simplified chart. Optionally may display the corresponding latest ground temperature value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card_with_background.json index dbe53fb714..f132f604bb 100644 --- a/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_ground_temperature_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_ground_temperature_chart_card_with_background", "name": "Simple ground temperature chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_ground_temperature_chart_card_with_background_system_widget_image.png", "description": "Displays historical ground temperature values as a simplified chart with background. Optionally may display the corresponding latest ground temperature value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_humidity_chart_card.json b/application/src/main/data/json/system/widget_types/simple_humidity_chart_card.json index 9532878d13..685299e85b 100644 --- a/application/src/main/data/json/system/widget_types/simple_humidity_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_humidity_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_humidity_chart_card", "name": "Simple humidity chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_humidity_chart_card_system_widget_image.png", "description": "Displays historical humidity values as a simplified chart. Optionally may display the corresponding latest humidity value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_humidity_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_humidity_chart_card_with_background.json index 21853dafd1..9c5bf14428 100644 --- a/application/src/main/data/json/system/widget_types/simple_humidity_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_humidity_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_humidity_chart_card_with_background", "name": "Simple humidity chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_humidity_chart_card_with_background_system_widget_image.png", "description": "Displays historical humidity values as a simplified chart with background. Optionally may display the corresponding latest humidity value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card.json b/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card.json index 0c525ea708..e68e3a7231 100644 --- a/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_illuminance_chart_card", "name": "Simple illuminance chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_illuminance_chart_card_system_widget_image.png", "description": "Displays historical illuminance values as a simplified chart. Optionally may display the corresponding latest illuminance value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card_with_background.json index a8f7092e15..7a26529897 100644 --- a/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_illuminance_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_illuminance_chart_card_with_background", "name": "Simple illuminance chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_illuminance_chart_card_with_background_system_widget_image.png", "description": "Displays historical illuminance values as a simplified chart with background. Optionally may display the corresponding latest illuminance value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card.json b/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card.json index 58f093daff..22ed0b342b 100644 --- a/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_individual_allergy_index_iai_chart_card", "name": "Simple individual allergy index (IAI) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple-IAI-value-and-chart-card.svg", "description": "Displays the concentration of airborne allergens, including pollen and mold spores, which can trigger allergic reactions in sensitive individuals as a simplified chart. Optionally may display the corresponding latest concentration of allergens value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card_with_background.json index a729bdc5b0..28c07b4525 100644 --- a/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_individual_allergy_index__iai__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_individual_allergy_index_iai_chart_card_with_background2", "name": "Simple individual allergy index (IAI) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/IAI-simple-chart-value-card-with-background.svg", "description": "Displays the concentration of airborne allergens, including pollen and mold spores, which can trigger allergic reactions in sensitive individuals as a simplified chart. Optionally may display the corresponding latest concentration of allergens value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card.json b/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card.json index dd8a5c4913..930f5c8b1a 100644 --- a/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_leaf_wetness_chart_card", "name": "Simple leaf wetness chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_leaf_wetness_chart_card_system_widget_image.png", "description": "Displays historical leaf wetness values as a simplified chart. Optionally may display the corresponding latest leaf wetness value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card_with_background.json index 81e32813f3..0244f1ec8a 100644 --- a/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_leaf_wetness_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_leaf_wetness_chart_card_with_background", "name": "Simple leaf wetness chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_leaf_wetness_chart_card_with_background_system_widget_image.png", "description": "Displays historical leaf wetness values as a simplified chart with background. Optionally may display the corresponding latest leaf wetness value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card.json b/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card.json index 2caf26bcf0..4b1d4be5d5 100644 --- a/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_nitrogen_dioxide_no2_chart_card", "name": "Simple nitrogen dioxide (NO2) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple-NO2-value-and-chart-card.svg", "description": "A gas produced from vehicle emissions and the burning of fossil fuels. Results displayed by combining the latest and aggregated values and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card_with_background.json index 72f4425456..a09850f594 100644 --- a/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_nitrogen_dioxide__no2__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_nitrogen_dioxide_no2_chart_card_with_background", "name": "Simple nitrogen dioxide (NO2) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/NO2-simple-chart-value-card-with-background.svg", "description": "A gas produced from vehicle emissions and the burning of fossil fuels. Results displayed by combining the latest and aggregated values and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card.json b/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card.json index 6fd4708209..3230c6fa6c 100644 --- a/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_noise_level_chart_card", "name": "Simple noise level chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_noise_level_chart_card_system_widget_image.png", "description": "Displays historical noise level values as a simplified chart. Optionally may display the corresponding latest noise level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card_with_background.json index 41a360c8a6..f1c2b17e32 100644 --- a/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_noise_level_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_noise_level_chart_card_with_background", "name": "Simple noise level chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_noise_level_chart_card_with_background_system_widget_image.png", "description": "Displays historical noise level values as a simplified chart with background. Optionally may display the corresponding latest noise level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card.json b/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card.json index acb65d35f6..ef9307fec8 100644 --- a/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_ozone_o3_chart_card", "name": "Simple ozone (O3) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple-ozone-value-and-chart-card.svg", "description": "A beneficial layer in the upper atmosphere, but harmful when present near ground level. Results displays mainly from vehicle exhaust and industrial emissions by combining the latest and aggregated values and optional simplified chart. ", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card_with_background.json index 129a0e2056..bee0ce2389 100644 --- a/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_ozone__o3__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_ozone_o3_chart_card_with_background", "name": "Simple ozone (O3) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/ozone-chart-value-card-with-background.svg", "description": "A beneficial layer in the upper atmosphere, but harmful when present near ground level. Results displays mainly from vehicle exhaust and industrial emissions by combining the latest and aggregated values and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_pm10_chart_card.json b/application/src/main/data/json/system/widget_types/simple_pm10_chart_card.json index a0e450dd2f..f588b60dc0 100644 --- a/application/src/main/data/json/system/widget_types/simple_pm10_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_pm10_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_pm10_chart_card", "name": "Simple PM10 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_pm10_chart_card_system_widget_image.png", "description": "Displays historical fine and coarse particulate matter (PM10) values as a simplified chart. Optionally may display the corresponding latest PM10 value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_pm10_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_pm10_chart_card_with_background.json index d10f51d454..a364937fa5 100644 --- a/application/src/main/data/json/system/widget_types/simple_pm10_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_pm10_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_pm10_chart_card_with_background", "name": "Simple PM10 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_pm10_chart_card_with_background_system_widget_image.png", "description": "Displays historical fine and coarse particulate matter (PM10) values as a simplified chart with background. Optionally may display the corresponding latest PM10 value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card.json b/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card.json index 5a9110d70d..7fc0320f0e 100644 --- a/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_pm2_5_chart_card", "name": "Simple PM2.5 chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_pm2_5_chart_card_system_widget_image.png", "description": "Displays historical fine particulate matter (PM2.5) values as a simplified chart. Optionally may display the corresponding latest PM2.5 value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card_with_background.json index b8f90f9b44..035dab65e1 100644 --- a/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_pm2_5_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_pm2_5_chart_card_with_background", "name": "Simple PM2.5 chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_pm2_5_chart_card_with_background_system_widget_image.png", "description": "Displays historical fine particulate matter (PM2.5) values as a simplified chart with background. Optionally may display the corresponding latest PM2.5 value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card.json b/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card.json index 766a1e1956..99760ff107 100644 --- a/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_power_consumption_chart_card", "name": "Simple power consumption chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_power_consumption_chart_card_(1).svg", "description": "Displays historical power consumption values as a simplified chart. Optionally may display the corresponding latest power consumption value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card_with_background.json index 58c5fc8beb..05fff58ef1 100644 --- a/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_power_consumption_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_power_consumption_chart_card_with_background", "name": "Simple power consumption chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_power_consumption_chart_card_with_background.svg", "description": "Displays historical power consumption values as a simplified chart with background. Optionally may display the corresponding latest power consumption value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_pressure_chart_card.json b/application/src/main/data/json/system/widget_types/simple_pressure_chart_card.json index 62178bbf2a..e745c8dc71 100644 --- a/application/src/main/data/json/system/widget_types/simple_pressure_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_pressure_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_pressure_chart_card", "name": "Simple pressure chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_pressure_chart_card_system_widget_image.png", "description": "Displays historical pressure values as a simplified chart. Optionally may display the corresponding latest pressure value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_pressure_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_pressure_chart_card_with_background.json index fde44a0366..bed416d420 100644 --- a/application/src/main/data/json/system/widget_types/simple_pressure_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_pressure_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_pressure_chart_card_with_background", "name": "Simple pressure chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_pressure_chart_card_with_background_system_widget_image.png", "description": "Displays historical pressure values as a simplified chart with background. Optionally may display the corresponding latest pressure value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card.json b/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card.json index 92673ec6d2..4298274160 100644 --- a/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_pump_vibration_chart_card", "name": "Simple vibration chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_vibration_chart_card_(1).svg", "description": "Displays historical vibration values as a simplified chart. Optionally may display the corresponding latest vibration value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card_with_background.json index b077b31919..61a2d29263 100644 --- a/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_pump_vibration_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_pump_vibration_chart_card_with_background", "name": "Simple vibration chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_vibration_chart_card_with_background.svg", "description": "Displays historical vibration values as a simplified chart with background. Optionally may display the corresponding latest vibration value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card.json b/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card.json index 72b811f2d1..53bba9c0c2 100644 --- a/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_radon_level_chart_card", "name": "Simple radon level chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_radon_level_chart_card_system_widget_image.png", "description": "Displays historical radon level values as a simplified chart. Optionally may display the corresponding latest radon level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card_with_background.json index 0ee5a825ae..2c8ae7a248 100644 --- a/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_radon_level_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_radon_level_chart_card_with_background", "name": "Simple radon level chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_radon_level_chart_card_with_background_system_widget_image.png", "description": "Displays historical radon level values as a simplified chart with background. Optionally may display the corresponding latest radon level value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card.json b/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card.json index 8cb70f6b3d..5d75aece01 100644 --- a/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_rainfall_chart_card", "name": "Simple rainfall chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_rainfall_chart_card_system_widget_image.png", "description": "Displays historical rainfall values as a simplified chart. Optionally may display the corresponding latest rainfall value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card_with_background.json index 92927e78de..3261057a3c 100644 --- a/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_rainfall_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_rainfall_chart_card_with_background", "name": "Simple rainfall chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_rainfall_chart_card_with_background_system_widget_image.png", "description": "Displays historical rainfall values as a simplified chart with background. Optionally may display the corresponding latest rainfall value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card.json b/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card.json index 5050fc8184..464dabe3fc 100644 --- a/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_rotational_speed_chart_card", "name": "Simple rotational speed chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_rotational_speed_chart_card.svg", "description": "Displays historical rotational speed values as a simplified chart. Optionally may display the corresponding latest rotational speed value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card_with_background.json index c1433bb2b4..4cda322a10 100644 --- a/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_rotational_speed_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_rotational_speed_chart_card_with_background", "name": "Simple rotational speed chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_rotational_speed_chart_card_with_background.svg", "description": "Displays historical rotational speed values as a simplified chart with background. Optionally may display the corresponding latest rotational speed value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card.json b/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card.json index f4509d1669..df263cd3cc 100644 --- a/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_snow_depth_chart_card", "name": "Simple snow depth chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_snow_depth_chart_card_system_widget_image.png", "description": "Displays historical snow depth values as a simplified chart. Optionally may display the corresponding latest snow depth value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card_with_background.json index 706f03e477..c30d695f75 100644 --- a/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_snow_depth_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_snow_depth_chart_card_with_background", "name": "Simple snow depth chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_snow_depth_chart_card_with_background_system_widget_image.png", "description": "Displays historical snow depth values as a simplified chart with background. Optionally may display the corresponding latest snow depth value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card.json b/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card.json index 8627fc8430..c59ea6a9f1 100644 --- a/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_soil_moisture_chart_card", "name": "Simple soil moisture chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_soil_moisture_chart_card_system_widget_image.png", "description": "Displays historical soil moisture values as a simplified chart. Optionally may display the corresponding latest soil moisture value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card_with_background.json index 096e88941f..908680d833 100644 --- a/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_soil_moisture_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_soil_moisture_chart_card_with_background", "name": "Simple soil moisture chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_soil_moisture_chart_card_with_background_system_widget_image.png", "description": "Displays historical soil moisture values as a simplified chart with background. Optionally may display the corresponding latest soil moisture value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card.json b/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card.json index c009adb989..d7879a21e9 100644 --- a/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_solar_radiation_chart_card", "name": "Simple solar radiation chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_solar_radiation_chart_card_system_widget_image.png", "description": "Displays historical solar radiation values as a simplified chart. Optionally may display the corresponding latest solar radiation value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card_with_background.json index 39b76d6c41..4b72ad991b 100644 --- a/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_solar_radiation_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_solar_radiation_chart_card_with_background", "name": "Simple solar radiation chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_solar_radiation_chart_card_with_background_system_widget_image.png", "description": "Displays historical solar radiation values as a simplified chart with background. Optionally may display the corresponding latest solar radiation value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card.json b/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card.json index 193e1f9042..fc267ddecc 100644 --- a/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_sulfur_dioxide_so2_chart_card", "name": "Simple sulfur dioxide (SO2) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/SO2-simple-value-and-chart-card.svg", "description": "Produced by volcanoes and industrial processes, particularly the burning of coal and oil. Results displayed by combining the latest and aggregated values and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card_with_background.json index f13af9970c..3f61b59885 100644 --- a/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_sulfur_dioxide__so2__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_sulfur_dioxide_so2_chart_card_with_background", "name": "Simple sulfur dioxide (SO2) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/SO2-simple-chart-value-card-with-background.svg", "description": "Produced by volcanoes and industrial processes, particularly the burning of coal and oil. Results displayed by combining the latest and aggregated values and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_temperature_chart_card.json b/application/src/main/data/json/system/widget_types/simple_temperature_chart_card.json index 5faeaace2a..b170cf1b81 100644 --- a/application/src/main/data/json/system/widget_types/simple_temperature_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_temperature_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_temperature_chart_card", "name": "Simple temperature chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_temperature_chart_card_system_widget_image.png", "description": "Displays historical temperature values as a simplified chart. Optionally may display the corresponding latest temperature value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_temperature_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_temperature_chart_card_with_background.json index c0d44315ce..1c6c891138 100644 --- a/application/src/main/data/json/system/widget_types/simple_temperature_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_temperature_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_temperature_chart_card_with_background", "name": "Simple temperature chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_temperature_chart_card_with_background_system_widget_image.png", "description": "Displays historical temperature values as a simplified chart with background. Optionally may display the corresponding latest temperature value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card.json b/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card.json index 25792079bc..3971e3b421 100644 --- a/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_uv_index_chart_card", "name": "Simple UV Index chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_uv_index_chart_card_system_widget_image.png", "description": "Displays historical UV index values as a simplified chart. Optionally may display the corresponding latest UV index value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card_with_background.json index a7173133ab..9ea933ec29 100644 --- a/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_uv_index_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_uv_index_chart_card_with_background", "name": "Simple UV Index chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_uv_index_chart_card_with_background_system_widget_image.png", "description": "Displays historical UV index values as a simplified chart with background. Optionally may display the corresponding latest UV index value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_vibration_chart_card.json b/application/src/main/data/json/system/widget_types/simple_vibration_chart_card.json index e1be525d80..05db628580 100644 --- a/application/src/main/data/json/system/widget_types/simple_vibration_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_vibration_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_vibration_chart_card", "name": "Simple vibration chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_vibration_chart_card_system_widget_image.png", "description": "Displays historical vibration values as a simplified chart. Optionally may display the corresponding latest vibration value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_vibration_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_vibration_chart_card_with_background.json index 367dece05a..80f841751a 100644 --- a/application/src/main/data/json/system/widget_types/simple_vibration_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_vibration_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_vibration_chart_card_with_background", "name": "Simple vibration chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_vibration_chart_card_with_background_system_widget_image.png", "description": "Displays historical vibration values as a simplified chart with background. Optionally may display the corresponding latest vibration value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_visibility_chart_card.json b/application/src/main/data/json/system/widget_types/simple_visibility_chart_card.json index d7bc882f73..c0e4fc46f3 100644 --- a/application/src/main/data/json/system/widget_types/simple_visibility_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_visibility_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_visibility_chart_card", "name": "Simple visibility chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_visibility_chart_card_system_widget_image.png", "description": "Displays historical visibility values as a simplified chart. Optionally may display the corresponding latest visibility value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_visibility_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_visibility_chart_card_with_background.json index c61471fa6d..9f8a285ec2 100644 --- a/application/src/main/data/json/system/widget_types/simple_visibility_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_visibility_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_visibility_chart_card_with_background", "name": "Simple visibility chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_visibility_chart_card_with_background_system_widget_image.png", "description": "Displays historical visibility values as a simplified chart with background. Optionally may display the corresponding latest visibility value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card.json b/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card.json index 3d60d3479a..0bdbaa8a4b 100644 --- a/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_volatile_organic_compounds_chart_card", "name": "Simple volatile organic compounds chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_volatile_organic_compounds_chart_card_system_widget_image.png", "description": "Displays historical volatile organic compounds (VOCs) values as a simplified chart. Optionally may display the corresponding latest VOCs value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card_with_background.json index 7adedd8c81..a1fb0e3530 100644 --- a/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_volatile_organic_compounds_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_volatile_organic_compounds_chart_card_with_background", "name": "Simple volatile organic compounds chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_volatile_organic_compounds_chart_card_with_background_system_widget_image.png", "description": "Displays historical volatile organic compounds (VOCs) values as a simplified chart with background. Optionally may display the corresponding latest VOCs value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card.json b/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card.json index ea60ba3edf..79137ce9f7 100644 --- a/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card.json +++ b/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "simple_wind_speed_chart_card", "name": "Simple wind speed chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_wind_speed_chart_card_system_widget_image.png", "description": "Displays historical wind speed values as a simplified chart. Optionally may display the corresponding latest wind speed value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card_with_background.json index d08cbc8a49..6ac9daff3a 100644 --- a/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/simple_wind_speed_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "simple_wind_speed_chart_card_with_background", "name": "Simple wind speed chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/simple_wind_speed_chart_card_with_background_system_widget_image.png", "description": "Displays historical wind speed values as a simplified chart with background. Optionally may display the corresponding latest wind speed value.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/snow_depth_card.json b/application/src/main/data/json/system/widget_types/snow_depth_card.json index 031db09beb..3d2e1ac919 100644 --- a/application/src/main/data/json/system/widget_types/snow_depth_card.json +++ b/application/src/main/data/json/system/widget_types/snow_depth_card.json @@ -1,7 +1,7 @@ { "fqn": "snow_depth_card", "name": "Snow depth card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/snow_depth_card_system_widget_image.png", "description": "Displays the latest snow depth telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/snow_depth_card_with_background.json b/application/src/main/data/json/system/widget_types/snow_depth_card_with_background.json index 9c294fbe70..1677146977 100644 --- a/application/src/main/data/json/system/widget_types/snow_depth_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/snow_depth_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "snow_depth_card_with_background", "name": "Snow depth card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/snow_depth_card_with_background_system_widget_image.png", "description": "Displays the latest snow depth telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/snow_depth_chart_card.json b/application/src/main/data/json/system/widget_types/snow_depth_chart_card.json index decfe00951..403579060a 100644 --- a/application/src/main/data/json/system/widget_types/snow_depth_chart_card.json +++ b/application/src/main/data/json/system/widget_types/snow_depth_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "snow_depth_chart_card", "name": "Snow depth chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/snow_depth_chart_card_system_widget_image.png", "description": "Displays a snow depth data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/snow_depth_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/snow_depth_chart_card_with_background.json index f0b2951731..e96d5a26d8 100644 --- a/application/src/main/data/json/system/widget_types/snow_depth_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/snow_depth_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "snow_depth_chart_card_with_background", "name": "Snow depth chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/snow_depth_chart_card_with_background_system_widget_image.png", "description": "Displays a snow depth data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/soil_moisture_card.json b/application/src/main/data/json/system/widget_types/soil_moisture_card.json index 65e0e6311a..204fb4ea3e 100644 --- a/application/src/main/data/json/system/widget_types/soil_moisture_card.json +++ b/application/src/main/data/json/system/widget_types/soil_moisture_card.json @@ -1,7 +1,7 @@ { "fqn": "soil_moisture_card", "name": "Soil moisture card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/soil_moisture_card_system_widget_image.png", "description": "Displays the latest soil moisture telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/soil_moisture_card_with_background.json b/application/src/main/data/json/system/widget_types/soil_moisture_card_with_background.json index 38864fb686..fbf179d06a 100644 --- a/application/src/main/data/json/system/widget_types/soil_moisture_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/soil_moisture_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "soil_moisture_card_with_background", "name": "Soil moisture card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/soil_moisture_card_with_background_system_widget_image.png", "description": "Displays the latest soil moisture telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/soil_moisture_chart_card.json b/application/src/main/data/json/system/widget_types/soil_moisture_chart_card.json index 986100613a..c0bc989c21 100644 --- a/application/src/main/data/json/system/widget_types/soil_moisture_chart_card.json +++ b/application/src/main/data/json/system/widget_types/soil_moisture_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "soil_moisture_chart_card", "name": "Soil moisture chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/soil_moisture_chart_card_system_widget_image.png", "description": "Displays a soil moisture data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/soil_moisture_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/soil_moisture_chart_card_with_background.json index acce7054b3..554e236cfc 100644 --- a/application/src/main/data/json/system/widget_types/soil_moisture_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/soil_moisture_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "soil_moisture_chart_card_with_background", "name": "Soil moisture chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/soil_moisture_chart_card_with_background_system_widget_image.png", "description": "Displays a soil moisture data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar.json b/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar.json index 0acba2ec65..7d8e6d50f0 100644 --- a/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar.json +++ b/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar.json @@ -1,7 +1,7 @@ { "fqn": "soil_moisture_progress_bar", "name": "Soil moisture progress bar", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/soil_moisture_progress_bar_system_widget_image.png", "description": "Displays soil moisture reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar_with_background.json b/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar_with_background.json index d3e63ede6a..b5e17c283c 100644 --- a/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar_with_background.json +++ b/application/src/main/data/json/system/widget_types/soil_moisture_progress_bar_with_background.json @@ -1,7 +1,7 @@ { "fqn": "soil_moisture_progress_bar_with_background", "name": "Soil moisture progress bar with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/soil_moisture_progress_bar_with_background_system_widget_image.png", "description": "Displays soil moisture reading as a horizontal progress bar with background. Allows to configure value range, bar colors, and other settings.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/solar_radiation_card.json b/application/src/main/data/json/system/widget_types/solar_radiation_card.json index 43e5cfc398..4e986c6815 100644 --- a/application/src/main/data/json/system/widget_types/solar_radiation_card.json +++ b/application/src/main/data/json/system/widget_types/solar_radiation_card.json @@ -1,7 +1,7 @@ { "fqn": "solar_radiation_card", "name": "Solar radiation card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/solar_radiation_card_system_widget_image.png", "description": "Displays the latest solar radiation telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/solar_radiation_card_with_background.json b/application/src/main/data/json/system/widget_types/solar_radiation_card_with_background.json index c49effbfec..7521070285 100644 --- a/application/src/main/data/json/system/widget_types/solar_radiation_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/solar_radiation_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "solar_radiation_card_with_background", "name": "Solar radiation card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/solar_radiation_card_with_background_system_widget_image.png", "description": "Displays the latest solar radiation telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/solar_radiation_chart_card.json b/application/src/main/data/json/system/widget_types/solar_radiation_chart_card.json index a96b0f8c89..9d4c0c3003 100644 --- a/application/src/main/data/json/system/widget_types/solar_radiation_chart_card.json +++ b/application/src/main/data/json/system/widget_types/solar_radiation_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "solar_radiation_chart_card", "name": "Solar radiation chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/solar_radiation_chart_card_system_widget_image.png", "description": "Displays a solar radiation data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/solar_radiation_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/solar_radiation_chart_card_with_background.json index a0dac85a70..1fb1360ddb 100644 --- a/application/src/main/data/json/system/widget_types/solar_radiation_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/solar_radiation_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "solar_radiation_chart_card_with_background", "name": "Solar radiation chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/solar_radiation_chart_card_with_background_system_widget_image.png", "description": "Displays a solar radiation data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card.json b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card.json index 166a0842c5..6622eca9f5 100644 --- a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card.json +++ b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card.json @@ -1,7 +1,7 @@ { "fqn": "sulfur_dioxide_so2_card", "name": "Sulfur dioxide (SO2) card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/SO2-value-card.svg", "description": "Produced by volcanoes and industrial processes, particularly the burning of coal and oil.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card_with_background.json b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card_with_background.json index 7a0c911bf4..b8f19427d8 100644 --- a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card_with_background.json +++ b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "sulfur_dioxide_so2_card_with_background", "name": "Sulfur dioxide (SO2) card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/SO2-value-card-with-background.svg", "description": "Produced by volcanoes and industrial processes, particularly the burning of coal and oil.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card.json b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card.json index 597c454242..d47d15c23b 100644 --- a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card.json +++ b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card.json @@ -1,7 +1,7 @@ { "fqn": "sulfur_dioxide_so2_chart_card", "name": "Sulfur dioxide (SO2) chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/SO2-value-and-chart-card.svg", "description": "Produced by volcanoes and industrial processes, particularly the burning of coal and oil. Results displayed by combining the latest and aggregated values and optional simplified chart. ", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card_with_background.json b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card_with_background.json index 3934f87dc0..7ba6e5ba62 100644 --- a/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "sulfur_dioxide_so2_chart_card_with_background", "name": "Sulfur dioxide (SO2) chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/SO2-value-chart-card-with-background.svg", "description": "Produced by volcanoes and industrial processes, particularly the burning of coal and oil. Results displayed by combining the latest and aggregated values and optional simplified chart. ", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/temperature_card.json b/application/src/main/data/json/system/widget_types/temperature_card.json index 93219788e6..3581603f0c 100644 --- a/application/src/main/data/json/system/widget_types/temperature_card.json +++ b/application/src/main/data/json/system/widget_types/temperature_card.json @@ -1,7 +1,7 @@ { "fqn": "temperature_card", "name": "Temperature card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/temperature_card_system_widget_image.png", "description": "Displays the latest temperature telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/temperature_card_with_background.json b/application/src/main/data/json/system/widget_types/temperature_card_with_background.json index fee17a4a87..ededb1b9e4 100644 --- a/application/src/main/data/json/system/widget_types/temperature_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/temperature_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "temperature_card_with_background", "name": "Temperature card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/temperature_card_with_background_system_widget_image.png", "description": "Displays the latest temperature telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/temperature_chart_card.json b/application/src/main/data/json/system/widget_types/temperature_chart_card.json index 689de25c3f..49495ec4ec 100644 --- a/application/src/main/data/json/system/widget_types/temperature_chart_card.json +++ b/application/src/main/data/json/system/widget_types/temperature_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "temperature_chart_card", "name": "Temperature chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/temperature_chart_card_system_widget_image.png", "description": "Displays a temperature data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/temperature_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/temperature_chart_card_with_background.json index 582d22c0f6..eedef9011e 100644 --- a/application/src/main/data/json/system/widget_types/temperature_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/temperature_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "temperature_chart_card_with_background", "name": "Temperature chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/temperature_chart_card_with_background_system_widget_image.png", "description": "Displays a temperature data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/temperature_gauge.json b/application/src/main/data/json/system/widget_types/temperature_gauge.json index a89477ccbf..1fd8faed44 100644 --- a/application/src/main/data/json/system/widget_types/temperature_gauge.json +++ b/application/src/main/data/json/system/widget_types/temperature_gauge.json @@ -1,7 +1,7 @@ { "fqn": "temperature_gauge", "name": "Temperature gauge", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/Temperature_gauge.svg", "description": "Temperature gauge.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/temperature_range_chart.json b/application/src/main/data/json/system/widget_types/temperature_range_chart.json index 2810381c51..1d34f69066 100644 --- a/application/src/main/data/json/system/widget_types/temperature_range_chart.json +++ b/application/src/main/data/json/system/widget_types/temperature_range_chart.json @@ -1,7 +1,7 @@ { "fqn": "temperature_range_chart", "name": "Temperature range chart", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/temperature_range_chart.svg", "description": "Displays changes to temperature data over time visualized with color ranges.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/temperature_range_chart_with_background.json b/application/src/main/data/json/system/widget_types/temperature_range_chart_with_background.json index e4e49fe12c..96f913a1b0 100644 --- a/application/src/main/data/json/system/widget_types/temperature_range_chart_with_background.json +++ b/application/src/main/data/json/system/widget_types/temperature_range_chart_with_background.json @@ -1,7 +1,7 @@ { "fqn": "temperature_range_chart_with_background", "name": "Temperature range chart with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/temperature_range_chart_with_background.svg", "description": "Displays changes to temperature data over time visualized with color ranges and background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/uv_index_card.json b/application/src/main/data/json/system/widget_types/uv_index_card.json index e0f9f3ea22..05eb3ba7f4 100644 --- a/application/src/main/data/json/system/widget_types/uv_index_card.json +++ b/application/src/main/data/json/system/widget_types/uv_index_card.json @@ -1,7 +1,7 @@ { "fqn": "uv_index_card", "name": "UV Index card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/uv_index_card_system_widget_image.png", "description": "Displays the latest UV index telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/uv_index_card_with_background.json b/application/src/main/data/json/system/widget_types/uv_index_card_with_background.json index fc94104bb7..6c2d7f7810 100644 --- a/application/src/main/data/json/system/widget_types/uv_index_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/uv_index_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "uv_index_card_with_background", "name": "UV Index card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/uv_index_card_with_background_system_widget_image.png", "description": "Displays the latest UV index telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/uv_index_chart_card.json b/application/src/main/data/json/system/widget_types/uv_index_chart_card.json index 393f7359a4..9a00d0a809 100644 --- a/application/src/main/data/json/system/widget_types/uv_index_chart_card.json +++ b/application/src/main/data/json/system/widget_types/uv_index_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "uv_index_chart_card", "name": "UV Index chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/uv_index_chart_card_system_widget_image.png", "description": "Displays a UV index data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/uv_index_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/uv_index_chart_card_with_background.json index 78a7b899be..9c19f15e50 100644 --- a/application/src/main/data/json/system/widget_types/uv_index_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/uv_index_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "uv_index_chart_card_with_background", "name": "UV Index chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/uv_index_chart_card_with_background_system_widget_image.png", "description": "Displays a UV index data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/vibration_card.json b/application/src/main/data/json/system/widget_types/vibration_card.json index fd2c4be2a3..07cfcb15ff 100644 --- a/application/src/main/data/json/system/widget_types/vibration_card.json +++ b/application/src/main/data/json/system/widget_types/vibration_card.json @@ -1,7 +1,7 @@ { "fqn": "vibration_card", "name": "Vibration card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_card_system_widget_image.png", "description": "Displays the latest vibration telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/vibration_card_with_background.json b/application/src/main/data/json/system/widget_types/vibration_card_with_background.json index 3c77ef51b3..bf2311ce27 100644 --- a/application/src/main/data/json/system/widget_types/vibration_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/vibration_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "vibration_card_with_background", "name": "Vibration card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_card_with_background_system_widget_image.png", "description": "Displays the latest vibration telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/vibration_chart_card.json b/application/src/main/data/json/system/widget_types/vibration_chart_card.json index c7dcf7a6ed..75d31e3954 100644 --- a/application/src/main/data/json/system/widget_types/vibration_chart_card.json +++ b/application/src/main/data/json/system/widget_types/vibration_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "vibration_chart_card", "name": "Vibration chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_chart_card_system_widget_image.png", "description": "Displays a vibration data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/vibration_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/vibration_chart_card_with_background.json index 443d1b7a33..bdf5418806 100644 --- a/application/src/main/data/json/system/widget_types/vibration_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/vibration_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "vibration_chart_card_with_background", "name": "Vibration chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/vibration_chart_card_with_background_system_widget_image.png", "description": "Displays a vibration data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/visibility_card.json b/application/src/main/data/json/system/widget_types/visibility_card.json index c30455251a..a30267d02c 100644 --- a/application/src/main/data/json/system/widget_types/visibility_card.json +++ b/application/src/main/data/json/system/widget_types/visibility_card.json @@ -1,7 +1,7 @@ { "fqn": "visibility_card", "name": "Visibility card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/visibility_card_system_widget_image.png", "description": "Displays the latest visibility telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/visibility_card_with_background.json b/application/src/main/data/json/system/widget_types/visibility_card_with_background.json index 4103cf657c..1a08be0d06 100644 --- a/application/src/main/data/json/system/widget_types/visibility_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/visibility_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "visibility_card_with_background", "name": "Visibility card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/visibility_card_with_background_system_widget_image.png", "description": "Displays the latest visibility telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/visibility_chart_card.json b/application/src/main/data/json/system/widget_types/visibility_chart_card.json index fe69562505..5659ba9f8c 100644 --- a/application/src/main/data/json/system/widget_types/visibility_chart_card.json +++ b/application/src/main/data/json/system/widget_types/visibility_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "visibility_chart_card", "name": "Visibility chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/visibility_chart_card_system_widget_image.png", "description": "Displays a visibility data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/visibility_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/visibility_chart_card_with_background.json index 52454f7947..00daf878de 100644 --- a/application/src/main/data/json/system/widget_types/visibility_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/visibility_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "visibility_chart_card_with_background", "name": "Visibility chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/visibility_chart_card_with_background_system_widget_image.png", "description": "Displays a visibility data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card.json b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card.json index 7a00e37826..c8344efa5b 100644 --- a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card.json +++ b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card.json @@ -1,7 +1,7 @@ { "fqn": "volatile_organic_compounds_card", "name": "Volatile organic compounds card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/volatile_organic_compounds_card_system_widget_image.png", "description": "Displays the latest volatile organic compounds (VOCs) telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card_with_background.json b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card_with_background.json index b76fb80d53..814b1fc2c7 100644 --- a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "volatile_organic_compounds_card_with_background", "name": "Volatile organic compounds card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/volatile_organic_compounds_card_with_background_system_widget_image.png", "description": "Displays the latest volatile organic compounds (VOCs) telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_chart_card.json b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_chart_card.json index b146908138..ab35fe4aa1 100644 --- a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_chart_card.json +++ b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "volatile_organic_compounds_chart_card", "name": "Volatile organic compounds chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/volatile_organic_compounds_chart_card_system_widget_image.png", "description": "Displays a volatile organic compounds (VOCs) data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_chart_card_with_background.json index 0e1e1890f6..d2bf98bf09 100644 --- a/application/src/main/data/json/system/widget_types/volatile_organic_compounds_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/volatile_organic_compounds_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "volatile_organic_compounds_chart_card_with_background", "name": "Volatile organic compounds chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/volatile_organic_compounds_chart_card_with_background_system_widget_image.png", "description": "Displays a volatile organic compounds (VOCs) data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/wind_speed_and_direction.json b/application/src/main/data/json/system/widget_types/wind_speed_and_direction.json index 5ad343c61e..d1888a50ca 100644 --- a/application/src/main/data/json/system/widget_types/wind_speed_and_direction.json +++ b/application/src/main/data/json/system/widget_types/wind_speed_and_direction.json @@ -1,7 +1,7 @@ { "fqn": "wind_speed_and_direction", "name": "Wind speed and direction", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/wind_speed_and_direction_system_widget_image.png", "description": "Displays the latest values of the wind speed and direction.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/wind_speed_and_direction_with_background.json b/application/src/main/data/json/system/widget_types/wind_speed_and_direction_with_background.json index 2b4f9055c1..2d5600f77a 100644 --- a/application/src/main/data/json/system/widget_types/wind_speed_and_direction_with_background.json +++ b/application/src/main/data/json/system/widget_types/wind_speed_and_direction_with_background.json @@ -1,7 +1,7 @@ { "fqn": "wind_speed_and_direction_with_background", "name": "Wind speed and direction with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/wind_speed_and_direction_with_background_system_widget_image.png", "description": "Displays the latest values of the wind speed and direction with background.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/wind_speed_card.json b/application/src/main/data/json/system/widget_types/wind_speed_card.json index 8f3d311d16..260799f168 100644 --- a/application/src/main/data/json/system/widget_types/wind_speed_card.json +++ b/application/src/main/data/json/system/widget_types/wind_speed_card.json @@ -1,7 +1,7 @@ { "fqn": "wind_speed_card", "name": "Wind speed card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/wind_speed_card_system_widget_image.png", "description": "Displays the latest wind speed telemetry in a scalable rectangle card.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/wind_speed_card_with_background.json b/application/src/main/data/json/system/widget_types/wind_speed_card_with_background.json index 8a37be56e8..aed00ed294 100644 --- a/application/src/main/data/json/system/widget_types/wind_speed_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/wind_speed_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "wind_speed_card_with_background", "name": "Wind speed card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/wind_speed_card_with_background_system_widget_image.png", "description": "Displays the latest wind speed telemetry in a scalable rectangle card with the background image.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/wind_speed_chart_card.json b/application/src/main/data/json/system/widget_types/wind_speed_chart_card.json index 1b057c932d..c793dd86ad 100644 --- a/application/src/main/data/json/system/widget_types/wind_speed_chart_card.json +++ b/application/src/main/data/json/system/widget_types/wind_speed_chart_card.json @@ -1,7 +1,7 @@ { "fqn": "wind_speed_chart_card", "name": "Wind speed chart card", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/wind_speed_chart_card_system_widget_image.png", "description": "Displays a wind speed data by combining the latest and aggregated values with an optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/json/system/widget_types/wind_speed_chart_card_with_background.json b/application/src/main/data/json/system/widget_types/wind_speed_chart_card_with_background.json index 7c5b7ff83d..f8bf3db950 100644 --- a/application/src/main/data/json/system/widget_types/wind_speed_chart_card_with_background.json +++ b/application/src/main/data/json/system/widget_types/wind_speed_chart_card_with_background.json @@ -1,7 +1,7 @@ { "fqn": "wind_speed_chart_card_with_background", "name": "Wind speed chart card with background", - "deprecated": false, + "deprecated": true, "image": "tb-image;/api/images/system/wind_speed_chart_card_with_background_system_widget_image.png", "description": "Displays a wind speed data by combining the latest and aggregated values with the background image and optional simplified chart.", "descriptor": { diff --git a/application/src/main/data/upgrade/lts/schema_update.sql b/application/src/main/data/upgrade/lts/schema_update.sql index 790dc50b52..e23405e637 100644 --- a/application/src/main/data/upgrade/lts/schema_update.sql +++ b/application/src/main/data/upgrade/lts/schema_update.sql @@ -23,3 +23,25 @@ ALTER TABLE calculated_field ADD COLUMN IF NOT EXISTS additional_info varchar; -- CALCULATED FIELD ADDITIONAL INFO ADDITION END + +-- IOT HUB INSTALLED ITEM START + +CREATE TABLE IF NOT EXISTS iot_hub_installed_item ( + id UUID NOT NULL PRIMARY KEY, + created_time BIGINT NOT NULL, + tenant_id UUID NOT NULL, + item_id UUID NOT NULL, + item_version_id UUID NOT NULL, + item_name VARCHAR NOT NULL, + item_type VARCHAR NOT NULL, + version VARCHAR NOT NULL, + descriptor JSONB NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_iot_hub_installed_item_tenant_id ON iot_hub_installed_item(tenant_id); + +CREATE INDEX IF NOT EXISTS idx_iot_hub_installed_item_item_type ON iot_hub_installed_item(tenant_id, item_type); + +CREATE INDEX IF NOT EXISTS idx_iot_hub_installed_item_item_id ON iot_hub_installed_item(tenant_id, item_id); + +-- IOT HUB INSTALLED ITEM END diff --git a/application/src/main/java/org/thingsboard/server/controller/IotHubController.java b/application/src/main/java/org/thingsboard/server/controller/IotHubController.java new file mode 100644 index 0000000000..2af21a4af5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/IotHubController.java @@ -0,0 +1,162 @@ +/** + * Copyright © 2016-2026 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 io.swagger.v3.oas.annotations.Hidden; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import com.fasterxml.jackson.databind.JsonNode; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import jakarta.servlet.http.HttpServletRequest; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.iot_hub.DeviceInstalledItemDescriptor; +import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; +import org.thingsboard.server.dao.device.DeviceConnectivityService; +import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.system.SystemSecurityService; +import org.thingsboard.server.service.iot_hub.InstallItemVersionResult; +import org.thingsboard.server.service.iot_hub.InstallPlan; +import org.thingsboard.server.service.iot_hub.InstallPlanResult; +import org.thingsboard.server.service.iot_hub.UpdateItemVersionResult; +import org.thingsboard.server.service.iot_hub.IotHubService; + +@Hidden +@RestController +@TbCoreComponent +@RequestMapping("/api/iot-hub") +@RequiredArgsConstructor +@Slf4j +public class IotHubController extends BaseController { + + private final IotHubService iotHubService; + private final IotHubInstalledItemService iotHubInstalledItemService; + private final DeviceConnectivityService deviceConnectivityService; + private final SystemSecurityService systemSecurityService; + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/versions/{versionId}/install") + @ResponseBody + public InstallItemVersionResult installItemVersion(@PathVariable String versionId, + @RequestBody(required = false) JsonNode data, + HttpServletRequest request) throws ThingsboardException { + return iotHubService.installItemVersion(getCurrentUser(), versionId, data, request); + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/versions/{versionId}/installPlan") + @ResponseBody + public InstallPlan resolveInstallPlan(@PathVariable String versionId) throws ThingsboardException { + return iotHubService.resolveInstallPlan(getCurrentUser(), versionId); + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/installPlan") + @ResponseBody + public InstallPlanResult installPlan(@RequestBody InstallPlanRequest body, + HttpServletRequest request) throws ThingsboardException { + return iotHubService.installPlan(getCurrentUser(), body.plan(), body.data(), request); + } + + public record InstallPlanRequest(InstallPlan plan, JsonNode data) {} + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/device/register") + @ResponseBody + public InstallItemVersionResult registerDeviceInstall( + @RequestParam String versionId, + @RequestBody JsonNode body) throws ThingsboardException { + DeviceInstalledItemDescriptor descriptor = JacksonUtil.treeToValue(body, DeviceInstalledItemDescriptor.class); + return iotHubService.registerDeviceInstall(getCurrentUser(), versionId, descriptor); + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/installedItems/{installedItemId}/update/{versionId}") + @ResponseBody + public UpdateItemVersionResult updateItemVersion(@PathVariable UUID installedItemId, + @PathVariable String versionId, + @RequestParam(required = false, defaultValue = "false") boolean force, + HttpServletRequest request) throws ThingsboardException { + return iotHubService.updateItemVersion(getCurrentUser(), new IotHubInstalledItemId(installedItemId), versionId, force, request); + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/installedItems") + @ResponseBody + public PageData getInstalledItems(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @RequestParam(required = false) List itemTypes, + @RequestParam(required = false) UUID itemId) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return iotHubInstalledItemService.findByTenantId(getTenantId(), itemTypes, itemId, pageLink); + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/installedItems/count") + @ResponseBody + public long getInstalledItemsCount(@RequestParam(required = false) String itemType) throws ThingsboardException { + return iotHubInstalledItemService.countByTenantId(getTenantId(), itemType); + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/installedItems/itemIds") + @ResponseBody + public List getInstalledItemIds() throws ThingsboardException { + return iotHubInstalledItemService.findInstalledItemIdsByTenantId(getTenantId()); + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/installedItems/counts") + @ResponseBody + public Map getInstalledItemCounts(@RequestParam String itemType) throws ThingsboardException { + return iotHubInstalledItemService.findInstalledItemCounts(getTenantId(), itemType); + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @DeleteMapping("/installedItems/{installedItemId}") + @ResponseBody + public void deleteInstalledItem(@PathVariable UUID installedItemId) throws ThingsboardException { + iotHubService.deleteInstalledItem(getCurrentUser(), new IotHubInstalledItemId(installedItemId)); + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/connectivity") + @ResponseBody + public JsonNode getConnectivitySettings(HttpServletRequest request) throws Exception { + String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request); + return deviceConnectivityService.getConnectivityInfo(baseUrl); + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index e414efa6e7..e9c10261ad 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -84,6 +84,9 @@ public class SystemInfoController extends BaseController { @Value("${sql.entity_data_query_nulls_order_strategy:default}") private String nullsOrderStrategy; + @Value("${iot-hub.base-url:https://iot-hub.thingsboard.io}") + private String iotHubBaseUrl; + private static final Set ACCEPTED_NULLS_ORDER_STRATEGIES = Set.of("default", "nulls_first", "nulls_last"); @Autowired(required = false) @@ -185,6 +188,7 @@ public class SystemInfoController extends BaseController { systemParams.setIntermediateAggregationIntervalInSecForCF(tenantProfileConfiguration.getIntermediateAggregationIntervalInSecForCF()); systemParams.setTrendzSettings(trendzSettingsService.findTrendzSettings(currentUser.getTenantId())); } + systemParams.setIotHubBaseUrl(iotHubBaseUrl); systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID)) .map(QrCodeSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage) .orElse(false)); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java index 4adb97f380..e5464505b0 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java @@ -32,6 +32,7 @@ import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal; import org.thingsboard.script.api.tbel.TbelCfTsRollingArg; import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @@ -99,7 +100,7 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp @Override @Transactional - public void delete(CalculatedField calculatedField, SecurityUser user) { + public void delete(CalculatedField calculatedField, User user) { ActionType actionType = ActionType.DELETED; TenantId tenantId = calculatedField.getTenantId(); CalculatedFieldId calculatedFieldId = calculatedField.getId(); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java index 9dbb20dc2f..a35eedbe02 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.entitiy.cf; import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -34,7 +35,7 @@ public interface TbCalculatedFieldService { PageData findByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, CalculatedFieldType type, PageLink pageLink); - void delete(CalculatedField calculatedField, SecurityUser user); + void delete(CalculatedField calculatedField, User user); JsonNode executeTestScript(TenantId tenantId, JsonNode inputParams); diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java new file mode 100644 index 0000000000..5da54eb8b5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java @@ -0,0 +1,1077 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.iot_hub.AlarmRuleInstalledItemDescriptor; +import org.thingsboard.server.common.data.iot_hub.CalculatedFieldInstalledItemDescriptor; +import org.thingsboard.server.common.data.iot_hub.DashboardInstalledItemDescriptor; +import org.thingsboard.server.common.data.iot_hub.DeviceInstalledItemDescriptor; +import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; +import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemDescriptor; +import org.thingsboard.server.common.data.iot_hub.RuleChainInstalledItemDescriptor; +import org.thingsboard.server.common.data.iot_hub.SolutionTemplateInstalledItemDescriptor; +import org.thingsboard.server.common.data.iot_hub.WidgetInstalledItemDescriptor; +import org.thingsboard.server.exception.EntitiesLimitExceededException; +import org.thingsboard.server.service.solutions.SolutionService; +import org.thingsboard.server.service.solutions.data.solution.SolutionInstallResponse; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.NodeConnectionInfo; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.dao.asset.AssetProfileService; +import org.thingsboard.server.dao.cf.CalculatedFieldService; +import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.device.DeviceProfileService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.asset.profile.TbAssetProfileService; +import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; +import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService; +import org.thingsboard.server.service.entitiy.device.TbDeviceService; +import org.thingsboard.server.service.entitiy.device.profile.TbDeviceProfileService; +import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService; +import org.thingsboard.server.service.rule.TbRuleChainService; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.HexFormat; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +@Service +@TbCoreComponent +@RequiredArgsConstructor +@Slf4j +public class DefaultIotHubService implements IotHubService { + + private static final String ACTION_INSTALL = "install"; + private static final String ACTION_UPDATE = "update"; + private static final String SECTION_PACKAGE_DATA = "package data"; + private static final String SECTION_RULE_CHAIN_METADATA = "rule chain metadata"; + + private static final String ITEM_TYPE_WIDGET = "widget"; + private static final String ITEM_TYPE_DASHBOARD = "dashboard"; + private static final String ITEM_TYPE_CALCULATED_FIELD = "calculated field"; + private static final String ITEM_TYPE_ALARM_RULE = "alarm rule"; + private static final String ITEM_TYPE_RULE_CHAIN = "rule chain"; + private static final String ITEM_TYPE_DEVICE_PROFILE = "device profile"; + + private final IotHubRestClient iotHubRestClient; + private final TbWidgetTypeService tbWidgetTypeService; + private final TbDashboardService tbDashboardService; + private final TbCalculatedFieldService tbCalculatedFieldService; + private final RuleChainService ruleChainService; + private final TbRuleChainService tbRuleChainService; + private final TbDeviceProfileService tbDeviceProfileService; + private final AssetProfileService assetProfileService; + private final TbAssetProfileService tbAssetProfileService; + private final IotHubInstalledItemService iotHubInstalledItemService; + private final WidgetTypeService widgetTypeService; + private final DashboardService dashboardService; + private final CalculatedFieldService calculatedFieldService; + private final DeviceProfileService deviceProfileService; + private final DeviceService deviceService; + private final TbDeviceService tbDeviceService; + private final SolutionService solutionService; + + // Field names of the marketplace version JSON payload. Both the install path and the + // install-plan resolver parse the same shape, so the contract lives here in one place. + private static final String FIELD_ID = "id"; + private static final String FIELD_ITEM_ID = "itemId"; + private static final String FIELD_TYPE = "type"; + private static final String FIELD_NAME = "name"; + private static final String FIELD_VERSION = "version"; + private static final String FIELD_RELATED_ITEMS = "relatedItems"; + + @Override + public InstallItemVersionResult installItemVersion(SecurityUser user, String versionId, JsonNode data, HttpServletRequest request) { + TenantId tenantId = user.getTenantId(); + log.info("[{}] Installing IoT Hub item version: {}", tenantId, versionId); + + JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId); + if (versionInfo == null) { + throw new IllegalArgumentException("Failed to get version info from IoT Hub"); + } + + try { + IotHubInstalledItem installedItem = doInstallVersion(user, versionId, data, request); + return InstallItemVersionResult.success(installedItem.getDescriptor()); + } catch (Exception e) { + log.error("[{}] Failed to install IoT Hub item version: {}", tenantId, versionId, e); + if (e instanceof EntitiesLimitExceededException el) { + throw el; + } + return InstallItemVersionResult.error(e.getMessage()); + } + } + + /** + * Fetch + apply a single marketplace version, persist the installed-item record, and ping + * the marketplace install counter. Throws on failure so callers (cascade install) can roll back. + * Package-private so the cascade-install / rollback orchestration can be unit-tested in isolation. + */ + IotHubInstalledItem doInstallVersion(SecurityUser user, String versionId, JsonNode data, HttpServletRequest request) throws Exception { + TenantId tenantId = user.getTenantId(); + JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId); + String itemType = versionInfo.get(FIELD_TYPE).asText(); + String itemName = versionInfo.get(FIELD_NAME).asText(); + UUID itemId = UUID.fromString(versionInfo.get(FIELD_ITEM_ID).asText()); + String version = versionInfo.get(FIELD_VERSION).asText(); + log.debug("[{}] Fetched version info: {} (type: {})", tenantId, itemName, itemType); + + byte[] fileData = iotHubRestClient.getVersionFileData(versionId); + log.debug("[{}] Fetched file data, size: {} bytes", tenantId, fileData != null ? fileData.length : 0); + + IotHubInstalledItemDescriptor descriptor = switch (itemType) { + case "WIDGET" -> installWidget(user, tenantId, fileData); + case "DASHBOARD" -> installDashboard(user, tenantId, fileData); + case "CALCULATED_FIELD" -> installCalculatedField(user, tenantId, fileData, data); + case "ALARM_RULE" -> installAlarmRule(user, tenantId, fileData, data); + case "RULE_CHAIN" -> installRuleChain(user, tenantId, fileData, data); + case "DEVICE" -> installDeviceProfile(user, tenantId, fileData); + case "SOLUTION_TEMPLATE" -> installSolution(user, tenantId, fileData, request); + default -> throw new IllegalArgumentException("Unsupported IoT Hub item type: " + itemType); + }; + + IotHubInstalledItem installedItem = new IotHubInstalledItem(); + installedItem.setTenantId(tenantId); + installedItem.setItemId(itemId); + installedItem.setItemVersionId(UUID.fromString(versionId)); + installedItem.setItemName(itemName); + installedItem.setItemType(itemType); + installedItem.setVersion(version); + installedItem.setDescriptor(descriptor); + try { + installedItem = iotHubInstalledItemService.save(tenantId, installedItem); + } catch (Exception e) { + // Tracking save failed after the entity was already created locally — compensate by + // deleting the orphaned entity so the user is not left with an installed item that + // IoT Hub update/delete flows cannot see. + log.error("[{}] Failed to save IoT Hub installed-item tracking row for {} ({}); rolling back created entity", + tenantId, itemName, itemType, e); + try { + deleteEntityForDescriptor(user, tenantId, descriptor, null); + } catch (Exception rb) { + log.error("[{}] Compensating delete after failed tracking save threw — entity may remain orphaned", + tenantId, rb); + } + throw e; + } + + try { + iotHubRestClient.reportVersionInstalled(versionId); + } catch (Exception e) { + // Counter ping is best-effort — do not fail the install if it errors. + log.warn("[{}] Failed to report install counter for version {}: {}", tenantId, versionId, e.getMessage()); + } + log.info("[{}] Successfully installed IoT Hub item version: {} (type: {})", tenantId, itemName, itemType); + return installedItem; + } + + private WidgetInstalledItemDescriptor installWidget(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception { + WidgetTypeDetails widgetTypeDetails; + try { + widgetTypeDetails = JacksonUtil.fromString(new String(fileData), WidgetTypeDetails.class, true); + } catch (Exception e) { + throw parseFailure(ACTION_INSTALL, ITEM_TYPE_WIDGET, e); + } + widgetTypeDetails.setId(null); + widgetTypeDetails.setTenantId(tenantId); + WidgetTypeDetails saved = tbWidgetTypeService.save(widgetTypeDetails, false, user); + log.debug("[{}] Widget installed: {}", tenantId, saved.getName()); + WidgetInstalledItemDescriptor descriptor = new WidgetInstalledItemDescriptor(); + descriptor.setWidgetTypeId(saved.getId()); + return descriptor; + } + + private DashboardInstalledItemDescriptor installDashboard(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception { + Dashboard dashboard; + try { + dashboard = JacksonUtil.fromString(new String(fileData), Dashboard.class, true); + } catch (Exception e) { + throw parseFailure(ACTION_INSTALL, ITEM_TYPE_DASHBOARD, e); + } + dashboard.setId(null); + dashboard.setTenantId(tenantId); + Dashboard saved = tbDashboardService.save(dashboard, user); + log.debug("[{}] Dashboard installed: {}", tenantId, saved.getTitle()); + DashboardInstalledItemDescriptor descriptor = new DashboardInstalledItemDescriptor(); + descriptor.setDashboardId(saved.getId()); + return descriptor; + } + + private CalculatedFieldInstalledItemDescriptor installCalculatedField(SecurityUser user, TenantId tenantId, byte[] fileData, JsonNode data) throws Exception { + CalculatedField calculatedField; + try { + calculatedField = JacksonUtil.fromString(new String(fileData), CalculatedField.class, true); + } catch (Exception e) { + throw parseFailure(ACTION_INSTALL, ITEM_TYPE_CALCULATED_FIELD, e); + } + calculatedField.setId(null); + calculatedField.setTenantId(tenantId); + if (data != null && data.has("entityId")) { + EntityId entityId = JacksonUtil.treeToValue(data.get("entityId"), EntityId.class); + calculatedField.setEntityId(entityId); + } + CalculatedField saved = tbCalculatedFieldService.save(calculatedField, user); + log.debug("[{}] Calculated field installed: {}", tenantId, saved.getName()); + CalculatedFieldInstalledItemDescriptor descriptor = new CalculatedFieldInstalledItemDescriptor(); + descriptor.setCalculatedFieldId(saved.getId()); + descriptor.setEntityId(saved.getEntityId()); + return descriptor; + } + + private AlarmRuleInstalledItemDescriptor installAlarmRule(SecurityUser user, TenantId tenantId, byte[] fileData, JsonNode data) throws Exception { + CalculatedField alarmRule; + try { + alarmRule = JacksonUtil.fromString(new String(fileData), CalculatedField.class, true); + } catch (Exception e) { + throw parseFailure(ACTION_INSTALL, ITEM_TYPE_ALARM_RULE, e); + } + if (alarmRule.getType() != CalculatedFieldType.ALARM) { + throw new Exception("Expected ALARM-type calculated field for ALARM_RULE install; got: " + alarmRule.getType()); + } + alarmRule.setId(null); + alarmRule.setTenantId(tenantId); + if (data != null && data.has("entityId")) { + EntityId entityId = JacksonUtil.treeToValue(data.get("entityId"), EntityId.class); + alarmRule.setEntityId(entityId); + } + CalculatedField saved = tbCalculatedFieldService.save(alarmRule, user); + log.debug("[{}] Alarm rule installed: {}", tenantId, saved.getName()); + AlarmRuleInstalledItemDescriptor descriptor = new AlarmRuleInstalledItemDescriptor(); + descriptor.setCalculatedFieldId(saved.getId()); + descriptor.setEntityId(saved.getEntityId()); + return descriptor; + } + + private RuleChainInstalledItemDescriptor installRuleChain( + SecurityUser user, TenantId tenantId, byte[] fileData, JsonNode data) throws Exception { + JsonNode json = JacksonUtil.toJsonNode(new String(fileData)); + + RuleChain ruleChain; + try { + ruleChain = JacksonUtil.fromString(json.get("ruleChain").toString(), RuleChain.class, true); + } catch (Exception e) { + throw parseFailure(ACTION_INSTALL, ITEM_TYPE_RULE_CHAIN, e); + } + + RuleChainMetaData metadata; + try { + metadata = JacksonUtil.fromString(json.get("metadata").toString(), RuleChainMetaData.class, true); + } catch (Exception e) { + throw parseFailure(ACTION_INSTALL, ITEM_TYPE_RULE_CHAIN, SECTION_RULE_CHAIN_METADATA, e); + } + + ruleChain.setId(null); + ruleChain.setTenantId(tenantId); + RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain); + + metadata.setRuleChainId(savedRuleChain.getId()); + metadata.setVersion(savedRuleChain.getVersion()); + ruleChainService.saveRuleChainMetaData(tenantId, metadata, tbRuleChainService::updateRuleNodeConfiguration); + + log.debug("[{}] Rule chain installed: {}", tenantId, savedRuleChain.getName()); + + RuleChainInstalledItemDescriptor descriptor = new RuleChainInstalledItemDescriptor(); + descriptor.setRuleChainId(savedRuleChain.getId()); + + if (data != null && data.has("entityId") && !data.get("entityId").isNull()) { + EntityId entityId = JacksonUtil.treeToValue(data.get("entityId"), EntityId.class); + setAsDefaultRuleChain(user, tenantId, entityId, savedRuleChain.getId()); + descriptor.setTargetProfileId(entityId); + } + + return descriptor; + } + + private void setAsDefaultRuleChain(SecurityUser user, TenantId tenantId, + EntityId entityId, RuleChainId ruleChainId) throws Exception { + switch (entityId.getEntityType()) { + case DEVICE_PROFILE -> { + DeviceProfile profile = deviceProfileService.findDeviceProfileById( + tenantId, (DeviceProfileId) entityId); + if (profile == null) { + throw new IllegalArgumentException( + "Device profile not found: " + entityId.getId()); + } + profile.setDefaultRuleChainId(ruleChainId); + tbDeviceProfileService.save(profile, user); + log.debug("[{}] Set rule chain {} as default on device profile {}", + tenantId, ruleChainId.getId(), entityId.getId()); + } + case ASSET_PROFILE -> { + AssetProfile profile = assetProfileService.findAssetProfileById( + tenantId, (AssetProfileId) entityId); + if (profile == null) { + throw new IllegalArgumentException( + "Asset profile not found: " + entityId.getId()); + } + profile.setDefaultRuleChainId(ruleChainId); + tbAssetProfileService.save(profile, user); + log.debug("[{}] Set rule chain {} as default on asset profile {}", + tenantId, ruleChainId.getId(), entityId.getId()); + } + default -> throw new IllegalArgumentException( + "Rule chain can only be set as default on device or asset profile, got: " + + entityId.getEntityType()); + } + } + + private DeviceInstalledItemDescriptor installDeviceProfile(SecurityUser user, TenantId tenantId, byte[] fileData) throws Exception { + DeviceProfile deviceProfile; + try { + deviceProfile = JacksonUtil.fromString(new String(fileData), DeviceProfile.class, true); + } catch (Exception e) { + throw parseFailure(ACTION_INSTALL, ITEM_TYPE_DEVICE_PROFILE, e); + } + deviceProfile.setId(null); + deviceProfile.setTenantId(tenantId); + tbDeviceProfileService.save(deviceProfile, user); + log.debug("[{}] Device profile installed: {}", tenantId, deviceProfile.getName()); + return new DeviceInstalledItemDescriptor(); + } + + private SolutionTemplateInstalledItemDescriptor installSolution(SecurityUser user, TenantId tenantId, byte[] fileData, HttpServletRequest request) throws Exception { + SolutionInstallResponse response = solutionService.installSolution(user, tenantId, fileData, request); + if (!response.isSuccess()) { + throw new RuntimeException(response.getDetails()); + } + SolutionTemplateInstalledItemDescriptor descriptor = new SolutionTemplateInstalledItemDescriptor(); + descriptor.setCreatedEntityIds(response.getCreatedEntityIds()); + descriptor.setTenantTelemetryKeys(response.getTenantTelemetryKeys()); + descriptor.setTenantAttributeKeys(response.getTenantAttributeKeys()); + descriptor.setDashboardId(response.getDashboardId()); + descriptor.setPublicId(response.getPublicId()); + descriptor.setMainDashboardPublic(response.isMainDashboardPublic()); + descriptor.setDetails(response.getDetails()); + return descriptor; + } + + @Override + public UpdateItemVersionResult updateItemVersion(SecurityUser user, IotHubInstalledItemId installedItemId, String versionId, boolean force, HttpServletRequest request) { + TenantId tenantId = user.getTenantId(); + log.info("[{}] Updating IoT Hub installed item {} to version: {}", tenantId, installedItemId, versionId); + + try { + IotHubInstalledItem installedItem = iotHubInstalledItemService.findById(tenantId, installedItemId); + if (installedItem == null) { + throw new IllegalArgumentException("Installed item not found"); + } + + String itemType = installedItem.getItemType(); + IotHubInstalledItemDescriptor descriptor = installedItem.getDescriptor(); + + // Skip checksum validation for solution templates + if (!"SOLUTION_TEMPLATE".equals(itemType)) { + JsonNode installedVersionInfo = iotHubRestClient.getVersionInfo(installedItem.getItemVersionId().toString()); + if (installedVersionInfo == null) { + throw new IllegalArgumentException("Failed to get installed version info from IoT Hub"); + } + String installedChecksum = installedVersionInfo.has("checksum") ? installedVersionInfo.get("checksum").asText() : null; + log.info("[{}] Installed version info: name={}, version={}, checksum={}", tenantId, installedItem.getItemName(), installedItem.getVersion(), installedChecksum); + + if (!force) { + String entityChecksum = calculateEntityChecksum(tenantId, installedItem); + boolean entityModified = installedChecksum != null && !installedChecksum.equals(entityChecksum); + log.info("[{}] Entity checksum: {}, modified: {}", tenantId, entityChecksum, entityModified); + + if (entityModified) { + return UpdateItemVersionResult.entityModified(); + } + } + } + + JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId); + + if (versionInfo == null) { + throw new IllegalArgumentException("Failed to get version info from IoT Hub"); + } + + String updateItemType = versionInfo.get("type").asText(); + if (!itemType.equals(updateItemType)) { + throw new IllegalArgumentException("Installed item type does not match the new version's item type."); + } + + String itemName = versionInfo.get("name").asText(); + String version = versionInfo.get("version").asText(); + + byte[] fileData = iotHubRestClient.getVersionFileData(versionId); + log.info("[{}] Fetched update file data, size: {} bytes", tenantId, fileData != null ? fileData.length : 0); + + switch (itemType) { + case "WIDGET" -> updateWidget(user, tenantId, (WidgetInstalledItemDescriptor) descriptor, fileData); + case "DASHBOARD" -> updateDashboard(user, tenantId, (DashboardInstalledItemDescriptor) descriptor, fileData); + case "CALCULATED_FIELD" -> updateCalculatedField(user, tenantId, (CalculatedFieldInstalledItemDescriptor) descriptor, fileData); + case "ALARM_RULE" -> updateAlarmRule(user, tenantId, (AlarmRuleInstalledItemDescriptor) descriptor, fileData); + case "RULE_CHAIN" -> updateRuleChain(tenantId, (RuleChainInstalledItemDescriptor) descriptor, fileData); + case "DEVICE" -> { + DeviceInstalledItemDescriptor dd = (DeviceInstalledItemDescriptor) descriptor; + deleteDevicePackageEntities(tenantId, dd.getCreatedEntityIds(), user); + dd.setCreatedEntityIds(null); + dd.setDashboardId(null); + } + case "SOLUTION_TEMPLATE" -> { + SolutionTemplateInstalledItemDescriptor stDescriptor = (SolutionTemplateInstalledItemDescriptor) descriptor; + solutionService.deleteSolution(tenantId, stDescriptor, user); + SolutionInstallResponse response = solutionService.installSolution(user, tenantId, fileData, request); + if (!response.isSuccess()) { + throw new RuntimeException(response.getDetails()); + } + stDescriptor.setCreatedEntityIds(response.getCreatedEntityIds()); + stDescriptor.setTenantTelemetryKeys(response.getTenantTelemetryKeys()); + stDescriptor.setTenantAttributeKeys(response.getTenantAttributeKeys()); + stDescriptor.setDashboardId(response.getDashboardId()); + stDescriptor.setPublicId(response.getPublicId()); + stDescriptor.setMainDashboardPublic(response.isMainDashboardPublic()); + stDescriptor.setDetails(response.getDetails()); + } + default -> throw new IllegalArgumentException("Unsupported IoT Hub item type: " + itemType); + } + + installedItem.setItemVersionId(UUID.fromString(versionId)); + installedItem.setItemName(itemName); + installedItem.setVersion(version); + iotHubInstalledItemService.save(tenantId, installedItem); + + log.info("[{}] Successfully updated IoT Hub item {} to version: {}", tenantId, itemName, version); + + return UpdateItemVersionResult.success(installedItem.getDescriptor()); + } catch (Exception e) { + log.error("[{}] Failed to update IoT Hub installed item {} to version: {}", tenantId, installedItemId, versionId, e); + return UpdateItemVersionResult.error(e.getMessage()); + } + } + + private void updateWidget(SecurityUser user, TenantId tenantId, WidgetInstalledItemDescriptor descriptor, byte[] fileData) throws Exception { + WidgetTypeDetails newWidgetType; + try { + newWidgetType = JacksonUtil.fromString(new String(fileData), WidgetTypeDetails.class, true); + } catch (Exception e) { + throw parseFailure(ACTION_UPDATE, ITEM_TYPE_WIDGET, e); + } + WidgetTypeDetails existing = widgetTypeService.findWidgetTypeDetailsById(tenantId, descriptor.getWidgetTypeId()); + if (existing == null) { + throw new Exception("Widget not found for update"); + } + existing.setName(newWidgetType.getName()); + existing.setDescriptor(newWidgetType.getDescriptor()); + tbWidgetTypeService.save(existing, false, user); + } + + private void updateDashboard(SecurityUser user, TenantId tenantId, DashboardInstalledItemDescriptor descriptor, byte[] fileData) throws Exception { + Dashboard newDashboard; + try { + newDashboard = JacksonUtil.fromString(new String(fileData), Dashboard.class, true); + } catch (Exception e) { + throw parseFailure(ACTION_UPDATE, ITEM_TYPE_DASHBOARD, e); + } + Dashboard existing = dashboardService.findDashboardById(tenantId, descriptor.getDashboardId()); + if (existing == null) { + throw new Exception("Dashboard not found for update"); + } + existing.setTitle(newDashboard.getTitle()); + existing.setConfiguration(newDashboard.getConfiguration()); + tbDashboardService.save(existing, user); + } + + private void updateCalculatedField(SecurityUser user, TenantId tenantId, CalculatedFieldInstalledItemDescriptor descriptor, byte[] fileData) throws Exception { + CalculatedField newCf; + try { + newCf = JacksonUtil.fromString(new String(fileData), CalculatedField.class, true); + } catch (Exception e) { + throw parseFailure(ACTION_UPDATE, ITEM_TYPE_CALCULATED_FIELD, e); + } + CalculatedField existing = calculatedFieldService.findById(tenantId, descriptor.getCalculatedFieldId()); + if (existing == null) { + throw new Exception("Calculated field not found for update"); + } + existing.setName(newCf.getName()); + existing.setType(newCf.getType()); + existing.setConfiguration(newCf.getConfiguration()); + tbCalculatedFieldService.save(existing, user); + } + + private void updateAlarmRule(SecurityUser user, TenantId tenantId, AlarmRuleInstalledItemDescriptor descriptor, byte[] fileData) throws Exception { + CalculatedField newCf; + try { + newCf = JacksonUtil.fromString(new String(fileData), CalculatedField.class, true); + } catch (Exception e) { + throw parseFailure(ACTION_UPDATE, ITEM_TYPE_ALARM_RULE, e); + } + if (newCf.getType() != CalculatedFieldType.ALARM) { + throw new Exception("Expected ALARM-type calculated field for ALARM_RULE update; got: " + newCf.getType()); + } + CalculatedField existing = calculatedFieldService.findById(tenantId, descriptor.getCalculatedFieldId()); + if (existing == null) { + throw new Exception("Alarm rule not found for update"); + } + existing.setName(newCf.getName()); + existing.setType(newCf.getType()); + existing.setConfiguration(newCf.getConfiguration()); + tbCalculatedFieldService.save(existing, user); + } + + private void updateRuleChain(TenantId tenantId, RuleChainInstalledItemDescriptor descriptor, byte[] fileData) throws Exception { + JsonNode json = JacksonUtil.toJsonNode(new String(fileData)); + RuleChainMetaData metadata; + try { + metadata = JacksonUtil.fromString(json.get("metadata").toString(), RuleChainMetaData.class, true); + } catch (Exception e) { + throw parseFailure(ACTION_UPDATE, ITEM_TYPE_RULE_CHAIN, SECTION_RULE_CHAIN_METADATA, e); + } + RuleChain existing = ruleChainService.findRuleChainById(tenantId, descriptor.getRuleChainId()); + if (existing == null) { + throw new Exception("Rule chain not found for update"); + } + RuleChain newRuleChain; + try { + newRuleChain = JacksonUtil.fromString(json.get("ruleChain").toString(), RuleChain.class, true); + } catch (Exception e) { + throw parseFailure(ACTION_UPDATE, ITEM_TYPE_RULE_CHAIN, e); + } + existing.setName(newRuleChain.getName()); + RuleChain savedRuleChain = ruleChainService.saveRuleChain(existing); + metadata.setRuleChainId(savedRuleChain.getId()); + metadata.setVersion(savedRuleChain.getVersion()); + ruleChainService.saveRuleChainMetaData(tenantId, metadata, tbRuleChainService::updateRuleNodeConfiguration); + } + + private String calculateEntityChecksum(TenantId tenantId, IotHubInstalledItem installedItem) { + IotHubInstalledItemDescriptor descriptor = installedItem.getDescriptor(); + if (descriptor instanceof WidgetInstalledItemDescriptor wd) { + return calculateWidgetChecksum(tenantId, wd); + } else if (descriptor instanceof DashboardInstalledItemDescriptor dd) { + return calculateDashboardChecksum(tenantId, dd); + } else if (descriptor instanceof CalculatedFieldInstalledItemDescriptor cd) { + return calculateCalculatedFieldChecksum(tenantId, cd.getCalculatedFieldId()); + } else if (descriptor instanceof AlarmRuleInstalledItemDescriptor ad) { + return calculateCalculatedFieldChecksum(tenantId, ad.getCalculatedFieldId()); + } else if (descriptor instanceof RuleChainInstalledItemDescriptor rd) { + return calculateRuleChainChecksum(tenantId, rd); + } + return null; + } + + private String calculateCalculatedFieldChecksum(TenantId tenantId, CalculatedFieldId calculatedFieldId) { + CalculatedField cf = calculatedFieldService.findById(tenantId, calculatedFieldId); + if (cf == null) { + return null; + } + String content = (cf.getName() != null ? cf.getName() : "") + + (cf.getType() != null ? cf.getType().name() : "") + + (cf.getConfiguration() != null ? JacksonUtil.valueToTree(cf.getConfiguration()).toString() : ""); + return sha256(content); + } + + private String calculateDashboardChecksum(TenantId tenantId, DashboardInstalledItemDescriptor descriptor) { + Dashboard dashboard = dashboardService.findDashboardById(tenantId, descriptor.getDashboardId()); + if (dashboard == null) { + return null; + } + String content = (dashboard.getTitle() != null ? dashboard.getTitle() : "") + + (dashboard.getConfiguration() != null ? dashboard.getConfiguration().toString() : ""); + return sha256(content); + } + + private String calculateWidgetChecksum(TenantId tenantId, WidgetInstalledItemDescriptor descriptor) { + WidgetTypeDetails widgetType = widgetTypeService.findWidgetTypeDetailsById(tenantId, descriptor.getWidgetTypeId()); + if (widgetType == null) { + return null; + } + String content = (widgetType.getFqn() != null ? widgetType.getFqn() : "") + + (widgetType.getName() != null ? widgetType.getName() : "") + + (widgetType.getDescriptor() != null ? widgetType.getDescriptor().toString() : ""); + return sha256(content); + } + + private String calculateRuleChainChecksum(TenantId tenantId, RuleChainInstalledItemDescriptor descriptor) { + RuleChain ruleChain = ruleChainService.findRuleChainById(tenantId, descriptor.getRuleChainId()); + if (ruleChain == null) { + return null; + } + RuleChainMetaData metadata = ruleChainService.loadRuleChainMetaData(tenantId, descriptor.getRuleChainId()); + StringBuilder content = new StringBuilder(); + content.append(ruleChain.getName() != null ? ruleChain.getName() : ""); + content.append(ruleChain.getType() != null ? ruleChain.getType().name() : ""); + if (metadata.getNodes() != null) { + for (RuleNode node : metadata.getNodes()) { + content.append(node.getType() != null ? node.getType() : ""); + content.append(node.getName() != null ? node.getName() : ""); + content.append(node.getConfiguration() != null ? node.getConfiguration().toString() : ""); + } + } + if (metadata.getConnections() != null) { + for (NodeConnectionInfo conn : metadata.getConnections()) { + content.append(conn.getFromIndex()); + content.append(conn.getToIndex()); + content.append(conn.getType() != null ? conn.getType() : ""); + } + } + return sha256(content.toString()); + } + + private static String sha256(String input) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(hash); + } catch (Exception e) { + throw new RuntimeException("Failed to calculate SHA-256", e); + } + } + + @Override + public InstallItemVersionResult registerDeviceInstall(SecurityUser user, String versionId, DeviceInstalledItemDescriptor descriptor) { + TenantId tenantId = user.getTenantId(); + log.info("[{}] Registering device package install for version: {}", tenantId, versionId); + + try { + JsonNode versionInfo = iotHubRestClient.getVersionInfo(versionId); + + if (versionInfo == null) { + throw new IllegalArgumentException("Failed to get version info from IoT Hub"); + } + + String itemName = versionInfo.get("name").asText(); + UUID itemId = UUID.fromString(versionInfo.get("itemId").asText()); + String version = versionInfo.get("version").asText(); + + IotHubInstalledItem installedItem = new IotHubInstalledItem(); + installedItem.setTenantId(tenantId); + installedItem.setItemId(itemId); + installedItem.setItemVersionId(UUID.fromString(versionId)); + installedItem.setItemName(itemName); + installedItem.setItemType("DEVICE"); + installedItem.setVersion(version); + installedItem.setDescriptor(descriptor); + try { + iotHubInstalledItemService.save(tenantId, installedItem); + } catch (Exception e) { + log.error("[{}] Failed to save IoT Hub installed-item tracking row for device package {} (version {}); rolling back created entities", + tenantId, itemName, version, e); + try { + deleteEntityForDescriptor(user, tenantId, descriptor, null); + } catch (Exception rb) { + log.error("[{}] Compensating delete after failed tracking save threw — device package entities may remain orphaned", + tenantId, rb); + } + throw e; + } + + try { + iotHubRestClient.reportVersionInstalled(versionId); + } catch (Exception e) { + // Counter ping is best-effort — do not fail the install if it errors. + log.warn("[{}] Failed to report install counter for version {}: {}", tenantId, versionId, e.getMessage()); + } + log.info("[{}] Registered device package install: {} (version {})", tenantId, itemName, version); + + return InstallItemVersionResult.success(descriptor); + } catch (Exception e) { + log.error("[{}] Failed to register device package install: {}", tenantId, versionId, e); + return InstallItemVersionResult.error(e.getMessage()); + } + } + + private void deleteDevicePackageEntities(TenantId tenantId, List createdEntityIds, SecurityUser user) { + if (createdEntityIds == null || createdEntityIds.isEmpty()) { + return; + } + List reversed = new ArrayList<>(createdEntityIds); + Collections.reverse(reversed); + for (EntityId entityId : reversed) { + try { + deleteDevicePackageEntity(tenantId, entityId, user); + } catch (Exception e) { + log.error("[{}] Failed to delete device package entity: {}", tenantId, entityId, e); + } + } + } + + private void deleteDevicePackageEntity(TenantId tenantId, EntityId entityId, SecurityUser user) { + switch (entityId.getEntityType()) { + case DEVICE -> { + var device = deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId())); + if (device != null) tbDeviceService.delete(device, user); + } + case DEVICE_PROFILE -> { + var profile = deviceProfileService.findDeviceProfileById(tenantId, new DeviceProfileId(entityId.getId())); + if (profile != null) tbDeviceProfileService.delete(profile, user); + } + case DASHBOARD -> { + var dashboard = dashboardService.findDashboardById(tenantId, new DashboardId(entityId.getId())); + if (dashboard != null) tbDashboardService.delete(dashboard, user); + } + case RULE_CHAIN -> { + var ruleChain = ruleChainService.findRuleChainById(tenantId, new RuleChainId(entityId.getId())); + if (ruleChain != null) tbRuleChainService.delete(ruleChain, user); + } + default -> log.warn("[{}] Unsupported entity type for device package delete: {}", tenantId, entityId.getEntityType()); + } + } + + @Override + public void deleteInstalledItem(SecurityUser user, IotHubInstalledItemId installedItemId) { + TenantId tenantId = user.getTenantId(); + IotHubInstalledItem installedItem = iotHubInstalledItemService.findById(tenantId, installedItemId); + if (installedItem == null) { + throw new IllegalArgumentException("Installed item not found"); + } + + deleteEntityForDescriptor(user, tenantId, installedItem.getDescriptor(), installedItemId); + + iotHubInstalledItemService.deleteById(tenantId, installedItemId); + log.info("[{}] Deleted installed IoT Hub item: {}", tenantId, installedItem.getItemName()); + } + + /** + * Per-type dispatcher that removes the local entities a marketplace install created. Shared by + * {@link #deleteInstalledItem} and by the tracking-row compensation path in + * {@link #doInstallVersion} so a failed tracking save does not leave the entity orphaned. + */ + private void deleteEntityForDescriptor(SecurityUser user, TenantId tenantId, + IotHubInstalledItemDescriptor descriptor, + IotHubInstalledItemId installedItemId) { + if (descriptor instanceof WidgetInstalledItemDescriptor wd) { + WidgetTypeDetails widgetType = widgetTypeService.findWidgetTypeDetailsById(tenantId, wd.getWidgetTypeId()); + if (widgetType != null) { + tbWidgetTypeService.delete(widgetType, user); + } + } else if (descriptor instanceof DashboardInstalledItemDescriptor dd) { + Dashboard dashboard = dashboardService.findDashboardById(tenantId, dd.getDashboardId()); + if (dashboard != null) { + tbDashboardService.delete(dashboard, user); + } + } else if (descriptor instanceof CalculatedFieldInstalledItemDescriptor cd) { + CalculatedField calculatedField = calculatedFieldService.findById(tenantId, cd.getCalculatedFieldId()); + if (calculatedField != null) { + tbCalculatedFieldService.delete(calculatedField, user); + } + } else if (descriptor instanceof AlarmRuleInstalledItemDescriptor ad) { + CalculatedField alarmRule = calculatedFieldService.findById(tenantId, ad.getCalculatedFieldId()); + if (alarmRule != null) { + tbCalculatedFieldService.delete(alarmRule, user); + } + } else if (descriptor instanceof RuleChainInstalledItemDescriptor rd) { + if (rd.getRuleChainId() != null) { + RuleChain ruleChain = ruleChainService.findRuleChainById(tenantId, rd.getRuleChainId()); + if (ruleChain != null) { + tbRuleChainService.delete(ruleChain, user); + } + } + } else if (descriptor instanceof DeviceInstalledItemDescriptor dd) { + deleteDevicePackageEntities(tenantId, dd.getCreatedEntityIds(), user); + } else if (descriptor instanceof SolutionTemplateInstalledItemDescriptor st) { + try { + solutionService.deleteSolution(tenantId, st, user); + } catch (ThingsboardException e) { + log.error("[{}] Failed to delete solution for installed item {}", tenantId, installedItemId, e); + } + } + } + + @Override + public InstallPlan resolveInstallPlan(SecurityUser user, String versionId) { + TenantId tenantId = user.getTenantId(); + log.debug("[{}] Resolving install plan for version: {}", tenantId, versionId); + + JsonNode rootVersion = iotHubRestClient.getVersionInfo(versionId); + if (rootVersion == null) { + throw new IllegalArgumentException("Marketplace version not found: " + versionId); + } + + // Null-safe: a malformed payload without an itemId must surface the friendly + // IllegalArgumentException thrown by addPlanEntry(root) below, not an NPE here. + String rootItemId = optText(rootVersion, FIELD_ITEM_ID); + JsonNode related = rootVersion.get(FIELD_RELATED_ITEMS); + + // The plan only ever touches the root and its (one-level-deep) related items, so we ask the + // DB whether just those item ids are already installed instead of loading every installed id + // for the tenant — a tenant may have thousands installed while a plan checks a handful. + Set candidateItemIds = new HashSet<>(); + addCandidateItemId(candidateItemIds, rootItemId); + if (related != null && related.isArray()) { + for (JsonNode relatedNode : related) { + addCandidateItemId(candidateItemIds, relatedNode.asText()); + } + } + Set alreadyInstalledItemIds = candidateItemIds.isEmpty() + ? Set.of() + : new HashSet<>(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(tenantId, candidateItemIds)); + + // LinkedHashMap preserves insertion order; we add the related items (deps) before the + // root so a single forward iteration yields the correct install sequence (deps first, + // root last). Related items are leaf dependencies — IoT Hub items are only ever one + // level deep, so there is no need to walk a related item's own related items. + LinkedHashMap entries = new LinkedHashMap<>(); + + if (related != null && related.isArray()) { + for (JsonNode relatedNode : related) { + String relatedItemId = relatedNode.asText(); + if (relatedItemId == null || relatedItemId.isEmpty() + || relatedItemId.equals(rootItemId) || entries.containsKey(relatedItemId)) { + continue; + } + JsonNode relatedVersion; + try { + relatedVersion = iotHubRestClient.getPublishedVersionByItemId(relatedItemId); + } catch (Exception e) { + log.warn("Failed to fetch related item {}: {}", relatedItemId, e.getMessage()); + entries.put(relatedItemId, missingEntry(relatedItemId, e.getMessage())); + continue; + } + if (relatedVersion == null) { + log.warn("Related IoT Hub item {} is missing or unpublished — recording in plan", relatedItemId); + entries.put(relatedItemId, missingEntry(relatedItemId, "Item not found or not published")); + continue; + } + addPlanEntry(relatedVersion, alreadyInstalledItemIds, entries, false); + } + } + + addPlanEntry(rootVersion, alreadyInstalledItemIds, entries, true); + + return new InstallPlan(versionId, new ArrayList<>(entries.values())); + } + + /** + * Build an {@link InstallPlanEntry} for a single marketplace version and append it to the + * plan. No traversal of {@code relatedItems} happens here — related items are resolved one + * level deep by {@link #resolveInstallPlan}. + */ + private void addPlanEntry(JsonNode versionInfo, + Set alreadyInstalledItemIds, + LinkedHashMap entries, + boolean root) { + String itemId = optText(versionInfo, FIELD_ITEM_ID); + String versionId = optText(versionInfo, FIELD_ID); + if (itemId == null || versionId == null) { + // The marketplace payload is missing its identifiers. The root must have them to be + // installable; a related item without them is recorded as missing so the rest of the + // plan can still proceed. + if (root) { + throw new IllegalArgumentException("Marketplace version is missing required '" + + FIELD_ITEM_ID + "'/'" + FIELD_ID + "' fields"); + } + String key = itemId != null ? itemId : versionId; + log.warn("Related IoT Hub item is missing required identifiers — recording in plan: {}", versionInfo); + entries.putIfAbsent(key != null ? key : versionInfo.toString(), + missingEntry(itemId, "Item descriptor is incomplete")); + return; + } + if (entries.containsKey(itemId)) { + return; + } + + InstallPlanEntry entry = new InstallPlanEntry(); + entry.setItemId(itemId); + entry.setVersionId(versionId); + entry.setName(optText(versionInfo, FIELD_NAME)); + entry.setType(optText(versionInfo, FIELD_TYPE)); + entry.setVersion(optText(versionInfo, FIELD_VERSION)); + entry.setRoot(root); + entry.setStatus(isAlreadyInstalled(entry, alreadyInstalledItemIds) + ? InstallPlanEntry.Status.ALREADY_INSTALLED + : InstallPlanEntry.Status.WILL_INSTALL); + entries.put(itemId, entry); + } + + private static InstallPlanEntry missingEntry(String itemId, String errorMessage) { + InstallPlanEntry entry = new InstallPlanEntry(); + entry.setItemId(itemId); + entry.setStatus(InstallPlanEntry.Status.MISSING); + entry.setErrorMessage(errorMessage); + return entry; + } + + private static String optText(JsonNode node, String field) { + return node != null && node.hasNonNull(field) ? node.get(field).asText() : null; + } + + @Override + public InstallPlanResult installPlan(SecurityUser user, InstallPlan plan, JsonNode data, HttpServletRequest request) { + TenantId tenantId = user.getTenantId(); + if (plan == null || plan.getEntries() == null || plan.getEntries().isEmpty()) { + return new InstallPlanResult(false, false, "Install plan is empty", null, new ArrayList<>(), new ArrayList<>()); + } + + // The plan is resolved by a separate request, so by the time it is submitted an item may + // already have been installed (stale or replayed plan). Re-check the current state for only + // the items this plan would install, so we never install the same item twice. + Set candidateItemIds = new HashSet<>(); + for (InstallPlanEntry entry : plan.getEntries()) { + if (entry.getStatus() == InstallPlanEntry.Status.WILL_INSTALL) { + addCandidateItemId(candidateItemIds, entry.getItemId()); + } + } + // Mutable on purpose: as the cascade installs each item we add its id below, so a later + // duplicate entry for the same item in this plan is recognised as installed and skipped + // instead of installed twice. A client-supplied plan is not de-duplicated the way a freshly + // resolved one is, so the loop has to guard against duplicates itself. + Set alreadyInstalledItemIds = new HashSet<>(); + if (!candidateItemIds.isEmpty()) { + alreadyInstalledItemIds.addAll(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(tenantId, candidateItemIds)); + } + + InstallPlanResult result = new InstallPlanResult(); + List resultEntries = new ArrayList<>(); + List missingItemIds = new ArrayList<>(); + List rollbackIds = new ArrayList<>(); + IotHubInstalledItemDescriptor rootDescriptor = null; + + for (InstallPlanEntry entry : plan.getEntries()) { + InstallPlanEntry resultEntry = cloneEntry(entry); + switch (entry.getStatus()) { + case MISSING -> { + missingItemIds.add(entry.getItemId()); + resultEntries.add(resultEntry); + } + case ALREADY_INSTALLED -> resultEntries.add(resultEntry); + case WILL_INSTALL -> { + if (isAlreadyInstalled(entry, alreadyInstalledItemIds)) { + // Installed since the plan was resolved — skip rather than create a duplicate. + resultEntry.setStatus(InstallPlanEntry.Status.ALREADY_INSTALLED); + resultEntries.add(resultEntry); + break; + } + try { + // Only the root entry receives the user's install data (target profile entityId); + // transitive deps install with defaults. + JsonNode entryData = entry.isRoot() ? data : null; + IotHubInstalledItem installed = doInstallVersion(user, entry.getVersionId(), entryData, request); + rollbackIds.add(installed.getId()); + // Record it as installed so a duplicate entry for the same item later in this + // plan is skipped rather than installed a second time. + addCandidateItemId(alreadyInstalledItemIds, entry.getItemId()); + if (entry.isRoot()) { + rootDescriptor = installed.getDescriptor(); + } + resultEntries.add(resultEntry); + } catch (Exception e) { + log.error("[{}] Cascade install failed at entry {} ({}): {}", tenantId, + entry.getName(), entry.getVersionId(), e.getMessage(), e); + resultEntry.setErrorMessage(e.getMessage()); + resultEntries.add(resultEntry); + boolean rolledBack = rollbackInstalledItems(user, rollbackIds); + result.setSuccess(false); + result.setRolledBack(rolledBack); + result.setErrorMessage("Failed to install '" + entry.getName() + "': " + e.getMessage()); + result.setEntries(resultEntries); + result.setMissingItemIds(missingItemIds); + return result; + } + } + } + } + + result.setSuccess(true); + result.setRolledBack(false); + result.setRootDescriptor(rootDescriptor); + result.setEntries(resultEntries); + result.setMissingItemIds(missingItemIds); + return result; + } + + /** + * Deletes the supplied installed items in reverse install order. Returns {@code true} only if + * every deletion succeeded — a {@code false} result means some entities were left behind and + * the rollback is partial. + */ + private boolean rollbackInstalledItems(SecurityUser user, List installedIds) { + boolean fullyRolledBack = true; + for (int i = installedIds.size() - 1; i >= 0; i--) { + IotHubInstalledItemId id = installedIds.get(i); + try { + deleteInstalledItem(user, id); + } catch (Exception e) { + fullyRolledBack = false; + log.error("[{}] Failed to roll back installed item {}: {}", user.getTenantId(), id, e.getMessage(), e); + } + } + return fullyRolledBack; + } + + private static void addCandidateItemId(Set candidates, String itemId) { + if (itemId == null || itemId.isEmpty()) { + return; + } + try { + candidates.add(UUID.fromString(itemId)); + } catch (IllegalArgumentException ignored) { + // A non-UUID item id can never match an installed record — leave it out of the lookup. + } + } + + private static boolean isAlreadyInstalled(InstallPlanEntry entry, Set alreadyInstalledItemIds) { + if (entry == null || entry.getItemId() == null) { + return false; + } + if (entry.isRoot() && (!"WIDGET".equals(entry.getType()) && !"SOLUTION_TEMPLATE".equals(entry.getType()))) { + return false; + } + try { + return alreadyInstalledItemIds.contains(UUID.fromString(entry.getItemId())); + } catch (IllegalArgumentException ex) { + return false; + } + } + + private static InstallPlanEntry cloneEntry(InstallPlanEntry src) { + return new InstallPlanEntry(src.getItemId(), src.getVersionId(), src.getName(), src.getType(), + src.getVersion(), src.getStatus(), src.isRoot(), src.getErrorMessage()); + } + + private static Exception parseFailure(String action, String itemTypeName, Exception cause) { + return parseFailure(action, itemTypeName, SECTION_PACKAGE_DATA, cause); + } + + private static Exception parseFailure(String action, String itemTypeName, String section, Exception cause) { + return new Exception("Unable to " + action + " this " + itemTypeName + " — the " + section + " could not be parsed. " + + "The package may be corrupted or in an unexpected format; please contact the creator to have it re-published.", cause); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallItemVersionResult.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallItemVersionResult.java new file mode 100644 index 0000000000..f91085cfd6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallItemVersionResult.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemDescriptor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class InstallItemVersionResult { + + private boolean success; + private String errorMessage; + private IotHubInstalledItemDescriptor descriptor; + + public static InstallItemVersionResult success(IotHubInstalledItemDescriptor descriptor) { + return new InstallItemVersionResult(true, null, descriptor); + } + + public static InstallItemVersionResult error(String errorMessage) { + return new InstallItemVersionResult(false, errorMessage, null); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlan.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlan.java new file mode 100644 index 0000000000..12c268a6b4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlan.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class InstallPlan { + + private String rootVersionId; + private List entries; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanEntry.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanEntry.java new file mode 100644 index 0000000000..c073ff8e0e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanEntry.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class InstallPlanEntry { + + public enum Status { + WILL_INSTALL, + ALREADY_INSTALLED, + MISSING + } + + private String itemId; + private String versionId; + private String name; + private String type; + private String version; + private Status status; + private boolean root; + private String errorMessage; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanResult.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanResult.java new file mode 100644 index 0000000000..2c3f49e071 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallPlanResult.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemDescriptor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class InstallPlanResult { + + private boolean success; + private boolean rolledBack; + private String errorMessage; + private IotHubInstalledItemDescriptor rootDescriptor; + private List entries = new ArrayList<>(); + private List missingItemIds = new ArrayList<>(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java new file mode 100644 index 0000000000..c67fef55a8 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java @@ -0,0 +1,128 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.time.Duration; + +@Component +@TbCoreComponent +@Slf4j +public class IotHubRestClient { + + @Value("${iot-hub.base-url:https://iot-hub.thingsboard.io}") + private String baseUrl; + + @Value("${iot-hub.connect-timeout-sec:5}") + private int connectTimeoutSec; + + @Value("${iot-hub.read-timeout-sec:10}") + private int readTimeoutSec; + + @Value("${iot-hub.max-file-data-size-bytes:104857600}") + private long maxFileDataSizeBytes; + + private RestTemplate restTemplate; + + @PostConstruct + public void initRestClient() { + restTemplate = new RestTemplateBuilder() + .connectTimeout(Duration.ofSeconds(connectTimeoutSec)) + .readTimeout(Duration.ofSeconds(readTimeoutSec)) + .build(); + } + + public JsonNode getVersionInfo(String versionId) { + String url = baseUrl + "/api/versions/" + versionId; + log.debug("Fetching IoT Hub version info: {}", url); + return restTemplate.getForObject(url, JsonNode.class); + } + + /** + * Returns the latest published version of an item, or {@code null} if the marketplace + * responds with 404 (item missing or never published). Other 4xx/5xx still propagate. + */ + public JsonNode getPublishedVersionByItemId(String itemId) { + String url = baseUrl + "/api/items/" + itemId + "/published"; + log.debug("Fetching IoT Hub published version for item: {}", url); + try { + return restTemplate.getForObject(url, JsonNode.class); + } catch (HttpClientErrorException e) { + if (e.getStatusCode() == HttpStatus.NOT_FOUND) { + return null; + } + throw e; + } + } + + public byte[] getVersionFileData(String versionId) { + String url = baseUrl + "/api/versions/" + versionId + "/fileData"; + log.debug("Fetching IoT Hub version file data: {}", url); + // Stream the response so we can reject oversized payloads before + // they get loaded fully into memory: + // 1) refuse early if the server advertises a Content-Length + // larger than the configured cap; + // 2) refuse mid-stream once we have already buffered more than + // the cap (for chunked transfers without Content-Length). + final long limit = maxFileDataSizeBytes; + return restTemplate.execute(url, HttpMethod.GET, null, response -> { + long contentLength = response.getHeaders().getContentLength(); + if (contentLength > limit) { + throw new IllegalStateException("IoT Hub file data size " + contentLength + + " bytes exceeds the configured limit of " + limit + " bytes"); + } + int initialCapacity = contentLength > 0 && contentLength <= Integer.MAX_VALUE + ? (int) contentLength : 8192; + try (InputStream in = response.getBody(); + ByteArrayOutputStream out = new ByteArrayOutputStream(initialCapacity)) { + byte[] buffer = new byte[8192]; + long total = 0; + int read; + while ((read = in.read(buffer)) != -1) { + total += read; + if (total > limit) { + throw new IllegalStateException("IoT Hub file data stream exceeded the configured limit of " + + limit + " bytes"); + } + out.write(buffer, 0, read); + } + return out.toByteArray(); + } catch (IOException e) { + throw new IllegalStateException("Failed to read IoT Hub file data", e); + } + }); + } + + public void reportVersionInstalled(String versionId) { + String url = baseUrl + "/api/versions/" + versionId + "/install"; + log.debug("Reporting IoT Hub version installed: {}", url); + restTemplate.postForObject(url, null, Void.class); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java new file mode 100644 index 0000000000..fac0f94b10 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubService.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.servlet.http.HttpServletRequest; +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; +import org.thingsboard.server.common.data.iot_hub.DeviceInstalledItemDescriptor; +import org.thingsboard.server.service.security.model.SecurityUser; + +public interface IotHubService { + + InstallItemVersionResult installItemVersion(SecurityUser user, String versionId, JsonNode data, HttpServletRequest request); + + UpdateItemVersionResult updateItemVersion(SecurityUser user, IotHubInstalledItemId installedItemId, String versionId, boolean force, HttpServletRequest request); + + InstallItemVersionResult registerDeviceInstall(SecurityUser user, String versionId, DeviceInstalledItemDescriptor descriptor); + + void deleteInstalledItem(SecurityUser user, IotHubInstalledItemId installedItemId); + + InstallPlan resolveInstallPlan(SecurityUser user, String versionId); + + InstallPlanResult installPlan(SecurityUser user, InstallPlan plan, JsonNode data, HttpServletRequest request); +} diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/UpdateItemVersionResult.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/UpdateItemVersionResult.java new file mode 100644 index 0000000000..1ba7b0a7b5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/UpdateItemVersionResult.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemDescriptor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UpdateItemVersionResult { + + private boolean success; + private boolean entityModified; + private String errorMessage; + private IotHubInstalledItemDescriptor descriptor; + + public static UpdateItemVersionResult success(IotHubInstalledItemDescriptor descriptor) { + return new UpdateItemVersionResult(true, false, null, descriptor); + } + + public static UpdateItemVersionResult entityModified() { + return new UpdateItemVersionResult(false, true, null, null); + } + + public static UpdateItemVersionResult error(String errorMessage) { + return new UpdateItemVersionResult(false, false, errorMessage, null); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java new file mode 100644 index 0000000000..17a018bdfa --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java @@ -0,0 +1,1715 @@ +/** + * Copyright © 2016-2026 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.solutions; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.gson.JsonParser; +import jakarta.annotation.PreDestroy; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.adaptor.JsonConverter; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.iot_hub.SolutionTemplateInstalledItemDescriptor; +import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; +import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.debug.DebugSettings; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.UserId; +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.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.asset.AssetProfileService; +import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.cf.CalculatedFieldService; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.device.DeviceConnectivityService; +import org.thingsboard.server.dao.device.DockerComposeParams; +import org.thingsboard.server.dao.device.DeviceCredentialsService; +import org.thingsboard.server.dao.device.DeviceProfileService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.exception.EntitiesLimitExceededException; +import org.thingsboard.server.exception.ThingsboardRuntimeException; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.provider.TbQueueProducerProvider; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.action.EntityActionService; +import org.thingsboard.server.service.entitiy.asset.TbAssetService; +import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; +import org.thingsboard.server.service.entitiy.device.TbDeviceService; +import org.thingsboard.server.service.entitiy.edge.TbEdgeService; +import org.thingsboard.server.service.entitiy.entity.relation.TbEntityRelationService; +import org.thingsboard.server.service.rule.TbRuleChainService; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.system.SystemSecurityService; +import org.thingsboard.server.service.solutions.data.CreatedAlarmRuleInfo; +import org.thingsboard.server.service.solutions.data.CreatedCalculatedFieldInfo; +import org.thingsboard.server.service.solutions.data.CreatedEntityInfo; +import org.thingsboard.server.service.solutions.data.DashboardLinkInfo; +import org.thingsboard.server.service.solutions.data.DeviceCredentialsInfo; +import org.thingsboard.server.service.solutions.data.EdgeLinkInfo; +import org.thingsboard.server.service.solutions.data.SolutionInstallContext; +import org.thingsboard.server.service.solutions.data.UserCredentialsInfo; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.thingsboard.server.service.solutions.data.definition.AssetDefinition; +import org.thingsboard.server.service.solutions.data.definition.AssetProfileDefinition; +import org.thingsboard.server.service.solutions.data.definition.DeviceDefinition; +import org.thingsboard.server.service.solutions.data.definition.CustomerDefinition; +import org.thingsboard.server.service.solutions.data.definition.DashboardUserDetailsDefinition; +import org.thingsboard.server.service.solutions.data.definition.CalculatedFieldDefinition; +import org.thingsboard.server.service.solutions.data.definition.EmulatorDefinition; +import org.thingsboard.server.service.solutions.data.definition.UserDefinition; +import org.thingsboard.server.service.solutions.data.definition.DashboardDefinition; +import org.thingsboard.server.service.solutions.data.definition.DeviceProfileDefinition; +import org.thingsboard.server.service.solutions.data.definition.EdgeDefinition; +import org.thingsboard.server.service.solutions.data.definition.RelationDefinition; +import org.thingsboard.server.service.solutions.data.definition.ReferenceableEntityDefinition; +import org.thingsboard.server.service.solutions.data.definition.TenantDefinition; +import org.thingsboard.server.service.solutions.data.emulator.AssetEmulatorLauncher; +import org.thingsboard.server.service.solutions.data.emulator.DeviceEmulatorLauncher; +import org.thingsboard.server.service.solutions.data.names.RandomNameData; +import org.thingsboard.server.service.solutions.data.names.RandomNameUtil; +import org.thingsboard.server.service.solutions.data.solution.SolutionInstallResponse; +import org.thingsboard.server.service.solutions.data.solution.TenantSolutionTemplateInstructions; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ExecutorService; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +@Service +@TbCoreComponent +@RequiredArgsConstructor +@Slf4j +public class DefaultSolutionService implements SolutionService { + + @Value("${ui.solution_templates.docs_base_url:https://thingsboard.io/docs}") + private String docsBaseUrl; + + @Value("${iot-hub.max-uncompressed-archive-bytes:209715200}") + private long maxUncompressedArchiveBytes; + + @Value("${iot-hub.max-uncompressed-entry-bytes:52428800}") + private long maxUncompressedEntryBytes; + + @Value("${iot-hub.max-archive-entry-count:10000}") + private int maxArchiveEntryCount; + + @Value("${iot-hub.max-install-timeout-ms:60000}") + private long maxInstallTimeoutMs; + + private final RuleChainService ruleChainService; + private final TbRuleChainService tbRuleChainService; + private final DeviceProfileService deviceProfileService; + private final DeviceService deviceService; + private final TbDeviceService tbDeviceService; + private final DeviceCredentialsService deviceCredentialsService; + private final AssetProfileService assetProfileService; + private final AssetService assetService; + private final TbAssetService tbAssetService; + private final CustomerService customerService; + private final UserService userService; + private final DashboardService dashboardService; + private final EdgeService edgeService; + private final TbEdgeService tbEdgeService; + private final TbEntityRelationService relationService; + private final AlarmService alarmService; + private final CalculatedFieldService calculatedFieldService; + private final TbCalculatedFieldService tbCalculatedFieldService; + private final AttributesService attributesService; + private final TimeseriesService tsService; + private final EntityActionService entityActionService; + private final SystemSecurityService systemSecurityService; + private final TbClusterService tbClusterService; + private final PartitionService partitionService; + private final TbQueueProducerProvider tbQueueProducerProvider; + private final TbServiceInfoProvider serviceInfoProvider; + private final TelemetrySubscriptionService tsSubService; + private final BCryptPasswordEncoder passwordEncoder; + private final DeviceConnectivityService deviceConnectivityService; + + private final ExecutorService emulatorExecutor = ThingsBoardExecutors.newWorkStealingPool(10, "solution-emulators-executor"); + + @PreDestroy + private void destroy() { + emulatorExecutor.shutdownNow(); + } + + @Override + public SolutionInstallResponse installSolution(SecurityUser user, TenantId tenantId, byte[] zipData, HttpServletRequest request) throws Exception { + Path tempDir = Files.createTempDirectory("iot-hub-solution-"); + try { + try { + extractZip(zipData, tempDir); + } catch (Throwable e) { + log.error("[{}] Failed to extract solution template zip", tenantId, e); + deleteDirectory(tempDir); + TenantSolutionTemplateInstructions instructions = new TenantSolutionTemplateInstructions(); + instructions.setDetails(e.getMessage()); + return new SolutionInstallResponse(instructions, false, List.of()); + } + + String solutionId = loadSolutionId(tempDir); + if (solutionId == null) { + throw new IllegalArgumentException("Solution template is missing solution.json or its 'title' field"); + } + + SolutionInstallResponse validateResult = validateSolution(tenantId, tempDir); + if (validateResult != null && !validateResult.isSuccess()) { + return validateResult; + } + return doInstallSolution(user, tenantId, solutionId, tempDir, request); + } finally { + deleteDirectory(tempDir); + } + } + + @Override + public void deleteSolution(TenantId tenantId, SolutionTemplateInstalledItemDescriptor descriptor, SecurityUser user) throws ThingsboardException { + try { + if (descriptor.getCreatedEntityIds() != null && !descriptor.getCreatedEntityIds().isEmpty()) { + List entityIds = new ArrayList<>(descriptor.getCreatedEntityIds()); + // Delete in the descending order of creation to avoid dependency issues. + Collections.reverse(entityIds); + for (EntityId entityId : entityIds) { + try { + deleteEntity(tenantId, entityId, user); + } catch (RuntimeException e) { + log.error("[{}] Failed to delete the entity: {}", tenantId, entityId, e); + } + } + } + List tsKeys = descriptor.getTenantTelemetryKeys(); + if (tsKeys != null && !tsKeys.isEmpty()) { + List queries = new ArrayList<>(tsKeys.size()); + for (String tsKey : tsKeys) { + queries.add(new BaseDeleteTsKvQuery(tsKey, 0, System.currentTimeMillis(), false)); + } + tsService.remove(tenantId, tenantId, queries).get(); + } + List attrKeys = descriptor.getTenantAttributeKeys(); + if (attrKeys != null && !attrKeys.isEmpty()) { + attributesService.removeAll(tenantId, tenantId, AttributeScope.SERVER_SCOPE, attrKeys).get(); + } + } catch (Exception e) { + log.error("[{}] Failed to delete the solution", tenantId, e); + throw new ThingsboardException(e, ThingsboardErrorCode.GENERAL); + } + } + + private SolutionInstallResponse validateSolution(TenantId tenantId, Path tempDir) { + Map> alreadyExistingEntities = new HashMap<>(); + + //TODO: check other entities. + + List ruleChains = loadListOfEntitiesIfFileExists(tempDir, "rule_chains.json", new TypeReference<>() { + }); + if (!ruleChains.isEmpty()) { + for (ReferenceableEntityDefinition ruleChain : ruleChains) { + List savedRuleChains = ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, new PageLink(1, 0, ruleChain.getName())).getData(); + if (savedRuleChains != null && !savedRuleChains.isEmpty()) { + alreadyExistingEntities.computeIfAbsent(EntityType.RULE_CHAIN, key -> new ArrayList<>()).add(savedRuleChains.get(0)); + } + } + } + + List deviceProfiles = loadListOfEntitiesIfFileExists(tempDir, "device_profiles.json", new TypeReference<>() { + }); + deviceProfiles.addAll(loadListOfEntitiesFromDirectory(tempDir, "device_profiles", DeviceProfileDefinition.class)); + // Validate that entities with such name does not exist entities + if (!deviceProfiles.isEmpty()) { + for (DeviceProfile deviceProfile : deviceProfiles) { + DeviceProfile savedProfile = deviceProfileService.findDeviceProfileByName(tenantId, deviceProfile.getName()); + if (savedProfile != null) { + alreadyExistingEntities.computeIfAbsent(EntityType.DEVICE_PROFILE, key -> new ArrayList<>()).add(savedProfile); + } + } + } + + List assetProfiles = loadListOfEntitiesIfFileExists(tempDir, "asset_profiles.json", new TypeReference<>() { + }); + assetProfiles.addAll(loadListOfEntitiesFromDirectory(tempDir, "asset_profiles", AssetProfileDefinition.class)); + // Validate that entities with such name does not exist entities + if (!assetProfiles.isEmpty()) { + for (AssetProfile assetProfile : assetProfiles) { + AssetProfile savedProfile = assetProfileService.findAssetProfileByName(tenantId, assetProfile.getName()); + if (savedProfile != null) { + alreadyExistingEntities.computeIfAbsent(EntityType.ASSET_PROFILE, key -> new ArrayList<>()).add(savedProfile); + } + } + } + + List dashboards = loadListOfEntitiesIfFileExists(tempDir, "dashboards.json", new TypeReference<>() { + }); + if (!dashboards.isEmpty()) { + for (DashboardDefinition dashboard : dashboards) { + List savedDashboards = dashboardService.findDashboardsByTenantId(tenantId, new PageLink(1, 0, dashboard.getName())).getData(); + if (savedDashboards != null && !savedDashboards.isEmpty()) { + alreadyExistingEntities.computeIfAbsent(EntityType.DASHBOARD, key -> new ArrayList<>()).add(savedDashboards.get(0)); + } + } + } + if (!alreadyExistingEntities.isEmpty()) { + SolutionInstallResponse solutionInstallResponse = new SolutionInstallResponse(); + StringBuilder detailsBuilder = new StringBuilder(); + detailsBuilder.append("## Validation failed").append(System.lineSeparator()).append(System.lineSeparator()); + alreadyExistingEntities.forEach((type, list) -> detailsBuilder.append("The following **").append(getTypeLabel(type)).append("** entities already exist: ") + .append(list.stream().map(HasName::getName).map(name -> "'" + name + "'").collect(Collectors.joining(","))).append(";") + .append(System.lineSeparator()).append(System.lineSeparator())); + solutionInstallResponse.setSuccess(false); + solutionInstallResponse.setDetails(detailsBuilder.toString()); + return solutionInstallResponse; + } else { + return null; + } + } + + private SolutionInstallResponse doInstallSolution(User user, TenantId tenantId, String solutionId, Path tempDir, HttpServletRequest request) { + SolutionInstallContext ctx = new SolutionInstallContext(tenantId, solutionId, tempDir, user, new TenantSolutionTemplateInstructions()); + + try { + + registerEmulatorsAndComputeOldestTelemetryTs(ctx); + + provisionTenantDetails(ctx); + + provisionRuleChains(ctx); + + provisionDeviceProfiles(ctx); + + provisionAssetProfiles(ctx); + + List customers = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "customers.json", new TypeReference<>() {}); + + provisionCustomers(ctx, customers); + + var assets = provisionAssets(ctx); + + var devices = provisionDevices(ctx); + + provisionDashboards(ctx); + + provisionCustomerUsers(ctx, customers); + + provisionRelations(ctx); + + updateRuleChains(ctx); + + provisionEdges(ctx); + + provisionAlarmRules(ctx); + + Set> telemetryLoading = launchEmulators(ctx, devices, assets); + + waitForTelemetryCompletion(telemetryLoading); + + provisionCalculatedFields(ctx); + + ctx.getSolutionInstructions().setDetails(prepareInstructions(ctx, request)); + + List ruleChainDefs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "rule_chains.json", new TypeReference<>() {}); + if (ruleChainDefs.stream().anyMatch(r -> StringUtils.isNotEmpty(r.getUpdate()))) { + long timeout = Math.min(loadInstallTimeoutMs(ctx.getTempDir()), maxInstallTimeoutMs); + if (timeout > 0) { + Thread.sleep(timeout); + } + finalUpdateRuleChains(ctx); + } + + log.info("[{}] Solution template installed, created {} entities", tenantId, ctx.getCreatedEntitiesList().size()); + + return new SolutionInstallResponse( + new TenantSolutionTemplateInstructions(ctx.getSolutionInstructions()), + true, + ctx.getCreatedEntitiesList(), + loadTenantTelemetryKeys(ctx.getTempDir()), + loadTenantAttributeKeys(ctx.getTempDir()) + ); + } catch (Throwable e) { + log.error("[{}][{}] Failed to install solution template", tenantId, solutionId, e); + rollback(tenantId, solutionId, ctx, e); + if (e instanceof EntitiesLimitExceededException el) { + throw el; + } + return new SolutionInstallResponse( + new TenantSolutionTemplateInstructions(ctx.getSolutionInstructions()), + false, + ctx.getCreatedEntitiesList() + ); + } + } + + private void waitForTelemetryCompletion(Set> futures) throws InterruptedException { + CompletableFuture all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + try { + all.get(); + Thread.sleep(futures.size() * 100L); + } catch (ExecutionException e) { + throw new RuntimeException("Telemetry processing failed", e.getCause()); + } + } + + private void rollback(TenantId tenantId, String solutionId, SolutionInstallContext ctx, Throwable e) { + List createdEntities = new ArrayList<>(ctx.getCreatedEntitiesList()); + Collections.reverse(createdEntities); + for (EntityId entityId : createdEntities) { + try { + deleteEntity(tenantId, entityId, ctx.getUser()); + } catch (RuntimeException re) { + log.error("[{}][{}] Failed to delete the entity: {}", tenantId, solutionId, entityId, re); + } + } + ctx.getCreatedEntitiesList().clear(); + ctx.getSolutionInstructions().setDetails(e.getMessage()); + } + + private String prepareInstructions(SolutionInstallContext ctx, HttpServletRequest request) { + + Path instructionsFile = ctx.getTempDir().resolve("instructions.md"); + if (!Files.exists(instructionsFile)) { + return null; + } + String template; + try { + template = Files.readString(instructionsFile); + } catch (IOException e) { + log.warn("[{}] Failed to read instructions.md", ctx.getTenantId(), e); + return null; + } + + String baseUrl = systemSecurityService.getBaseUrl(ctx.getTenantId(), null, request); + + // Inject edge instructions first, then run the full replacement logic on the combined string + if (template.contains("${edge_instructions}")) { + if (ctx.getCreatedEdges().isEmpty()) { + template = template.replace("${edge_instructions}", ""); + } else { + Path edgeFile = ctx.getTempDir().resolve("edge_instructions.md"); + String edgeTemplate = Files.exists(edgeFile) ? readFileContent(edgeFile) : ""; + template = template.replace("${edge_instructions}", edgeTemplate); + } + } + + template = template.replace("${DOCS_BASE_URL}", docsBaseUrl); + template = template.replace("${BASE_URL}", baseUrl); + + TenantSolutionTemplateInstructions solutionInstructions = ctx.getSolutionInstructions(); + + if (solutionInstructions.getDashboardId() != null) { + template = template.replace("${MAIN_DASHBOARD_URL}", + getDashboardLink(solutionInstructions, solutionInstructions.getDashboardId(), false)); + if (solutionInstructions.isMainDashboardPublic()) { + template = template.replace("${MAIN_DASHBOARD_PUBLIC_URL}", + getDashboardLink(solutionInstructions, solutionInstructions.getDashboardId(), true)); + } + } + + for (DashboardLinkInfo dashboardLinkInfo : ctx.getDashboardLinks()) { + template = template.replace("${" + dashboardLinkInfo.getName() + "DASHBOARD_URL}", + getDashboardLink(solutionInstructions, dashboardLinkInfo.getDashboardId(), false)); + if (dashboardLinkInfo.isPublic()) { + template = template.replace("${" + dashboardLinkInfo.getName() + "DASHBOARD_PUBLIC_URL}", + getDashboardLink(solutionInstructions, dashboardLinkInfo.getDashboardId(), true)); + } + } + + if (template.contains("${GATEWAYS_URL}")) { + template = template.replace("${GATEWAYS_URL}", "/gateways"); + } + + // Device list and credentials + StringBuilder devList = new StringBuilder(); + devList.append("| Device name | Access token | Owner |"); + devList.append(System.lineSeparator()); + devList.append("| :--- | :--- | :--- |"); + devList.append(System.lineSeparator()); + + for (DeviceCredentialsInfo credentialsInfo : ctx.getCreatedDevices().values()) { + devList.append("|").append(credentialsInfo.getName()) + .append("|").append(credentialsInfo.getCredentials().getCredentialsId()).append("{:copy-code}") + .append("|").append(credentialsInfo.getCustomerName() != null ? credentialsInfo.getCustomerName() : "Tenant"); + devList.append(System.lineSeparator()); + + template = template.replace("${" + credentialsInfo.getName() + "ACCESS_TOKEN}", credentialsInfo.getCredentials().getCredentialsId()); + + if (credentialsInfo.isGateway()) { + template = template.replace("${DOCKER_CONFIG}", + prepareDockerComposeFile(ctx.getTenantId(), ctx.getSolutionId(), baseUrl, credentialsInfo.getCredentials().getDeviceId())); + } + } + + template = template.replace("${device_list_and_credentials}", devList.toString()); + + // User list (without user group column) + StringBuilder userList = new StringBuilder(); + userList.append("| Name | Login | Password | Customer name |"); + userList.append(System.lineSeparator()); + userList.append("| :--- | :--- | :--- | :--- |"); + userList.append(System.lineSeparator()); + + for (UserCredentialsInfo credentialsInfo : ctx.getCreatedUsers().values()) { + userList.append("|").append(credentialsInfo.getName()) + .append("|").append(credentialsInfo.getLogin()).append("{:copy-code}") + .append("|").append(credentialsInfo.getPassword()).append("{:copy-code}") + .append("|").append(credentialsInfo.getCustomerName() != null ? credentialsInfo.getCustomerName() : ""); + userList.append(System.lineSeparator()); + } + + template = template.replace("${user_list}", userList.toString()); + + // Edge detail URLs + for (Map.Entry edgeLinkInfoEntry : ctx.getCreatedEdges().entrySet()) { + EdgeLinkInfo edgeLinkInfo = edgeLinkInfoEntry.getValue(); + StringBuilder edgeDetailsUrl = new StringBuilder(); + if (EntityType.CUSTOMER.equals(edgeLinkInfo.getOwnerId().getEntityType())) { + edgeDetailsUrl.append("/customers/").append(edgeLinkInfo.getOwnerId().getId()); + edgeDetailsUrl.append("/edgeInstances/").append(edgeLinkInfo.getEdgeId().getId()); + } else { + edgeDetailsUrl.append("/edgeManagement/instances/").append(edgeLinkInfo.getEdgeId().getId()); + } + String edgeName = edgeLinkInfoEntry.getKey(); + String edgeDetailsPlaceholder = "${" + edgeName + "EDGE_DETAILS_URL}"; + template = template.replace(edgeDetailsPlaceholder, edgeDetailsUrl.toString()); + } + + template = replaceAlarmRules(ctx, template); + template = replaceCalculatedFields(ctx, template); + template = replaceCreatedEntities(ctx, template); + + return template; + } + + private static String replaceAlarmRules(SolutionInstallContext ctx, String template) { + StringBuilder alarmRules = new StringBuilder(); + + alarmRules.append("| Entity Profile Name | Alarm Type | Severities |").append(System.lineSeparator()); + alarmRules.append("| :--- | :--- | :--- |").append(System.lineSeparator()); + + ctx.getCreatedAlarmRules().entrySet().stream() + .sorted(Map.Entry.comparingByValue(Comparator.comparing(CreatedAlarmRuleInfo::entityName, String.CASE_INSENSITIVE_ORDER) + .thenComparing(CreatedAlarmRuleInfo::alarmType, String.CASE_INSENSITIVE_ORDER))) + .forEach(entry -> { + UUID key = entry.getKey(); + var alarmRuleInfo = entry.getValue(); + + String alarmType = alarmRuleInfo.alarmType(); + String link = alarmRuleInfo.getCfPageLink(key); + + String alarmTypeWithLink = "" + alarmType + ""; + + String profileName = alarmRuleInfo.entityId() != null ? + "" + alarmRuleInfo.entityName() + "" + : alarmRuleInfo.entityName(); + + alarmRules.append("|") + .append(profileName).append("|") + .append(alarmTypeWithLink).append("|") + .append(alarmRuleInfo.severities()).append("|") + .append(System.lineSeparator()); + }); + + return template.replace("${alarm_rules}", alarmRules.toString()); + } + + private static String replaceCalculatedFields(SolutionInstallContext ctx, String template) { + StringBuilder calculatedFields = new StringBuilder(); + + calculatedFields.append("| Entity Profile Name | Field Name | Field Type |").append(System.lineSeparator()); + calculatedFields.append("| :--- | :--- | :--- |").append(System.lineSeparator()); + + ctx.getCreatedCalculatedFields().entrySet().stream() + .sorted(Map.Entry.comparingByValue( + Comparator.comparing(CreatedCalculatedFieldInfo::entityName, String.CASE_INSENSITIVE_ORDER) + .thenComparing(CreatedCalculatedFieldInfo::name, String.CASE_INSENSITIVE_ORDER) + )) + .forEach(entry -> { + UUID key = entry.getKey(); + var cfInfo = entry.getValue(); + + String cfTitle = cfInfo.name(); + String link = cfInfo.getCfPageLink(key); + + String cfTitleWithLink = "" + cfTitle + ""; + + String profileName = cfInfo.entityId() != null ? + "" + cfInfo.entityName() + "" + : cfInfo.entityName(); + + calculatedFields.append("|") + .append(profileName).append("|") + .append(cfTitleWithLink).append("|") + .append(cfInfo.type()).append("|") + .append(System.lineSeparator()); + }); + + return template.replace("${calculated_fields}", calculatedFields.toString()); + } + + private static String replaceCreatedEntities(SolutionInstallContext ctx, String template) { + StringBuilder entityList = new StringBuilder(); + + entityList.append("| Name | Type | Owner |").append(System.lineSeparator()); + entityList.append("| :--- | :--- | :--- |").append(System.lineSeparator()); + + for (Map.Entry entry : ctx.getCreatedEntities().entrySet()) { + UUID key = entry.getKey(); + var entityInfo = entry.getValue(); + String link = entityInfo.getEntityPageLink(key); + String entityName = entityInfo.getName(); + + String name = link != null ? + "" + entityName + "" + : entityName; + + entityList.append("|") + .append(name).append("|") + .append(entityInfo.getType().getNormalName()).append("|") + .append(entityInfo.getOwner()).append("|") + .append(System.lineSeparator()); + } + return template.replace("${all_entities}", entityList.toString()); + } + + private String getDashboardLink(TenantSolutionTemplateInstructions solutionInstructions, DashboardId dashboardId, boolean isPublic) { + if (isPublic && solutionInstructions.getPublicId() != null) { + return "/dashboard/" + dashboardId.getId() + "?publicId=" + solutionInstructions.getPublicId(); + } + return "/dashboards/" + dashboardId.getId(); + } + + private String prepareDockerComposeFile(TenantId tenantId, String solutionId, String baseUrl, DeviceId deviceId) { + Device device = new Device(deviceId); + device.setTenantId(tenantId); + String containerName = "tb-gateway-" + solutionId.replace('_', '-'); + DockerComposeParams params = new DockerComposeParams(false, containerName, false, true, false, false); + try (InputStream inputStream = deviceConnectivityService.createGatewayDockerComposeFile(baseUrl, device, params).getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)) + ) { + return reader.lines().collect(Collectors.joining("\n")); + } catch (Exception e) { + throw new RuntimeException("Failed to read or process the docker-compose.yml file.", e); + } + } + + private void provisionRuleChains(SolutionInstallContext ctx) { + List ruleChainDefs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "rule_chains.json", new TypeReference<>() {}); + for (ReferenceableEntityDefinition entityDef : ruleChainDefs) { + Path ruleChainPath = ctx.getTempDir().resolve("rule_chains").resolve(entityDef.getFile()); + if (!Files.exists(ruleChainPath)) { + log.warn("[{}] Rule chain file not found: {}", ctx.getTenantId(), entityDef.getFile()); + continue; + } + JsonNode ruleChainJson = replaceIds(ctx, JacksonUtil.toJsonNode(ruleChainPath)); + + RuleChain ruleChain = JacksonUtil.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class); + ruleChain.setId(null); + ruleChain.setTenantId(ctx.getTenantId()); + String metadataStr = JacksonUtil.toString(ruleChainJson.get("metadata")); + RuleChainMetaData metadata = JacksonUtil.treeToValue(JacksonUtil.toJsonNode(metadataStr), RuleChainMetaData.class); + + RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain); + metadata.setRuleChainId(savedRuleChain.getId()); + metadata.setVersion(savedRuleChain.getVersion()); + ruleChainService.saveRuleChainMetaData(ctx.getTenantId(), metadata, tbRuleChainService::updateRuleNodeConfiguration); + if (ruleChain.isRoot()) { + ruleChainService.setRootRuleChain(ctx.getTenantId(), savedRuleChain.getId()); + } + + ctx.register(entityDef.getJsonId(), savedRuleChain); + log.debug("[{}] Rule chain provisioned: {}", ctx.getTenantId(), savedRuleChain.getName()); + } + } + + private void updateRuleChains(SolutionInstallContext ctx) { + List ruleChainDefs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "rule_chains.json", new TypeReference<>() {}); + for (ReferenceableEntityDefinition entityDef : ruleChainDefs) { + Path ruleChainPath = ctx.getTempDir().resolve("rule_chains").resolve(entityDef.getFile()); + if (!Files.exists(ruleChainPath)) { + continue; + } + String realId = ctx.getRealIds().get(entityDef.getJsonId()); + if (realId == null) { + continue; + } + RuleChainId ruleChainId = new RuleChainId(UUID.fromString(realId)); + RuleChain savedRuleChain = ruleChainService.findRuleChainById(ctx.getTenantId(), ruleChainId); + if (savedRuleChain == null) { + continue; + } + JsonNode ruleChainJson = JacksonUtil.toJsonNode(ruleChainPath); + String metadataStr = JacksonUtil.toString(ruleChainJson.get("metadata")); + String oldMetadataStr = metadataStr; + for (var entry : ctx.getRealIds().entrySet()) { + metadataStr = metadataStr.replace(entry.getKey(), entry.getValue()); + } + if (metadataStr.equals(oldMetadataStr)) { + continue; + } + RuleChainMetaData metadata = JacksonUtil.treeToValue(JacksonUtil.toJsonNode(metadataStr), RuleChainMetaData.class); + metadata.setRuleChainId(ruleChainId); + metadata.setVersion(savedRuleChain.getVersion()); + ruleChainService.saveRuleChainMetaData(ctx.getTenantId(), metadata, tbRuleChainService::updateRuleNodeConfiguration); + } + } + + private void finalUpdateRuleChains(SolutionInstallContext ctx) { + List ruleChains = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "rule_chains.json", new TypeReference<>() {}); + for (ReferenceableEntityDefinition entityDefinition : ruleChains) { + if (StringUtils.isEmpty(entityDefinition.getUpdate())) { + continue; + } + Path ruleChainPath = ctx.getTempDir().resolve("rule_chains").resolve(entityDefinition.getUpdate()); + JsonNode ruleChainJson = JacksonUtil.toJsonNode(ruleChainPath); + RuleChain ruleChain = JacksonUtil.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class); + ruleChain.setTenantId(ctx.getTenantId()); + String metadataStr = JacksonUtil.toString(ruleChainJson.get("metadata")); + for (var entry : ctx.getRealIds().entrySet()) { + metadataStr = metadataStr.replace(entry.getKey(), entry.getValue()); + } + RuleChainMetaData ruleChainMetaData = JacksonUtil.treeToValue(JacksonUtil.toJsonNode(metadataStr), RuleChainMetaData.class); + + String realRuleChainId = ctx.getRealIds().get(entityDefinition.getJsonId()); + if (StringUtils.isEmpty(realRuleChainId)) { + continue; + } + RuleChainId ruleChainId = new RuleChainId(UUID.fromString(realRuleChainId)); + RuleChain savedRuleChain = ruleChainService.findRuleChainById(ctx.getTenantId(), ruleChainId); + ruleChainMetaData.setRuleChainId(savedRuleChain.getId()); + ruleChainMetaData.setVersion(savedRuleChain.getVersion()); + ruleChainService.saveRuleChainMetaData(ctx.getTenantId(), ruleChainMetaData, tbRuleChainService::updateRuleNodeConfiguration); + } + } + + private void provisionDeviceProfiles(SolutionInstallContext ctx) { + List deviceProfiles = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "device_profiles.json", new TypeReference<>() {}); + deviceProfiles.addAll(loadListOfEntitiesFromDirectory(ctx.getTempDir(), "device_profiles", DeviceProfileDefinition.class)); + deviceProfiles.forEach(deviceProfile -> { + deviceProfile.setId(null); + deviceProfile.setCreatedTime(0L); + deviceProfile.setTenantId(ctx.getTenantId()); + if (deviceProfile.getDefaultRuleChainId() != null) { + String newId = ctx.getRealIds().get(deviceProfile.getDefaultRuleChainId().getId().toString()); + if (newId != null) { + deviceProfile.setDefaultRuleChainId(new RuleChainId(UUID.fromString(newId))); + } else { + log.error("[{}] Device profile: {} references non existing rule chain.", ctx.getTenantId(), deviceProfile.getName()); + throw new RuntimeException("Device profile: " + deviceProfile.getName() + " references non existing rule chain."); + } + } + if (deviceProfile.getDefaultEdgeRuleChainId() != null) { + String newId = ctx.getRealIds().get(deviceProfile.getDefaultEdgeRuleChainId().getId().toString()); + if (StringUtils.isEmpty(newId)) { + deviceProfile.setDefaultEdgeRuleChainId(null); + } else { + deviceProfile.setDefaultEdgeRuleChainId(new RuleChainId(UUID.fromString(newId))); + } + } + if (deviceProfile.getDefaultDashboardId() != null) { + String newId = ctx.getRealIds().get(deviceProfile.getDefaultDashboardId().getId().toString()); + if (newId != null) { + deviceProfile.setDefaultDashboardId(new DashboardId(UUID.fromString(newId))); + } + } + }); + + deviceProfiles.forEach(deviceProfileDefinition -> { + DeviceProfile deviceProfile = new DeviceProfile(deviceProfileDefinition); + deviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + ctx.register(deviceProfileDefinition, deviceProfile); + }); + } + + private void provisionAssetProfiles(SolutionInstallContext ctx) { + List assetProfiles = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "asset_profiles.json", new TypeReference<>() {}); + assetProfiles.addAll(loadListOfEntitiesFromDirectory(ctx.getTempDir(), "asset_profiles", AssetProfileDefinition.class)); + assetProfiles.forEach(assetProfile -> { + assetProfile.setId(null); + assetProfile.setCreatedTime(0L); + assetProfile.setTenantId(ctx.getTenantId()); + if (assetProfile.getDefaultRuleChainId() != null) { + String newId = ctx.getRealIds().get(assetProfile.getDefaultRuleChainId().getId().toString()); + if (newId != null) { + assetProfile.setDefaultRuleChainId(new RuleChainId(UUID.fromString(newId))); + } else { + log.error("[{}] Asset profile: {} references non existing rule chain.", ctx.getTenantId(), assetProfile.getName()); + throw new RuntimeException("Asset profile: " + assetProfile.getName() + " references non existing rule chain."); + } + } + if (assetProfile.getDefaultEdgeRuleChainId() != null) { + String newId = ctx.getRealIds().get(assetProfile.getDefaultEdgeRuleChainId().getId().toString()); + if (StringUtils.isEmpty(newId)) { + assetProfile.setDefaultEdgeRuleChainId(null); + } else { + assetProfile.setDefaultEdgeRuleChainId(new RuleChainId(UUID.fromString(newId))); + } + } + }); + + assetProfiles.forEach(assetProfileDefinition -> { + AssetProfile assetProfile = new AssetProfile(assetProfileDefinition); + assetProfile = assetProfileService.saveAssetProfile(assetProfile); + ctx.register(assetProfileDefinition, assetProfile); + }); + } + + private CustomerId getPublicCustomerId(SolutionInstallContext ctx) { + CustomerId publicId = ctx.getSolutionInstructions().getPublicId(); + if (publicId != null) { + return publicId; + } + Customer publicCustomer = customerService.findOrCreatePublicCustomer(ctx.getTenantId()); + ctx.getSolutionInstructions().setPublicId(publicCustomer.getId()); + return publicCustomer.getId(); + } + + private void provisionDashboards(SolutionInstallContext ctx) { + List dashboardDefs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "dashboards.json", new TypeReference<>() {}); + for (DashboardDefinition entityDef : dashboardDefs) { + CustomerId customerId = entityDef.isMakePublic() ? getPublicCustomerId(ctx) : ctx.getIdFromMap(EntityType.CUSTOMER, entityDef.getCustomer()); + Path dashboardPath = ctx.getTempDir().resolve("dashboards").resolve(entityDef.getFile()); + if (!Files.exists(dashboardPath)) { + log.warn("[{}] Dashboard file not found: {}", ctx.getTenantId(), entityDef.getFile()); + continue; + } + JsonNode dashboardJson = replaceIds(ctx, JacksonUtil.toJsonNode(dashboardPath)); + Dashboard dashboardTemplate = JacksonUtil.treeToValue(dashboardJson, Dashboard.class); + + Dashboard dashboard = new Dashboard(); + dashboard.setTenantId(ctx.getTenantId()); + dashboard.setTitle(entityDef.getName()); + dashboard.setConfiguration(dashboardTemplate.getConfiguration()); + dashboard.setImage(dashboardTemplate.getImage()); + dashboard.setResources(dashboardTemplate.getResources()); + if (dashboardJson.has("mobileHide") && dashboardJson.get("mobileHide").isBoolean()) { + dashboard.setMobileHide(dashboardJson.get("mobileHide").asBoolean()); + } + if (dashboardJson.has("mobileOrder") && dashboardJson.get("mobileOrder").isInt()) { + dashboard.setMobileOrder(dashboardJson.get("mobileOrder").asInt()); + } + + dashboard = dashboardService.saveDashboard(dashboard); + if (customerId != null) { + dashboardService.assignDashboardToCustomer(ctx.getTenantId(), dashboard.getId(), customerId); + } + ctx.register(entityDef, dashboard); + ctx.putIdToMap(EntityType.DASHBOARD, entityDef.getName(), dashboard.getId()); + + if (entityDef.isMain()) { + ctx.getSolutionInstructions().setDashboardId(dashboard.getId()); + ctx.getSolutionInstructions().setMainDashboardPublic(entityDef.isMakePublic()); + } + ctx.getDashboardLinks().add(new DashboardLinkInfo(dashboard.getTitle(), dashboard.getId(), entityDef.isMakePublic())); + + log.debug("[{}] Dashboard provisioned: {}", ctx.getTenantId(), dashboard.getTitle()); + } + } + + private void provisionRelations(SolutionInstallContext ctx) { + ctx.getRelationDefinitions().forEach((id, relations) -> { + for (RelationDefinition relationDef : relations) { + log.info("[{}] Saving relation: {}", id, relationDef); + EntityRelation entityRelation = new EntityRelation(); + EntityId otherId = resolveRelatedEntityId(relationDef, ctx); + if (EntitySearchDirection.FROM.equals(relationDef.getDirection())) { + entityRelation.setFrom(otherId); + entityRelation.setTo(id); + } else { + entityRelation.setFrom(id); + entityRelation.setTo(otherId); + } + entityRelation.setTypeGroup(RelationTypeGroup.COMMON); + entityRelation.setType(relationDef.getType()); + try { + relationService.save(ctx.getTenantId(), null, entityRelation, null); + } catch (Exception e) { + log.info("[{}] Failed to save relation: {}, cause: {}", id, relationDef, e.getMessage()); + } + } + }); + } + + private EntityId resolveRelatedEntityId(RelationDefinition relationDef, SolutionInstallContext ctx) { + if (relationDef.getEntityType() == EntityType.TENANT) { + return ctx.getTenantId(); + } + return ctx.getIdFromMap(relationDef.getEntityType(), relationDef.getEntityName()); + } + + private Map provisionDevices(SolutionInstallContext ctx) { + Map result = new HashMap<>(); + Set deviceTypeSet = new HashSet<>(); + List devices = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "devices.json", new TypeReference<>() {}); + for (DeviceDefinition entityDef : devices) { + CustomerId customerId = entityDef.isMakePublic() ? getPublicCustomerId(ctx) : ctx.getIdFromMap(EntityType.CUSTOMER, entityDef.getCustomer()); + Device entity = new Device(); + entity.setTenantId(ctx.getTenantId()); + entity.setName(entityDef.getName()); + entity.setLabel(entityDef.getLabel()); + ensureDeviceProfileExists(ctx, deviceTypeSet, entityDef); + entity.setType(entityDef.getType()); + entity.setCustomerId(customerId); + entity.setAdditionalInfo(entityDef.getAdditionalInfo()); + entity = deviceService.saveDevice(entity); + entityActionService.logEntityAction(ctx.getUser(), entity.getId(), entity, customerId, ActionType.ADDED, null); + ctx.register(entityDef, entity); + log.info("[{}] Saved device: {}", entity.getId(), entity); + DeviceId entityId = entity.getId(); + ctx.putIdToMap(entityDef, entityId); + saveServerSideAttributes(ctx, entityId, entityDef.getAttributes()); + saveSharedAttributes(ctx, entityId, entityDef.getSharedAttributes()); + ctx.put(entityId, entityDef.getRelations()); + + DeviceCredentialsInfo deviceCredentialsInfo = new DeviceCredentialsInfo(); + deviceCredentialsInfo.setName(entity.getName()); + deviceCredentialsInfo.setType(entity.getType()); + deviceCredentialsInfo.setCustomerName(entityDef.getCustomer()); + deviceCredentialsInfo.setCredentials(deviceCredentialsService.findDeviceCredentialsByDeviceId(ctx.getTenantId(), entityId)); + JsonNode additionalInfo = entity.getAdditionalInfo(); + boolean isGateway = additionalInfo != null && additionalInfo.hasNonNull("gateway") && additionalInfo.get("gateway").asBoolean(); + deviceCredentialsInfo.setGateway(isGateway); + ctx.addDeviceCredentials(deviceCredentialsInfo); + + result.put(entity, entityDef); + } + return result; + } + + private void ensureDeviceProfileExists(SolutionInstallContext ctx, Set deviceTypeSet, DeviceDefinition entityDef) { + if (!deviceTypeSet.contains(entityDef.getType())) { + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByName(ctx.getTenantId(), entityDef.getType()); + if (deviceProfile == null) { + DeviceProfile created = deviceProfileService.findOrCreateDeviceProfile(ctx.getTenantId(), entityDef.getType()); + ctx.register(created.getId()); + log.info("Saved device profile: {}", created.getId()); + } + deviceTypeSet.add(entityDef.getType()); + } + } + + private void registerEmulatorsAndComputeOldestTelemetryTs(SolutionInstallContext ctx) { + List emulatorDefinitions = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "device_emulators.json", new TypeReference<>() { + }); + Map deviceEmulators = emulatorDefinitions.stream().collect(Collectors.toMap(EmulatorDefinition::getName, Function.identity())); + emulatorDefinitions.stream().filter(ed -> StringUtils.isNotEmpty(ed.getExtendz())) + .forEach(ed -> { + EmulatorDefinition parent = deviceEmulators.get(ed.getExtendz()); + if (parent != null) { + ed.enrich(parent); + } + }); + Map assetEmulators = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "asset_emulators.json", new TypeReference>() { + }).stream().collect(Collectors.toMap(EmulatorDefinition::getName, Function.identity())); + + ctx.setDeviceEmulators(deviceEmulators); + ctx.setAssetEmulators(assetEmulators); + + long solutionInstallTs = ctx.getInstallTs(); + long oldestDeviceEmulatorsTs = deviceEmulators.values().stream() + .mapToLong(value -> value.getOldestTs(solutionInstallTs)) + .min().orElse(solutionInstallTs); + long oldestAssetEmulatorsTs = assetEmulators.values().stream() + .mapToLong(value -> value.getOldestTs(solutionInstallTs)) + .min().orElse(solutionInstallTs); + long solutionOldestTs = Math.min(oldestDeviceEmulatorsTs, oldestAssetEmulatorsTs); + + ctx.setOldestTelemetryTs(solutionOldestTs); + } + + private Set> launchEmulators(SolutionInstallContext ctx, Map devicesMap, Map assets) throws Exception { + Set> results = new HashSet<>(); + + for (var entry : devicesMap.entrySet().stream().filter(e -> StringUtils.isNotBlank(e.getValue().getEmulator())).collect(Collectors.toSet())) { + results.add(DeviceEmulatorLauncher.builder() + .entity(entry.getKey()) + .emulatorDefinition(ctx.getDeviceEmulators().get(entry.getValue().getEmulator())) + .oldTelemetryExecutor(emulatorExecutor) + .tbClusterService(tbClusterService) + .partitionService(partitionService) + .tbQueueProducerProvider(tbQueueProducerProvider) + .serviceInfoProvider(serviceInfoProvider) + .tsSubService(tsSubService) + .build().launch()); + } + + for (var entry : assets.entrySet().stream().filter(e -> StringUtils.isNotBlank(e.getValue().getEmulator())).collect(Collectors.toSet())) { + results.add(AssetEmulatorLauncher.builder() + .entity(entry.getKey()) + .emulatorDefinition(ctx.getAssetEmulators().get(entry.getValue().getEmulator())) + .oldTelemetryExecutor(emulatorExecutor) + .tbClusterService(tbClusterService) + .partitionService(partitionService) + .tbQueueProducerProvider(tbQueueProducerProvider) + .serviceInfoProvider(serviceInfoProvider) + .tsSubService(tsSubService) + .build().launch()); + } + + return results; + } + + private void provisionTenantDetails(SolutionInstallContext ctx) { + TenantDefinition tenant = loadEntityIfFileExists(ctx.getTempDir(), "tenant.json", TenantDefinition.class); + if (tenant != null) { + saveServerSideAttributes(ctx, ctx.getTenantId(), tenant.getAttributes()); + ctx.put(ctx.getTenantId(), tenant.getRelations()); + } + } + + private Map provisionAssets(SolutionInstallContext ctx) { + Map result = new HashMap<>(); + Set assetTypeSet = new HashSet<>(); + List assets = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "assets.json", new TypeReference<>() {}); + for (AssetDefinition entityDef : assets) { + Asset entity = new Asset(); + entity.setTenantId(ctx.getTenantId()); + entity.setName(entityDef.getName()); + entity.setLabel(entityDef.getLabel()); + entity.setType(entityDef.getType()); + if (entityDef.isMakePublic()) { + entity.setCustomerId(getPublicCustomerId(ctx)); + } else { + entity.setCustomerId(ctx.getIdFromMap(EntityType.CUSTOMER, entityDef.getCustomer())); + } + ensureAssetProfileExists(ctx, assetTypeSet, entityDef); + entity = assetService.saveAsset(entity); + ctx.register(entityDef, entity); + log.info("[{}] Saved asset: {}", entity.getId(), entity); + AssetId entityId = entity.getId(); + ctx.putIdToMap(entityDef, entityId); + saveServerSideAttributes(ctx, entityId, entityDef.getAttributes()); + ctx.put(entityId, entityDef.getRelations()); + result.put(entity, entityDef); + } + return result; + } + + private void ensureAssetProfileExists(SolutionInstallContext ctx, Set assetTypeSet, AssetDefinition entityDef) { + if (!assetTypeSet.contains(entityDef.getType())) { + AssetProfile assetProfile = assetProfileService.findAssetProfileByName(ctx.getTenantId(), entityDef.getType()); + if (assetProfile == null) { + AssetProfile created = assetProfileService.findOrCreateAssetProfile(ctx.getTenantId(), entityDef.getType()); + ctx.register(created.getId()); + log.info("Saved asset profile: {}", created.getId()); + } + assetTypeSet.add(entityDef.getType()); + } + } + + private void provisionCustomers(SolutionInstallContext ctx, List customers) { + for (CustomerDefinition entityDef : customers) { + entityDef.setRandomNameData(generateRandomName(ctx)); + Customer customer = new Customer(); + customer.setTenantId(ctx.getTenantId()); + customer.setTitle(randomize(entityDef.getName(), entityDef.getRandomNameData())); + customer.setEmail(randomize(entityDef.getEmail(), entityDef.getRandomNameData())); + customer.setCountry(entityDef.getCountry()); + customer.setCity(entityDef.getCity()); + customer.setState(entityDef.getState()); + customer.setZip(entityDef.getZip()); + customer.setAddress(entityDef.getAddress()); + customer = customerService.saveCustomer(customer); + log.info("[{}] Saved customer: {}", customer.getId(), customer); + ctx.register(entityDef, customer); + CustomerId entityId = customer.getId(); + ctx.putIdToMap(entityDef, entityId); + saveServerSideAttributes(ctx, entityId, entityDef.getAttributes(), entityDef.getRandomNameData()); + ctx.put(entityId, entityDef.getRelations()); + entityDef.setName(customer.getName()); + } + } + + private void provisionCustomerUsers(SolutionInstallContext ctx, List customers) { + for (CustomerDefinition entityDef : customers) { + Customer customer = customerService.findCustomerByTenantIdAndTitle(ctx.getTenantId(), entityDef.getName()).get(); + for (UserDefinition uDef : entityDef.getUsers()) { + String originalName = uDef.getName(); + User user = createUser(ctx, customer, uDef, entityDef); + + UserCredentials credentials = userService.findUserCredentialsByUserId(ctx.getTenantId(), user.getId()); + credentials.setEnabled(true); + credentials.setActivateToken(null); + credentials.setPassword(passwordEncoder.encode(uDef.getPassword())); + userService.saveUserCredentials(ctx.getTenantId(), credentials); + + DashboardUserDetailsDefinition dd = uDef.getDashboard(); + if (dd != null) { + DashboardId dashboardId = ctx.getIdFromMap(EntityType.DASHBOARD, dd.getName()); + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); + additionalInfo.put("defaultDashboardId", dashboardId.getId().toString()); + additionalInfo.put("defaultDashboardFullscreen", dd.isFullScreen()); + user.setAdditionalInfo(additionalInfo); + userService.saveUser(ctx.getTenantId(), user); + log.info("[{}] Added default dashboard for user {}", customer.getId(), user.getEmail()); + } + + UserCredentialsInfo credentialsInfo = new UserCredentialsInfo(); + credentialsInfo.setName(user.getFirstName() + " " + user.getLastName()); + credentialsInfo.setLogin(uDef.getName()); + credentialsInfo.setPassword(uDef.getPassword()); + credentialsInfo.setCustomerName(entityDef.getName()); + ctx.addUserCredentials(credentialsInfo); + ctx.register(entityDef, user); + ctx.put(user.getId(), uDef.getRelations()); + ctx.putIdToMap(EntityType.USER, originalName, user.getId()); + ctx.putIdToMap(EntityType.USER, uDef.getName(), user.getId()); + saveServerSideAttributes(ctx, user.getId(), uDef.getAttributes()); + } + } + } + + private User createUser(SolutionInstallContext ctx, Customer customer, UserDefinition uDef, CustomerDefinition cDef) { + int maxAttempts = 10; + int attempts = 0; + Exception finalE = null; + while (attempts < maxAttempts) { + try { + boolean lastAttempt = maxAttempts == (attempts + 1); + var randomName = lastAttempt ? RandomNameUtil.nextSuperRandom() : RandomNameUtil.next(); + User user = new User(); + if (!StringUtils.isEmpty(uDef.getFirstname())) { + user.setFirstName(randomize(uDef.getFirstname(), randomName, cDef.getRandomNameData())); + } else { + user.setFirstName(randomName.getFirstName()); + } + if (!StringUtils.isEmpty(uDef.getLastname())) { + user.setLastName(randomize(uDef.getLastname(), randomName, cDef.getRandomNameData())); + } else { + user.setLastName(randomName.getLastName()); + } + user.setAuthority(Authority.CUSTOMER_USER); + user.setEmail(randomize(uDef.getName(), randomName, cDef.getRandomNameData())); + user.setCustomerId(customer.getId()); + user.setTenantId(ctx.getTenantId()); + log.info("[{}] Saving user: {}", customer.getId(), user); + user = userService.saveUser(ctx.getTenantId(), user); + uDef.setName(user.getEmail()); + return user; + } catch (Exception e) { + finalE = e; + attempts++; + } + } + throw new RuntimeException(finalE); + } + + private void provisionEdges(SolutionInstallContext ctx) throws Exception { + List edges = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "edges.json", new TypeReference<>() {}); + RuleChain edgeTemplateRootRuleChain = ruleChainService.getEdgeTemplateRootRuleChain(ctx.getTenantId()); + for (EdgeDefinition entityDef : edges) { + CustomerId customerId = entityDef.isMakePublic() ? getPublicCustomerId(ctx) : ctx.getIdFromMap(EntityType.CUSTOMER, entityDef.getCustomer()); + Edge entity = new Edge(); + entity.setTenantId(ctx.getTenantId()); + entity.setName(entityDef.getName()); + entity.setLabel(entityDef.getLabel()); + entity.setType(entityDef.getType()); + entity.setCustomerId(customerId); + entity.setRoutingKey(UUID.randomUUID().toString()); + entity.setSecret(StringUtils.randomAlphanumeric(20)); + RuleChainId rootRuleChainId = edgeTemplateRootRuleChain.getId(); + if (StringUtils.isNotBlank(entityDef.getRootRuleChainId())) { + String newId = ctx.getRealIds().get(entityDef.getRootRuleChainId()); + if (newId != null) { + rootRuleChainId = new RuleChainId(UUID.fromString(newId)); + } else { + log.error("[{}] Edge: {} references non existing rule chain.", ctx.getTenantId(), entity.getName()); + throw new RuntimeException("Edge: " + entity.getName() + " references non existing rule chain."); + } + } + entity.setRootRuleChainId(rootRuleChainId); + RuleChain rootRuleChain = ruleChainService.findRuleChainById(ctx.getTenantId(), rootRuleChainId); + entity = tbEdgeService.save(entity, rootRuleChain, ctx.getUser()); + ctx.register(entityDef, entity); + assignRuleChainsToEdge(ctx, entityDef.getRuleChainIds(), entity); + assignAssetsToEdge(ctx, entityDef.getAssetIds(), entity); + assignDevicesToEdge(ctx, entityDef.getDeviceIds(), entity); + assignDashboardsToEdge(ctx, entityDef.getDashboardIds(), entity); + log.info("[{}] Saved edge: {}", entity.getId(), entity); + EdgeId entityId = entity.getId(); + ctx.putIdToMap(entityDef, entityId); + saveServerSideAttributes(ctx, entityId, entityDef.getAttributes()); + ctx.put(entityId, entityDef.getRelations()); + ctx.addEdgeLinkInfo(entity.getName(), new EdgeLinkInfo(entity.getId(), customerId != null ? customerId : ctx.getTenantId())); + } + } + + private void assignRuleChainsToEdge(SolutionInstallContext ctx, List ruleChainIds, Edge entity) { + if (ruleChainIds == null || ruleChainIds.isEmpty()) { + return; + } + for (String strRuleChainId : ruleChainIds) { + String newId = ctx.getRealIds().get(strRuleChainId); + if (newId != null) { + RuleChainId ruleChainId = new RuleChainId(UUID.fromString(newId)); + ruleChainService.assignRuleChainToEdge(ctx.getTenantId(), ruleChainId, entity.getId()); + } else { + log.error("[{}] Edge: {} references non existing edge rule chain.", ctx.getTenantId(), entity.getName()); + throw new RuntimeException("Edge: " + entity.getName() + " references non existing edge rule chain."); + } + } + } + + private void assignAssetsToEdge(SolutionInstallContext ctx, List assetIds, Edge entity) { + if (assetIds == null || assetIds.isEmpty()) { + return; + } + for (String strAssetId : assetIds) { + String newId = ctx.getRealIds().get(strAssetId); + if (newId != null) { + AssetId assetId = new AssetId(UUID.fromString(newId)); + assetService.assignAssetToEdge(ctx.getTenantId(), assetId, entity.getId()); + } else { + log.error("[{}] Edge: {} references non existing asset.", ctx.getTenantId(), entity.getName()); + throw new RuntimeException("Edge: " + entity.getName() + " references non existing asset."); + } + } + } + + private void assignDevicesToEdge(SolutionInstallContext ctx, List deviceIds, Edge entity) { + if (deviceIds == null || deviceIds.isEmpty()) { + return; + } + for (String strDeviceId : deviceIds) { + String newId = ctx.getRealIds().get(strDeviceId); + if (newId != null) { + DeviceId deviceId = new DeviceId(UUID.fromString(newId)); + deviceService.assignDeviceToEdge(ctx.getTenantId(), deviceId, entity.getId()); + } else { + log.error("[{}] Edge: {} references non existing device.", ctx.getTenantId(), entity.getName()); + throw new RuntimeException("Edge: " + entity.getName() + " references non existing device."); + } + } + } + + private void assignDashboardsToEdge(SolutionInstallContext ctx, List dashboardIds, Edge entity) { + if (dashboardIds == null || dashboardIds.isEmpty()) { + return; + } + for (String strDashboardId : dashboardIds) { + String newId = ctx.getRealIds().get(strDashboardId); + if (newId != null) { + DashboardId dashboardId = new DashboardId(UUID.fromString(newId)); + dashboardService.assignDashboardToEdge(ctx.getTenantId(), dashboardId, entity.getId()); + } else { + log.error("[{}] Edge: {} references non existing dashboard.", ctx.getTenantId(), entity.getName()); + throw new RuntimeException("Edge: " + entity.getName() + " references non existing dashboard."); + } + } + } + + private void provisionAlarmRules(SolutionInstallContext ctx) { + List cfs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "alarm_rules.json", new TypeReference<>() {}); + cfs.addAll(loadListOfEntitiesFromDirectory(ctx.getTempDir(), "alarm_rules", CalculatedField.class)); + cfs.forEach(cf -> ctx.register(createCalculatedField(cf, ctx))); + } + + private void provisionCalculatedFields(SolutionInstallContext ctx) { + List cfs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "calculated_fields.json", new TypeReference<>() { + }); + cfs.addAll(loadListOfEntitiesFromDirectory(ctx.getTempDir(), "calculated_fields", CalculatedFieldDefinition.class)); + + List createOnly = new ArrayList<>(); + TreeMap> ordered = new TreeMap<>(); + + for (CalculatedFieldDefinition cf : cfs) { + if (cf.getReprocessingOrder() == null || cf.getReprocessingOrder() < 0) { + createOnly.add(cf); + } else { + ordered.computeIfAbsent(cf.getReprocessingOrder(), integer -> new ArrayList<>()).add(cf); + } + } + + createOnly.forEach(cf -> ctx.register(createCalculatedField(cf, ctx))); + + for (Map.Entry> entry : ordered.entrySet()) { + List cfDefs = entry.getValue(); + for (CalculatedFieldDefinition cfDef : cfDefs) { + CalculatedField calculatedField = createCalculatedField(cfDef, ctx); + ctx.register(calculatedField); + } + } + } + + private CalculatedField createCalculatedField(CalculatedField cf, SolutionInstallContext ctx) { + cf.setId(null); + cf.setCreatedTime(0L); + cf.setTenantId(ctx.getTenantId()); + cf.setDebugSettings(new DebugSettings(true, System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(15))); + + Map realIds = ctx.getRealIds(); + + EntityId entityId = cf.getEntityId(); + if (entityId != null) { + String newEntityId = realIds.get(entityId.getId().toString()); + if (newEntityId != null) { + cf.setEntityId(EntityIdFactory.getByTypeAndUuid(entityId.getEntityType(), newEntityId)); + } else { + log.error("[{}] Calculated field: {} references non existing entity.", ctx.getTenantId(), cf.getName()); + throw new RuntimeException("Calculated field: " + cf.getName() + " references non existing entity."); + } + } + if (cf.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argBasedCfg) { + argBasedCfg.getArguments().forEach((key, argument) -> { + EntityId refEntityId = argument.getRefEntityId(); + if (refEntityId != null) { + if (refEntityId.getEntityType() == EntityType.TENANT) { + argument.setRefEntityId(ctx.getTenantId()); + } else { + String newId = realIds.get(refEntityId.getId().toString()); + if (newId != null) { + argument.setRefEntityId(EntityIdFactory.getByTypeAndUuid(refEntityId.getEntityType(), newId)); + } else { + log.error("[{}][{}] Calculated field: {} references non existing entity.", ctx.getTenantId(), ctx.getSolutionId(), cf.getName()); + throw new ThingsboardRuntimeException(); + } + } + } + }); + } + + CalculatedField calculatedField = new CalculatedField(cf); + return calculatedFieldService.save(calculatedField); + } + + private RandomNameData generateRandomName(SolutionInstallContext ctx) { + int i = 0; + while (i < 10) { + var randomName = RandomNameUtil.next(); + var user = userService.findUserByEmail(ctx.getTenantId(), randomName.getEmail()); + if (user == null) { + return randomName; + } else { + i++; + } + } + String firstName = StringUtils.randomAlphanumeric(5); + String lastName = StringUtils.randomAlphanumeric(5); + return new RandomNameData(firstName, lastName, firstName + "." + lastName + "@thingsboard.io"); + } + + private String randomize(String src, RandomNameData name) { + return randomize(src, name, null); + } + + private String randomize(String src, RandomNameData name, RandomNameData customer) { + if (src == null) { + return null; + } else { + String result = src + .replace("$randomFirstName", name.getFirstName()) + .replace("$randomLastName", name.getLastName()) + .replace("$randomEmail", name.getEmail()); + if (customer != null) { + result = result + .replace("$customerFirstName", customer.getFirstName()) + .replace("$customerLastName", customer.getLastName()) + .replace("$customerEmail", customer.getEmail()); + } + return result.replace("$random", StringUtils.randomAlphanumeric(10).toLowerCase()); + } + } + + private void saveServerSideAttributes(SolutionInstallContext ctx, EntityId entityId, JsonNode attributes) { + saveServerSideAttributes(ctx, entityId, attributes, null); + } + + private void saveServerSideAttributes(SolutionInstallContext ctx, EntityId entityId, JsonNode attributes, RandomNameData randomNameData) { + saveAttributes(ctx, entityId, attributes, randomNameData, AttributeScope.SERVER_SCOPE); + } + + private void saveSharedAttributes(SolutionInstallContext ctx, EntityId entityId, JsonNode attributes) { + if (!EntityType.DEVICE.equals(entityId.getEntityType())) { + throw new IllegalArgumentException(entityId.getEntityType() + " cannot have shared attributes."); + } + saveAttributes(ctx, entityId, attributes, null, AttributeScope.SHARED_SCOPE); + } + + private void saveAttributes(SolutionInstallContext ctx, EntityId entityId, JsonNode attributes, RandomNameData randomNameData, AttributeScope attributeScope) { + if (attributes != null && !attributes.isNull() && !attributes.isEmpty()) { + attributes = prepareAttributes(attributes); + log.info("[{}] Saving attributes: {}", entityId, attributes); + if (randomNameData != null) { + attributes = JacksonUtil.toJsonNode(randomize(JacksonUtil.toString(attributes), randomNameData, null)); + } + attributesService.save(ctx.getTenantId(), entityId, attributeScope, + new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString(JacksonUtil.toString(attributes)), ctx.getOldestTelemetryTs()))); + } + } + + private JsonNode prepareAttributes(JsonNode attributes) { + ObjectNode attributesObj = (ObjectNode) attributes; + attributes.fields().forEachRemaining(entry -> { + JsonNode value = entry.getValue(); + if (value.isTextual() && isTimeExpression(value.asText())) { + value = JacksonUtil.toJsonNode(parseTimeExpression(value.asText())); + } + attributesObj.set(entry.getKey(), value); + }); + return attributesObj; + } + + private boolean isTimeExpression(String text) { + return Pattern.matches("\\$\\{currentTime(?:([+-])(\\d+)([mwdh]|min))?}", text); + } + + private String parseTimeExpression(String timeExpression) { + Matcher matcher = Pattern.compile("\\$\\{currentTime(?:([+-])(\\d+)([mwdh]|min))?}").matcher(timeExpression); + + if (!matcher.matches()) { + return timeExpression; + } + + String operator = matcher.group(1); + String amountStr = matcher.group(2); + String unit = matcher.group(3); + + ZonedDateTime now = ZonedDateTime.now(); + + if (operator != null && amountStr != null && unit != null) { + int amount = Integer.parseInt(amountStr); + now = switch (unit) { + case "m" -> operator.equals("+") ? now.plusMonths(amount) : now.minusMonths(amount); + case "w" -> operator.equals("+") ? now.plusWeeks(amount) : now.minusWeeks(amount); + case "d" -> operator.equals("+") ? now.plusDays(amount) : now.minusDays(amount); + case "h" -> operator.equals("+") ? now.plusHours(amount) : now.minusHours(amount); + case "min" -> operator.equals("+") ? now.plusMinutes(amount) : now.minusMinutes(amount); + default -> now; + }; + } + + return String.valueOf(now.toInstant().toEpochMilli()); + } + + private String getTypeLabel(EntityType type) { + return type.name().toLowerCase().replace('_', ' '); + } + + private T loadEntityIfFileExists(Path tempDir, String fileName, Class clazz) { + Path filePath = tempDir.resolve("entities").resolve(fileName); + if (Files.exists(filePath)) { + return JacksonUtil.readValue(filePath.toFile(), clazz); + } else { + return null; + } + } + + private List loadListOfEntitiesIfFileExists(Path tempDir, String fileName, TypeReference> typeReference) { + Path filePath = tempDir.resolve("entities").resolve(fileName); + if (Files.exists(filePath)) { + try { + return JacksonUtil.readValue(filePath.toFile(), typeReference); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid json file " + fileName + " data structure", e); + } + } else { + return new ArrayList<>(); + } + } + + private List loadListOfEntitiesFromDirectory(Path tempDir, String dirName, Class clazz) { + Path dirPath = tempDir.resolve(dirName); + if (Files.exists(dirPath) && Files.isDirectory(dirPath)) { + List result = new ArrayList<>(); + try { + for (Path filePath : Files.list(dirPath).collect(Collectors.toList())) { + try { + result.add(JacksonUtil.readValue(filePath.toFile(), clazz)); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid json file " + filePath.getFileName() + " data structure", e); + } + } + } catch (IOException e) { + log.warn("Failed to read directory: {}", dirName, e); + throw new RuntimeException(e); + } + return result; + } else { + return new ArrayList<>(); + } + } + + private String loadSolutionId(Path tempDir) { + Path solutionJson = tempDir.resolve("solution.json"); + if (Files.exists(solutionJson)) { + JsonNode node = JacksonUtil.toJsonNode(solutionJson); + if (node != null && node.has("title")) { + String title = node.get("title").asText(""); + return title.trim().toLowerCase().replaceAll("[^a-z0-9]+", "-").replaceAll("^-|-$", ""); + } + } + return null; + } + + private long loadInstallTimeoutMs(Path tempDir) { + Path solutionJson = tempDir.resolve("solution.json"); + if (Files.exists(solutionJson)) { + JsonNode node = JacksonUtil.toJsonNode(solutionJson); + if (node != null && node.has("installTimeoutMs")) { + return node.get("installTimeoutMs").asLong(0L); + } + } + return 0L; + } + + private List loadTenantTelemetryKeys(Path tempDir) { + Path solutionJson = tempDir.resolve("solution.json"); + if (Files.exists(solutionJson)) { + JsonNode node = JacksonUtil.toJsonNode(solutionJson); + if (node != null && node.has("tenantTelemetryKeys")) { + return JacksonUtil.convertValue(node.get("tenantTelemetryKeys"), new TypeReference<>() {}); + } + } + return Collections.emptyList(); + } + + private List loadTenantAttributeKeys(Path tempDir) { + Path solutionJson = tempDir.resolve("solution.json"); + if (Files.exists(solutionJson)) { + JsonNode node = JacksonUtil.toJsonNode(solutionJson); + if (node != null && node.has("tenantAttributeKeys")) { + return JacksonUtil.convertValue(node.get("tenantAttributeKeys"), new TypeReference<>() {}); + } + } + return Collections.emptyList(); + } + + private static final int EXTRACT_BUFFER_SIZE = 8 * 1024; + + private void extractZip(byte[] zipData, Path destDir) throws IOException { + long totalBytes = 0; + int entryCount = 0; + byte[] buf = new byte[EXTRACT_BUFFER_SIZE]; + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipData))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (++entryCount > maxArchiveEntryCount) { + throw new IOException("Solution template archive exceeds max entry count: " + maxArchiveEntryCount); + } + Path entryPath = destDir.resolve(entry.getName()).normalize(); + if (!entryPath.startsWith(destDir)) { + throw new IOException("ZIP entry outside of target directory: " + entry.getName()); + } + if (entry.isDirectory()) { + Files.createDirectories(entryPath); + } else { + Files.createDirectories(entryPath.getParent()); + try (OutputStream out = Files.newOutputStream(entryPath)) { + long entryBytes = 0; + int n; + while ((n = zis.read(buf)) > 0) { + entryBytes += n; + totalBytes += n; + if (entryBytes > maxUncompressedEntryBytes) { + throw new IOException("Solution template entry exceeds max uncompressed size: " + entry.getName()); + } + if (totalBytes > maxUncompressedArchiveBytes) { + throw new IOException("Solution template archive exceeds max uncompressed size: " + maxUncompressedArchiveBytes + " bytes"); + } + out.write(buf, 0, n); + } + } + } + zis.closeEntry(); + } + } + } + + private static void deleteDirectory(Path dir) { + try { + Files.walkFileTree(dir, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path d, IOException exc) throws IOException { + Files.delete(d); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + log.warn("Failed to clean up temp directory: {}", dir, e); + } + } + + private void deleteEntity(TenantId tenantId, EntityId entityId, User user) { + try { + List alarmIds = alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, null, false)) + .getData().stream().map(AlarmInfo::getId).collect(Collectors.toList()); + Set typesToRemove = new HashSet<>(); + alarmIds.forEach(alarmId -> { + var result = alarmService.delAlarm(tenantId, alarmId, false); + if (result.isSuccessful()) { + typesToRemove.add(result.getAlarm().getType()); + } + }); + alarmService.delAlarmTypes(tenantId, typesToRemove); + } catch (Exception e) { + log.error("[{}] Failed to delete alarms for entity", entityId.getId(), e); + } + switch (entityId.getEntityType()) { + case CALCULATED_FIELD: + CalculatedField cf = calculatedFieldService.findById(tenantId, new CalculatedFieldId(entityId.getId())); + if (cf != null) { + tbCalculatedFieldService.delete(cf, user); + } + break; + case RULE_CHAIN: + ruleChainService.deleteRuleChainById(tenantId, new RuleChainId(entityId.getId())); + break; + case DEVICE: + Device device = deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId())); + if (device != null) { + tbDeviceService.delete(device, user); + } + break; + case DEVICE_PROFILE: + deviceProfileService.deleteDeviceProfile(tenantId, new DeviceProfileId(entityId.getId())); + break; + case ASSET: + Asset asset = assetService.findAssetById(tenantId, new AssetId(entityId.getId())); + if (asset != null) { + tbAssetService.delete(asset, user); + } + break; + case ASSET_PROFILE: + assetProfileService.deleteAssetProfile(tenantId, new AssetProfileId(entityId.getId())); + break; + case CUSTOMER: + customerService.deleteCustomer(tenantId, new CustomerId(entityId.getId())); + break; + case USER: + User userToDelete = userService.findUserById(tenantId, new UserId(entityId.getId())); + if (userToDelete != null) { + userService.deleteUser(tenantId, userToDelete); + } + break; + case EDGE: + Edge edge = edgeService.findEdgeById(tenantId, new EdgeId(entityId.getId())); + if (edge != null) { + tbEdgeService.delete(edge, user); + } + break; + case DASHBOARD: + dashboardService.deleteDashboard(tenantId, new DashboardId(entityId.getId())); + break; + default: + log.warn("[{}] Unsupported entity type for deletion: {}", tenantId, entityId.getEntityType()); + } + } + + private JsonNode replaceIds(SolutionInstallContext ctx, JsonNode json) { + String jsonStr = JacksonUtil.toString(json); + for (var e : ctx.getRealIds().entrySet()) { + jsonStr = jsonStr.replace(e.getKey(), e.getValue()); + } + return JacksonUtil.toJsonNode(jsonStr); + } + + private String readFileContent(Path path) { + try { + return Files.readString(path); + } catch (IOException e) { + log.warn("Failed to read file: {}", path, e); + return ""; + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/SolutionService.java b/application/src/main/java/org/thingsboard/server/service/solutions/SolutionService.java new file mode 100644 index 0000000000..58c47d1dc2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/SolutionService.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 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.solutions; + +import jakarta.servlet.http.HttpServletRequest; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.iot_hub.SolutionTemplateInstalledItemDescriptor; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.solutions.data.solution.SolutionInstallResponse; + +import java.util.List; + +public interface SolutionService { + + SolutionInstallResponse installSolution(SecurityUser user, TenantId tenantId, byte[] zipData, HttpServletRequest request) throws Exception; + + void deleteSolution(TenantId tenantId, SolutionTemplateInstalledItemDescriptor descriptor, SecurityUser user) throws ThingsboardException; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedAlarmRuleInfo.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedAlarmRuleInfo.java new file mode 100644 index 0000000000..e03b28588f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedAlarmRuleInfo.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2026 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.solutions.data; + +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.UUID; +import java.util.stream.Collectors; + +public record CreatedAlarmRuleInfo(EntityId entityId, String entityName, String alarmType, + String severities) implements HasAppliedToEntity { + + public static CreatedAlarmRuleInfo from(EntityId entityId, String entityName, CalculatedField calculatedField) { + if (calculatedField.getType() != CalculatedFieldType.ALARM) { + throw new UnsupportedOperationException("Only alarm calculated fields are supported"); + } + String severities = ((AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration()) + .getCreateRules().keySet().stream() + .map(AlarmSeverity::getDisplayName) + .sorted() + .collect(Collectors.joining(", ")); + return new CreatedAlarmRuleInfo(entityId, entityName, calculatedField.getName(), severities); + } + + @Override + public String getCfPageLink(UUID cfId) { + return "/alarms/alarm-rules/" + cfId; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedCalculatedFieldInfo.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedCalculatedFieldInfo.java new file mode 100644 index 0000000000..7b19fbc0cf --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedCalculatedFieldInfo.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 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.solutions.data; + +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.UUID; + +public record CreatedCalculatedFieldInfo(EntityId entityId, String entityName, String type, + String name) implements HasAppliedToEntity { + + public static CreatedCalculatedFieldInfo from(EntityId entityId, String entityName, CalculatedField calculatedField) { + return new CreatedCalculatedFieldInfo(entityId, entityName, calculatedField.getType().getDisplayName(), calculatedField.getName()); + } + + @Override + public String getCfPageLink(UUID cfId) { + return "/calculatedFields/" + cfId; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedEntityInfo.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedEntityInfo.java new file mode 100644 index 0000000000..53105a6e31 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedEntityInfo.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 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.solutions.data; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.EntityType; + +import java.util.UUID; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CreatedEntityInfo { + + private String name; + private EntityType type; + private String owner; + + public String getEntityPageLink(UUID id) { + return switch (type) { + case CUSTOMER -> "/customers/" + id; + case USER -> "/users/" + id; + case ASSET -> "/entities/assets/" + id; + case DEVICE -> "/entities/devices/" + id; + case DEVICE_PROFILE -> "/profiles/deviceProfiles/" + id; + case ASSET_PROFILE -> "/profiles/assetProfiles/" + id; + case DASHBOARD -> "/dashboards/" + id; + case RULE_CHAIN -> "/ruleChains/" + id; + case EDGE -> "/edgeManagement/instances/" + id; + default -> null; + }; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedRuleChainInfo.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedRuleChainInfo.java new file mode 100644 index 0000000000..9a785242b2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedRuleChainInfo.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2026 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.solutions.data; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.rule.RuleChainType; + +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +public class CreatedRuleChainInfo extends CreatedEntityInfo { + + private RuleChainType ruleChainType; + + public CreatedRuleChainInfo(String name, RuleChainType ruleChainType, String owner) { + super(name, EntityType.RULE_CHAIN, owner); + this.ruleChainType = ruleChainType; + } + + @Override + public String getEntityPageLink(UUID id) { + return ruleChainType == RuleChainType.EDGE ? "/edgeManagement/ruleChains/" + id : "/ruleChains/" + id; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/DashboardLinkInfo.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/DashboardLinkInfo.java new file mode 100644 index 0000000000..63eb0e3757 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/DashboardLinkInfo.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 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.solutions.data; + +import lombok.Data; +import org.thingsboard.server.common.data.id.DashboardId; + +@Data +public class DashboardLinkInfo { + + private final String name; + private final DashboardId dashboardId; + private final boolean isPublic; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/DeviceCredentialsInfo.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/DeviceCredentialsInfo.java new file mode 100644 index 0000000000..e93104729c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/DeviceCredentialsInfo.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2026 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.solutions.data; + +import lombok.Data; +import org.thingsboard.server.common.data.security.DeviceCredentials; + +@Data +public class DeviceCredentialsInfo { + + String name; + String type; + DeviceCredentials credentials; + String customerName; + boolean gateway; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/EdgeLinkInfo.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/EdgeLinkInfo.java new file mode 100644 index 0000000000..e5f057636d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/EdgeLinkInfo.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 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.solutions.data; + +import lombok.Data; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.EntityId; + +@Data +public class EdgeLinkInfo { + + private final EdgeId edgeId; + private final EntityId ownerId; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/HasAppliedToEntity.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/HasAppliedToEntity.java new file mode 100644 index 0000000000..9b029bfa78 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/HasAppliedToEntity.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2026 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.solutions.data; + +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.UUID; + +public interface HasAppliedToEntity { + + EntityId entityId(); + + String getCfPageLink(UUID cfId); + + default String getEntityPageLink() { + EntityId id = entityId(); + if (id == null) { + return null; + } + String idStr = id.getId().toString(); + return switch (id.getEntityType()) { + case DEVICE_PROFILE -> "/profiles/deviceProfiles/" + idStr; + case ASSET_PROFILE -> "/profiles/assetProfiles/" + idStr; + case DEVICE -> "/entities/devices/" + idStr; + case ASSET -> "/entities/assets/" + idStr; + default -> null; + }; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java new file mode 100644 index 0000000000..4985ca1685 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java @@ -0,0 +1,210 @@ +/** + * Copyright © 2016-2026 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.solutions.data; + +import lombok.Data; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.service.solutions.data.definition.AssetDefinition; +import org.thingsboard.server.service.solutions.data.definition.AssetProfileDefinition; +import org.thingsboard.server.service.solutions.data.definition.CustomerDefinition; +import org.thingsboard.server.service.solutions.data.definition.DashboardDefinition; +import org.thingsboard.server.service.solutions.data.definition.DeviceDefinition; +import org.thingsboard.server.service.solutions.data.definition.EdgeDefinition; +import org.thingsboard.server.service.solutions.data.definition.DeviceProfileDefinition; +import org.thingsboard.server.service.solutions.data.definition.EntityDefinition; +import org.thingsboard.server.service.solutions.data.definition.EntitySearchKey; +import org.thingsboard.server.service.solutions.data.definition.EmulatorDefinition; +import org.thingsboard.server.service.solutions.data.definition.RelationDefinition; +import org.thingsboard.server.service.solutions.data.solution.TenantSolutionTemplateInstructions; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Data +public class SolutionInstallContext { + + private final TenantId tenantId; + private final Path tempDir; + private final User user; + private final TenantSolutionTemplateInstructions solutionInstructions; + private final List createdEntitiesList = new ArrayList<>(); + private final Map realIds = new HashMap<>(); + private final Map entityIdMap = new HashMap<>(); + private final Map> relationDefinitions = new LinkedHashMap<>(); + private final List dashboardLinks = new ArrayList<>(); + + // For instructions + private final Map createdDevices = new LinkedHashMap<>(); + private final Map createdUsers = new LinkedHashMap<>(); + private final Map createdEdges = new LinkedHashMap<>(); + private final Map createdEntities = new LinkedHashMap<>(); + private final Map createdAlarmRules = new LinkedHashMap<>(); + private final Map createdCalculatedFields = new LinkedHashMap<>(); + private final String solutionId; + private final long installTs; + private long oldestTelemetryTs; + private Map deviceEmulators; + private Map assetEmulators; + + public SolutionInstallContext(TenantId tenantId, String solutionId, Path tempDir, User user, TenantSolutionTemplateInstructions solutionInstructions) { + this.tenantId = tenantId; + this.solutionId = solutionId; + this.tempDir = tempDir; + this.user = user; + this.solutionInstructions = solutionInstructions; + this.installTs = System.currentTimeMillis(); + put(new EntitySearchKey(tenantId, EntityType.TENANT, null), tenantId); + } + + public void registerReferenceOnly(String referenceId, EntityId entityId) { + if (!StringUtils.isEmpty(referenceId)) { + realIds.put(referenceId, entityId.getId().toString()); + } + } + + public void register(String referenceId, EntityId entityId) { + registerReferenceOnly(referenceId, entityId); + register(entityId); + } + + public void register(EntityId entityId) { + createdEntitiesList.add(entityId); + } + + public void register(String referenceId, RuleChain ruleChain) { + register(referenceId, ruleChain.getId()); + createdEntities.put(ruleChain.getUuidId(), new CreatedRuleChainInfo(ruleChain.getName(), ruleChain.getType(), "Tenant")); + } + + public void register(DeviceProfileDefinition definition, DeviceProfile deviceProfile) { + register(definition.getJsonId(), deviceProfile.getId()); + createdEntities.put(deviceProfile.getUuidId(), new CreatedEntityInfo(deviceProfile.getName(), EntityType.DEVICE_PROFILE, "Tenant")); + } + + public void register(AssetProfileDefinition definition, AssetProfile assetProfile) { + register(definition.getJsonId(), assetProfile.getId()); + createdEntities.put(assetProfile.getUuidId(), new CreatedEntityInfo(assetProfile.getName(), EntityType.ASSET_PROFILE, "Tenant")); + } + + public void register(AssetDefinition definition, Asset asset) { + register(definition.getJsonId(), asset.getId()); + createdEntities.put(asset.getUuidId(), new CreatedEntityInfo(asset.getName(), EntityType.ASSET, + StringUtils.isEmpty(definition.getCustomer()) ? "Tenant" : definition.getCustomer())); + } + + public void register(DeviceDefinition definition, Device device) { + register(definition.getJsonId(), device.getId()); + createdEntities.put(device.getUuidId(), new CreatedEntityInfo(device.getName(), EntityType.DEVICE, + StringUtils.isEmpty(definition.getCustomer()) ? "Tenant" : definition.getCustomer())); + } + + public void register(CustomerDefinition definition, Customer customer) { + register(definition.getJsonId(), customer.getId()); + createdEntities.put(customer.getUuidId(), new CreatedEntityInfo(customer.getName(), EntityType.CUSTOMER, "Tenant")); + } + + public void register(CustomerDefinition customerDef, User user) { + register((String) null, user.getId()); + createdEntities.put(user.getUuidId(), new CreatedEntityInfo(user.getName(), EntityType.USER, + StringUtils.isEmpty(customerDef.getName()) ? "Tenant" : customerDef.getName())); + } + + public void register(EdgeDefinition definition, Edge edge) { + register(definition.getJsonId(), edge.getId()); + createdEntities.put(edge.getUuidId(), new CreatedEntityInfo(edge.getName(), EntityType.EDGE, + StringUtils.isEmpty(definition.getCustomer()) ? "Tenant" : definition.getCustomer())); + } + + public void register(DashboardDefinition definition, Dashboard dashboard) { + register(definition.getJsonId(), dashboard.getId()); + createdEntities.put(dashboard.getUuidId(), new CreatedEntityInfo(dashboard.getTitle(), EntityType.DASHBOARD, + StringUtils.isEmpty(definition.getCustomer()) ? "Tenant" : definition.getCustomer())); + } + + public void register(CalculatedField calculatedField) { + register(calculatedField.getId()); + EntityId entityId = calculatedField.getEntityId(); + CreatedEntityInfo entityInfo = createdEntities.get(entityId.getId()); + boolean alarmRule = calculatedField.getType() == CalculatedFieldType.ALARM; + if (entityInfo == null) { + String target = alarmRule ? "Alarm rule" : "Calculated field"; + throw new IllegalStateException("Failed to register " + target + " with name: " + + calculatedField.getName() + " for non-existing entity with id: " + entityId); + } + if (alarmRule) { + createdAlarmRules.put(calculatedField.getUuidId(), CreatedAlarmRuleInfo.from(entityId, entityInfo.getName(), calculatedField)); + return; + } + createdCalculatedFields.put(calculatedField.getUuidId(), CreatedCalculatedFieldInfo.from(entityId, entityInfo.getName(), calculatedField)); + } + + public void putIdToMap(EntityDefinition entityDefinition, EntityId entityId) { + putIdToMap(entityDefinition.getEntityType(), entityDefinition.getName(), entityId); + } + + public void putIdToMap(EntityType entityType, String entityName, EntityId entityId) { + putIdToMap(tenantId, entityType, entityName, entityId); + } + + public void putIdToMap(EntityId ownerId, EntityType entityType, String entityName, EntityId entityId) { + put(new EntitySearchKey(ownerId, entityType, entityName), entityId); + } + + public void put(EntitySearchKey entitySearchKey, EntityId entityId) { + entityIdMap.put(entitySearchKey, entityId); + } + + @SuppressWarnings("unchecked") + public T getIdFromMap(EntityType entityType, String entityName) { + return (T) entityIdMap.get(new EntitySearchKey(tenantId, entityType, entityName)); + } + + public void put(EntityId entityId, List relations) { + relationDefinitions.put(entityId, relations); + } + + public void addDeviceCredentials(DeviceCredentialsInfo deviceCredentialsInfo) { + createdDevices.put(deviceCredentialsInfo.getName(), deviceCredentialsInfo); + } + + public void addUserCredentials(UserCredentialsInfo userCredentialsInfo) { + createdUsers.put(userCredentialsInfo.getName(), userCredentialsInfo); + } + + public void addEdgeLinkInfo(String edgeName, EdgeLinkInfo edgeLinkInfo) { + createdEdges.put(edgeName, edgeLinkInfo); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/UserCredentialsInfo.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/UserCredentialsInfo.java new file mode 100644 index 0000000000..d35900c5b4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/UserCredentialsInfo.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 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.solutions.data; + +import lombok.Data; + +@Data +public class UserCredentialsInfo { + + String name; + String login; + String password; + String customerName; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/AssetDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/AssetDefinition.java new file mode 100644 index 0000000000..2554dbafb0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/AssetDefinition.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.EntityType; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +public class AssetDefinition extends CustomerEntityDefinition { + + private String type; + private String label; + private String emulator; + + @Override + public EntityType getEntityType() { + return EntityType.ASSET; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/AssetProfileDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/AssetProfileDefinition.java new file mode 100644 index 0000000000..9f95294fa7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/AssetProfileDefinition.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.asset.AssetProfile; + +@Data +@EqualsAndHashCode(callSuper = true) +public class AssetProfileDefinition extends AssetProfile { + + private String jsonId; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/BaseEntityDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/BaseEntityDefinition.java new file mode 100644 index 0000000000..84ec880380 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/BaseEntityDefinition.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Collections; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public abstract class BaseEntityDefinition implements EntityDefinition { + + @JsonProperty("name") + private String name; + @JsonProperty("attributes") + private JsonNode attributes; + @JsonProperty("sharedAttributes") + private JsonNode sharedAttributes; + @JsonProperty("relations") + private List relations = Collections.emptyList(); + @JsonProperty("jsonId") + private String jsonId; + + public void setRelations(List relations) { + if (relations != null) { + this.relations = relations; + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CalculatedFieldDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CalculatedFieldDefinition.java new file mode 100644 index 0000000000..1dcc7a6bbd --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CalculatedFieldDefinition.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.cf.CalculatedField; + +@Data +@EqualsAndHashCode(callSuper = true) +public class CalculatedFieldDefinition extends CalculatedField { + + private Integer reprocessingOrder; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CustomerDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CustomerDefinition.java new file mode 100644 index 0000000000..3cc40c658c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CustomerDefinition.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.service.solutions.data.names.RandomNameData; + +import java.util.Collections; +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +public class CustomerDefinition extends BaseEntityDefinition { + + private String email; + private String country; + private String city; + private String state; + private String zip; + private String address; + private List users = Collections.emptyList(); + + @JsonIgnore + private RandomNameData randomNameData; + + @Override + public EntityType getEntityType() { + return EntityType.CUSTOMER; + } + + public void setUsers(List users) { + if (users != null) { + this.users = users; + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CustomerEntityDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CustomerEntityDefinition.java new file mode 100644 index 0000000000..d2da2f266c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/CustomerEntityDefinition.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public abstract class CustomerEntityDefinition extends BaseEntityDefinition { + + private String customer; + private boolean makePublic; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DashboardDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DashboardDefinition.java new file mode 100644 index 0000000000..a5a9e9c22b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DashboardDefinition.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.EntityType; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +public class DashboardDefinition extends CustomerEntityDefinition { + + private String file; + private boolean main; + + @Override + public EntityType getEntityType() { + return EntityType.DASHBOARD; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DashboardUserDetailsDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DashboardUserDetailsDefinition.java new file mode 100644 index 0000000000..2ec4701829 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DashboardUserDetailsDefinition.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import lombok.Data; + +@Data +public class DashboardUserDetailsDefinition { + + private String name; + private boolean fullScreen; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DeviceDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DeviceDefinition.java new file mode 100644 index 0000000000..81239c46dd --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DeviceDefinition.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.EntityType; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +public class DeviceDefinition extends CustomerEntityDefinition { + + private String type; + private String label; + private String emulator; + private JsonNode additionalInfo; + + @Override + public EntityType getEntityType() { + return EntityType.DEVICE; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DeviceProfileDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DeviceProfileDefinition.java new file mode 100644 index 0000000000..3dee5c179a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/DeviceProfileDefinition.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.DeviceProfile; + +@Data +@EqualsAndHashCode(callSuper = true) +public class DeviceProfileDefinition extends DeviceProfile { + + private String jsonId; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EdgeDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EdgeDefinition.java new file mode 100644 index 0000000000..5e8e7aa203 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EdgeDefinition.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.EntityType; + +import java.util.Collections; +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +public class EdgeDefinition extends CustomerEntityDefinition { + + private String type; + private String label; + private String rootRuleChainId; + private List ruleChainIds = Collections.emptyList(); + private List deviceIds = Collections.emptyList(); + private List assetIds = Collections.emptyList(); + private List dashboardIds = Collections.emptyList(); + + @Override + public EntityType getEntityType() { + return EntityType.EDGE; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EmulatorDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EmulatorDefinition.java new file mode 100644 index 0000000000..37c726e091 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EmulatorDefinition.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import lombok.Data; +import org.thingsboard.server.common.data.StringUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Data +public class EmulatorDefinition { + private String name; + private String extendz; + private String clazz; + private int publishPeriodInDays; + private int publishFrequencyInSeconds; + private int publishPauseInMillis; + private long activityPeriodInMillis; + private List telemetryProfiles = Collections.emptyList(); + + public void enrich(EmulatorDefinition parent) { + if (StringUtils.isEmpty(clazz)) { + clazz = parent.getClazz(); + } + if (publishPeriodInDays == 0) { + publishPeriodInDays = parent.getPublishPeriodInDays(); + } + if (publishFrequencyInSeconds == 0) { + publishFrequencyInSeconds = parent.getPublishFrequencyInSeconds(); + } + if (publishPauseInMillis == 0) { + publishPauseInMillis = parent.getPublishPauseInMillis(); + } + if (activityPeriodInMillis == 0L) { + activityPeriodInMillis = parent.getActivityPeriodInMillis(); + } + var profilesMap = telemetryProfiles.stream().collect(Collectors.toMap(TelemetryProfile::getKey, Function.identity())); + parent.getTelemetryProfiles().forEach(tp -> profilesMap.putIfAbsent(tp.getKey(), tp)); + telemetryProfiles = new ArrayList<>(profilesMap.values()); + } + + public long getOldestTs(long startTs) { + return startTs - TimeUnit.DAYS.toMillis(publishPeriodInDays) - publishFrequencyInSeconds; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EntityDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EntityDefinition.java new file mode 100644 index 0000000000..c76d183bda --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EntityDefinition.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import org.thingsboard.server.common.data.EntityType; + +public interface EntityDefinition { + + String getName(); + + EntityType getEntityType(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EntitySearchKey.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EntitySearchKey.java new file mode 100644 index 0000000000..f880e909b0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/EntitySearchKey.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; + +@Data +@AllArgsConstructor +public class EntitySearchKey { + + private final EntityId ownerId; + private final EntityType entityType; + private final String entityName; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/ReferenceableEntityDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/ReferenceableEntityDefinition.java new file mode 100644 index 0000000000..13dabb08cc --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/ReferenceableEntityDefinition.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ReferenceableEntityDefinition { + + private String name; + private String file; + private String update; + private String jsonId; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/RelationDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/RelationDefinition.java new file mode 100644 index 0000000000..670d3f1ab4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/RelationDefinition.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RelationDefinition { + + private EntityType entityType; + private String entityName; + private EntitySearchDirection direction; + private String type; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/TelemetryProfile.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/TelemetryProfile.java new file mode 100644 index 0000000000..259744a2f0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/TelemetryProfile.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.service.solutions.data.values.ValueStrategyDefinition; + +@Data +@AllArgsConstructor +public class TelemetryProfile { + + private String key; + private ValueStrategyDefinition valueStrategy; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/TenantDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/TenantDefinition.java new file mode 100644 index 0000000000..957547cff6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/TenantDefinition.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import lombok.Data; +import org.thingsboard.server.common.data.EntityType; + +@Data +public class TenantDefinition extends BaseEntityDefinition { + + @Override + public EntityType getEntityType() { + return EntityType.TENANT; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/UserDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/UserDefinition.java new file mode 100644 index 0000000000..1815cc6e57 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/definition/UserDefinition.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2026 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.solutions.data.definition; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.EntityType; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +public class UserDefinition extends BaseEntityDefinition { + + private String firstname; + private String lastname; + private String password; + private DashboardUserDetailsDefinition dashboard; + + @Override + public EntityType getEntityType() { + return EntityType.USER; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/AbstractEmulatorLauncher.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/AbstractEmulatorLauncher.java new file mode 100644 index 0000000000..abed74af3e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/AbstractEmulatorLauncher.java @@ -0,0 +1,195 @@ +/** + * Copyright © 2016-2026 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.solutions.data.emulator; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.cluster.TbClusterService; +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.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; +import org.thingsboard.server.common.data.msg.TbMsgType; +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.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.provider.TbQueueProducerProvider; +import org.thingsboard.server.service.solutions.data.definition.EmulatorDefinition; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +@Slf4j +@Data +public abstract class AbstractEmulatorLauncher & HasTenantId & HasName> { + + protected static final TbQueueCallback EMPTY_CALLBACK = new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + + } + + @Override + public void onFailure(Throwable t) { + + } + }; + protected final T entity; + protected final EmulatorDefinition emulatorDefinition; + protected final ExecutorService oldTelemetryExecutor; + protected final TbClusterService tbClusterService; + protected final PartitionService partitionService; + protected final TbQueueProducerProvider tbQueueProducerProvider; + protected final TbServiceInfoProvider serviceInfoProvider; + protected final TelemetrySubscriptionService tsSubService; + protected final long publishFrequency; + + private final Emulator emulator; + private ScheduledFuture> scheduledFuture; + + public AbstractEmulatorLauncher(T entity, + EmulatorDefinition emulatorDefinition, + ExecutorService oldTelemetryExecutor, + TbClusterService tbClusterService, + PartitionService partitionService, + TbQueueProducerProvider tbQueueProducerProvider, + TbServiceInfoProvider serviceInfoProvider, + TelemetrySubscriptionService tsSubService) throws Exception { + this.entity = entity; + this.emulatorDefinition = emulatorDefinition; + this.oldTelemetryExecutor = oldTelemetryExecutor; + this.tbClusterService = tbClusterService; + this.partitionService = partitionService; + this.tbQueueProducerProvider = tbQueueProducerProvider; + this.serviceInfoProvider = serviceInfoProvider; + this.tsSubService = tsSubService; + this.publishFrequency = TimeUnit.SECONDS.toMillis(Math.max(emulatorDefinition.getPublishFrequencyInSeconds(), 1)); + if (StringUtils.isEmpty(emulatorDefinition.getClazz())) { + emulator = new BasicEmulator(); + } else { + emulator = (Emulator) Class.forName(emulatorDefinition.getClazz()).getDeclaredConstructor().newInstance(); + } + emulator.init(emulatorDefinition); + } + + public CompletableFuture launch() { + final long oldestTs = emulatorDefinition.getOldestTs(System.currentTimeMillis()); + CompletableFuture future = new CompletableFuture<>(); + oldTelemetryExecutor.submit(() -> { + AtomicInteger pending = new AtomicInteger(1); + try { + if (emulator instanceof SimpleEmulator) { + if (oldestTs < (System.currentTimeMillis() - publishFrequency)) { + pushOldTelemetry(oldestTs, pending, future); + } + } else if (emulator instanceof CustomEmulator customEmulator) { + Pair telemetry = customEmulator.getNextValue(); + while (telemetry != null) { + pending.incrementAndGet(); + publishTelemetry(telemetry.getFirst(), telemetry.getSecond(), pending, future); + telemetry = customEmulator.getNextValue(); + } + } + } catch (Exception e) { + log.warn("Telemetry upload failed for {}", entity.getName(), e); + future.completeExceptionally(e); + } finally { + if (pending.decrementAndGet() == 0 && !future.isDone()) { + log.trace("[{}] Telemetry emulation finished. Producer and all callbacks completed successfully", entity.getName()); + future.complete(null); + } + } + + future.whenComplete((v, t) -> { + try { + postProcessEntity(entity); + } catch (Exception e) { + log.warn("Post-processing failed for {}", entity.getName(), e); + } + }); + + }); + return future; + } + + protected void postProcessEntity(T entity) { + } + + private void pushOldTelemetry(long oldestTs, AtomicInteger pending, CompletableFuture future) throws InterruptedException { + for (long ts = oldestTs; ts < System.currentTimeMillis(); ts += publishFrequency) { + pending.incrementAndGet(); + publishTelemetry(ts, pending, future); + } + } + + private void publishTelemetry(long ts, AtomicInteger pending, CompletableFuture future) throws InterruptedException { + ObjectNode values = ((SimpleEmulator) emulator).getValue(ts); + publishTelemetry(ts, values, pending, future); + Thread.sleep(emulatorDefinition.getPublishPauseInMillis()); + } + + public void stop() { + scheduledFuture.cancel(true); + } + + private void publishTelemetry(long ts, ObjectNode value, AtomicInteger pending, CompletableFuture future) { + String msgData = JacksonUtil.toString(value); + log.debug("[{}] Publishing telemetry: {}", entity.getName(), msgData); + TbMsgMetaData md = new TbMsgMetaData(); + md.putValue("ts", Long.toString(ts)); + TbMsg tbMsg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(entity.getId()) + .copyMetaData(md) + .dataType(TbMsgDataType.JSON) + .data(msgData) + .build(); + + TbQueueCallback callback = new TbQueueCallback() { + + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + log.debug("[{}] Successfully pushed message to Rule Engine for ts {}", entity.getName(), ts); + if (pending.decrementAndGet() == 0 && !future.isDone()) { + log.trace("[{}] Completing emulation future from callback for msg with ts: {}", entity.getName(), ts); + future.complete(null); + } + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Telemetry upload failed for ts {}", entity.getName(), ts, t); + if (pending.decrementAndGet() == 0 && !future.isDone()) { + future.completeExceptionally(t); + } + } + }; + + tbClusterService.pushMsgToRuleEngine(entity.getTenantId(), entity.getId(), tbMsg, callback); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/AssetEmulatorLauncher.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/AssetEmulatorLauncher.java new file mode 100644 index 0000000000..1f2d144b99 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/AssetEmulatorLauncher.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2026 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.solutions.data.emulator; + +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.provider.TbQueueProducerProvider; +import org.thingsboard.server.service.solutions.data.definition.EmulatorDefinition; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; + +import java.util.concurrent.ExecutorService; + +@Slf4j +public class AssetEmulatorLauncher extends AbstractEmulatorLauncher { + + @Builder + public AssetEmulatorLauncher(Asset entity, EmulatorDefinition emulatorDefinition, ExecutorService oldTelemetryExecutor, TbClusterService tbClusterService, + PartitionService partitionService, + TbQueueProducerProvider tbQueueProducerProvider, + TbServiceInfoProvider serviceInfoProvider, + TelemetrySubscriptionService tsSubService) throws Exception { + super(entity, emulatorDefinition, oldTelemetryExecutor, tbClusterService, partitionService, tbQueueProducerProvider, serviceInfoProvider, tsSubService); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/BasicEmulator.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/BasicEmulator.java new file mode 100644 index 0000000000..c81aed3c02 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/BasicEmulator.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2026 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.solutions.data.emulator; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.service.solutions.data.definition.EmulatorDefinition; +import org.thingsboard.server.service.solutions.data.values.TelemetryGenerator; +import org.thingsboard.server.service.solutions.data.values.TelemetryGeneratorFactory; + +import java.util.HashMap; +import java.util.Map; + +public class BasicEmulator implements SimpleEmulator { + + private final Map tsGenerators = new HashMap<>(); + + @Override + public void init(EmulatorDefinition emulatorDefinition) { + emulatorDefinition.getTelemetryProfiles().forEach(tp -> tsGenerators.put(tp.getKey(), TelemetryGeneratorFactory.create(tp))); + } + + @Override + public ObjectNode getValue(long ts) { + ObjectNode values = JacksonUtil.newObjectNode(); + tsGenerators.values().forEach(gen -> gen.addValue(ts, values)); + return values; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/CustomEmulator.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/CustomEmulator.java new file mode 100644 index 0000000000..627fe4e94d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/CustomEmulator.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2026 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.solutions.data.emulator; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.data.util.Pair; + +public interface CustomEmulator extends Emulator { + + Pair getNextValue(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/DeviceEmulatorLauncher.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/DeviceEmulatorLauncher.java new file mode 100644 index 0000000000..62fe03aa31 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/DeviceEmulatorLauncher.java @@ -0,0 +1,98 @@ +/** + * Copyright © 2016-2026 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.solutions.data.emulator; + +import com.google.common.util.concurrent.FutureCallback; +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.provider.TbQueueProducerProvider; +import org.thingsboard.server.service.solutions.data.definition.EmulatorDefinition; +import org.thingsboard.server.service.state.DefaultDeviceStateService; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; + +import java.util.UUID; +import java.util.concurrent.ExecutorService; + +@Slf4j +public class DeviceEmulatorLauncher extends AbstractEmulatorLauncher { + + @Builder + public DeviceEmulatorLauncher(Device entity, + EmulatorDefinition emulatorDefinition, + ExecutorService oldTelemetryExecutor, + TbClusterService tbClusterService, + PartitionService partitionService, + TbQueueProducerProvider tbQueueProducerProvider, + TbServiceInfoProvider serviceInfoProvider, + TelemetrySubscriptionService tsSubService) throws Exception { + super(entity, emulatorDefinition, oldTelemetryExecutor, tbClusterService, partitionService, tbQueueProducerProvider, serviceInfoProvider, tsSubService); + } + + @Override + protected void postProcessEntity(Device entity) { + if (this.emulatorDefinition.getActivityPeriodInMillis() > 0) { + tsSubService.saveAttributes(AttributesSaveRequest.builder() + .tenantId(entity.getTenantId()) + .entityId(entity.getId()) + .scope(AttributeScope.SERVER_SCOPE) + .entry(new LongDataEntry(DefaultDeviceStateService.INACTIVITY_TIMEOUT, this.emulatorDefinition.getActivityPeriodInMillis())) + .callback(new FutureCallback<>() { + @Override + public void onSuccess(@Nullable Void unused) { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, entity.getTenantId(), entity.getId()); + UUID sessionId = UUID.randomUUID(); + TransportProtos.TransportToDeviceActorMsg msg = TransportProtos.TransportToDeviceActorMsg.newBuilder() + .setSessionInfo(TransportProtos.SessionInfoProto.newBuilder() + .setSessionIdMSB(sessionId.getMostSignificantBits()) + .setSessionIdLSB(sessionId.getLeastSignificantBits()) + .setDeviceIdMSB(entity.getId().getId().getMostSignificantBits()) + .setDeviceIdLSB(entity.getId().getId().getLeastSignificantBits()) + .setDeviceProfileIdMSB(entity.getId().getId().getMostSignificantBits()) + .setDeviceProfileIdLSB(entity.getId().getId().getLeastSignificantBits()) + .setDeviceName(entity.getName()) + .setDeviceType(entity.getType()) + .setTenantIdMSB(entity.getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(entity.getTenantId().getId().getLeastSignificantBits()) + .setNodeId(serviceInfoProvider.getServiceId()) + .build()) + .setSubscriptionInfo(TransportProtos.SubscriptionInfoProto.newBuilder().setLastActivityTime(System.currentTimeMillis()).build()) + .build(); + tbQueueProducerProvider.getTbCoreMsgProducer().send(tpi, + new TbProtoQueueMsg<>(entity.getUuidId(), + TransportProtos.ToCoreMsg.newBuilder().setToDeviceActorMsg(msg).build()), EMPTY_CALLBACK + ); + } + + @Override + public void onFailure(Throwable throwable) { + } + }) + .build()); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/Emulator.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/Emulator.java new file mode 100644 index 0000000000..45a9a029b8 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/Emulator.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2026 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.solutions.data.emulator; + +import org.thingsboard.server.service.solutions.data.definition.EmulatorDefinition; + +public interface Emulator { + + void init(EmulatorDefinition emulatorDefinition); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/FieldIrrigationStateEmulator.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/FieldIrrigationStateEmulator.java new file mode 100644 index 0000000000..44c9eb550f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/FieldIrrigationStateEmulator.java @@ -0,0 +1,108 @@ +/** + * Copyright © 2016-2026 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.solutions.data.emulator; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.data.util.Pair; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.service.solutions.data.definition.EmulatorDefinition; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.TimeZone; + +public class FieldIrrigationStateEmulator implements CustomEmulator { + + private static final int START_HOUR = 6; + private int idx; + private List> data = new ArrayList<>(); + + @Override + public void init(EmulatorDefinition emulatorDefinition) { + idx = 0; + var tz = TimeZone.getTimeZone("America/New_York"); + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(System.currentTimeMillis()); + c.setTimeZone(tz); + int curHour = c.get(Calendar.HOUR_OF_DAY); + c.add(Calendar.DAY_OF_MONTH, curHour < START_HOUR ? -7 : -6); + c.set(Calendar.HOUR_OF_DAY, START_HOUR); + c.set(Calendar.MINUTE, 0); + + add(c, JacksonUtil.newObjectNode() + .put("startTs", c.getTimeInMillis()) + .put("durationThreshold", 30 * 60000) + .put("consumption", 423) + .put("duration", 30 * 60000) + ); + add(c, JacksonUtil.newObjectNode() + .put("startTs", c.getTimeInMillis()) + .put("durationThreshold", 30 * 60000) + .put("consumption", 407) + .put("duration", 30 * 60000) + ); + add(c, JacksonUtil.newObjectNode() + .put("startTs", c.getTimeInMillis()) + .put("consumptionThreshold", 1000) + .put("consumption", 1000) + .put("duration", 63 * 60000) + ); + add(c, JacksonUtil.newObjectNode() + .put("startTs", c.getTimeInMillis()) + .put("consumptionThreshold", 500) + .put("consumption", 500) + .put("duration", 36 * 60000) + ); + add(c, JacksonUtil.newObjectNode() + .put("startTs", c.getTimeInMillis()) + .put("consumptionThreshold", 1000) + .put("consumption", 1000) + .put("duration", 63 * 60000) + ); + add(c, JacksonUtil.newObjectNode() + .put("startTs", c.getTimeInMillis()) + .put("durationThreshold", 30 * 60000) + .put("duration", 30 * 60000) + .put("consumption", 452) + ); + add(c, JacksonUtil.newObjectNode() + .put("startTs", c.getTimeInMillis()) + .put("durationThreshold", 30 * 60000) + .put("duration", 30 * 60000) + .put("consumption", 447) + ); + } + + private void add(Calendar c, ObjectNode objectNode) { + ObjectNode startIrrigationMsg = JacksonUtil.newObjectNode(); + startIrrigationMsg.put("irrigationState", "DONE"); + startIrrigationMsg.set("irrigationTask", objectNode); + data.add(Pair.of(c.getTimeInMillis(), startIrrigationMsg)); + c.add(Calendar.DAY_OF_MONTH, 1); + } + + @Override + public Pair getNextValue() { + if (idx < data.size()) { + var result = data.get(idx); + idx++; + return result; + } else { + return null; + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/SimpleEmulator.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/SimpleEmulator.java new file mode 100644 index 0000000000..144a5f7f26 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/emulator/SimpleEmulator.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2026 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.solutions.data.emulator; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +public interface SimpleEmulator extends Emulator { + + ObjectNode getValue(long ts); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/names/RandomNameData.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/names/RandomNameData.java new file mode 100644 index 0000000000..ca66c37199 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/names/RandomNameData.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2026 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.solutions.data.names; + +import lombok.Data; + +@Data +public class RandomNameData { + + private final String firstName; + private final String lastName; + private final String email; +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/names/RandomNameUtil.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/names/RandomNameUtil.java new file mode 100644 index 0000000000..50bc3b9dc5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/names/RandomNameUtil.java @@ -0,0 +1,100 @@ +/** + * Copyright © 2016-2026 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.solutions.data.names; + +import org.thingsboard.server.common.data.StringUtils; + +import java.util.Random; + +public class RandomNameUtil { + + private static final Random RANDOM = new Random(System.currentTimeMillis()); + + private static String[] FIRST_NAMES = new String[]{ + "Nadia", "Lynne", "Lera", "Holly", "Zina", "Mandi", "Kasie", "Josef", "Liane", "Man", + "Karan", "Nga", "Tesha", "Elva", "Bree", "Ida", "Mimi", "Altha", "Omar", "Ollie", + "Nicol", "Ned", "Teri", "Sabra", "Kari", "Eva", "Penni", "Simon", "Kaci", "Jess", + "Bea", "Ron", "Melba", "Vella", "Nada", "Cyndi", "Audry", "Helga", "Chana", "Lola", + "Terry", "Tami", "Dedra", "Erwin", "Anne", "Zoila", "Nelia", "Hyman", "Deane", "Erica", + "Amos", "Maura", "Gwenn", "Evan", "Lelia", "Grant", "Abe", "Fanny", "Cindi", "Pilar", + "Darcy", "Len", "Casie", "Jose", "Kylie", "Cami", "Casey", "Kerri", "Bruno", "Theda", + "Ardis", "Carin", "Belia", "Jeff", "Yetta", "Ola", "Lyla", "Megan", "Zita", "Rocky", + "Darci", "Dale", "Mirta", "Tanja", "Stacy", "Julie", "Aisha", "Avis", "Hugh", "Anh", + "Bud", "Earle", "Ossie", "Odell", "Dovie", "Afton", "Joel", "Aida", "Vina", "Emiko", + "Tori", "Keena", "Addie", "Nancy", "Tonda", "Josue", "Dina", "Mitch", "Thea", "Cole", + "Lyman", "Donna", "Roma", "Deena", "Lue", "Bette", "Ilene", "Vera", "Kirby", "Lenna", + "Pat", "Grace", "Gus", "Frank", "Lena", "Adele", "Kerry", "Santo", "Wade", "Trudi", + "Bruce", "Raul", "Katy", "Heidi", "Marna", "Faith", "Ronna", "Kylee", "Emma", "Luna", + "Ilda", "Mose", "Juan", "Cori", "Gayla", "Otha", "Jung", "Bambi", "Joi", "Sibyl", + "Reid", "Bonny", "Roni", "Joy", "Farah", "Jeane", "Jill", "Tisha", "Leon", "Leigh", + "Inge", "Zella", "Rubin", "Carri", "Nana", "Sofia", "Lucio", "Eboni", "Adam", "Lino", + "Elvis", "Marci", "Adina", "Nanci", "Joyce", "James", "Louis", "Aurea", "Chi", "Kathy", + "Nina", "Lise", "Reda", "Candi", "Ralph", "Velva", "Mari", "Mavis", "Kory", "Tammi", + "Lucy", "Lavon", "Ana", "Lin", "Alena", "Haley", "Myra", "Amy", "Marlo", "Ami" + }; + + private static String[] LAST_NAMES = new String[]{ + "Vinson", "Rogers", "Burkett", "Gamboa", "Gross", "Toledo", "Kiser", "Harman", "Pierce", "Lovett", + "Sexton", "Coates", "Seymour", "Holland", "Finley", "Hagen", "Schmitt", "Beach", "Rea", "Peck", + "Noel", "Archer", "Zamora", "Wood", "Rivera", "Lentz", "Alonso", "Rider", "Story", "Schwab", + "Mercado", "Caruso", "Bynum", "Spears", "Bingham", "Tyler", "Ponce", "Skaggs", "Matos", "Stinson", + "Burgos", "Coles", "Helton", "Joyce", "Galvan", "Grant", "Stahl", "Daigle", "Hayden", "Quiroz", + "Horne", "Shaw", "Addison", "Harvey", "Watkins", "Romano", "Ahmed", "Lind", "Swenson", "Kemp", + "Barnes", "Horton", "Dugan", "Farr", "Swanson", "Hale", "Hidalgo", "Schulte", "Ervin", "Osorio", + "Lincoln", "Zhang", "Oakes", "Stiles", "Lu", "Culver", "Singer", "Knott", "Bullard", "Butts", + "Coley", "Roth", "Cobb", "Darnell", "Vaughan", "Conrad", "Austin", "Rico", "Hobson", "Brunner", + "Mccray", "Mattson", "Barber", "Clement", "Ly", "Dejesus", "Painter", "Macias", "Healy", "Duncan", + "Forrest", "Hurley", "Torres", "Blue", "Rucker", "Ferrell", "Maddox", "Osborne", "Holt", "Briggs", + "Irvin", "Bassett", "Urban", "Overton", "Sloan", "Guzman", "Estrada", "Redding", "Gold", "Paz", + "Lucero", "Reyes", "Conner", "Keith", "Hays", "Hutton", "Moore", "Tuttle", "Powers", "William", + "Reeder", "Castro", "Vela", "Baca", "Frazier", "Kline", "Huggins", "Zepeda", "Pate", "Marion", + "Hollis", "Ybarra", "Cuellar", "Gallo", "Gomes", "Louis", "Bolden", "Liu", "Hayes", "Simpson", + "Otero", "Conway", "Peoples", "Mohr", "Loomis", "Engel", "Hughes", "Chapman", "Bower", "Schmidt", + "Mcmahon", "Booth", "Ward", "Drake", "Cutler", "Gordon", "Black", "Clifton", "Curran", "Pierre", + "Workman", "Tilley", "Moody", "Kuhn", "Eastman", "Scott", "Vang", "Tillman", "Cleary", "Yi", + "Griffin", "Mendoza", "Mason", "Pittman", "Landis", "Gay", "Rowland", "Jordan", "Lo", "Kumar", + "Vernon", "Boyd", "Rocha", "Mcqueen", "Cornett", "Deaton", "Borden", "Lacey", "Gaston", "Miles" + }; + + public static String nextFirstName() { + return randomElement(FIRST_NAMES); + } + + public static String nextLastName() { + return randomElement(LAST_NAMES); + } + + private static String toEmail(String firstName, String lastName) { + return firstName.toLowerCase() + "." + lastName.toLowerCase() + "@thingsboard.io"; + } + + public static RandomNameData next() { + var firstName = nextFirstName(); + var lastName = nextLastName(); + return new RandomNameData(firstName, lastName, toEmail(firstName, lastName)); + } + + public static RandomNameData nextSuperRandom() { + var firstName = nextFirstName() + StringUtils.randomAlphanumeric(10).toLowerCase(); + var lastName = nextLastName() + StringUtils.randomAlphanumeric(10).toLowerCase(); + return new RandomNameData(firstName, lastName, toEmail(firstName, lastName)); + } + + private static String randomElement(String[] array) { + return array[RANDOM.nextInt(array.length)]; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/solution/SolutionInstallResponse.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/solution/SolutionInstallResponse.java new file mode 100644 index 0000000000..a122a562a9 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/solution/SolutionInstallResponse.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2026 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.solutions.data.solution; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.Collections; +import java.util.List; + +@Schema +@Data +public class SolutionInstallResponse extends TenantSolutionTemplateInstructions { + + @Schema(description = "Indicates that template was installed successfully") + private boolean success; + @Schema(description = "List of entity IDs created during solution installation") + private List createdEntityIds; + @Schema(description = "What keys to delete during template uninstall") + private List tenantTelemetryKeys; + @Schema(description = "What attributes to delete during template uninstall") + private List tenantAttributeKeys; + + public SolutionInstallResponse(TenantSolutionTemplateInstructions instructions, boolean success, List createdEntityIds) { + this(instructions, success, createdEntityIds, Collections.emptyList(), Collections.emptyList()); + } + + public SolutionInstallResponse(TenantSolutionTemplateInstructions instructions, boolean success, List createdEntityIds, + List tenantTelemetryKeys, List tenantAttributeKeys) { + super(instructions); + this.success = success; + this.createdEntityIds = createdEntityIds; + this.tenantTelemetryKeys = tenantTelemetryKeys; + this.tenantAttributeKeys = tenantAttributeKeys; + } + + public SolutionInstallResponse() { + super(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/solution/TenantSolutionTemplateInstructions.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/solution/TenantSolutionTemplateInstructions.java new file mode 100644 index 0000000000..fabd89d072 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/solution/TenantSolutionTemplateInstructions.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2026 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.solutions.data.solution; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; + +@Schema +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TenantSolutionTemplateInstructions { + + @Schema(description = "Id of the main dashboard of the solution") + private DashboardId dashboardId; + @Schema(description = "Id of the public customer if solution has public entities") + private CustomerId publicId; + @Schema(description = "Is the main dashboard public") + private boolean mainDashboardPublic; + @Schema(description = "Markdown with solution usage instructions") + private String details; + + public TenantSolutionTemplateInstructions(TenantSolutionTemplateInstructions instructions) { + this.dashboardId = instructions.getDashboardId(); + this.publicId = instructions.getPublicId(); + this.mainDashboardPublic = instructions.isMainDashboardPublic(); + this.details = instructions.getDetails(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/CompositeValueStrategyDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/CompositeValueStrategyDefinition.java new file mode 100644 index 0000000000..ba803faf87 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/CompositeValueStrategyDefinition.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class CompositeValueStrategyDefinition implements ValueStrategyDefinition { + + private ValueStrategyDefinition defaultHours; + private ValueStrategyDefinition workHours; + private ValueStrategyDefinition nightHours; + private ValueStrategyDefinition holidayHours; + + @Override + public ValueStrategyDefinitionType getStrategyType() { + return ValueStrategyDefinitionType.COMPOSITE; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/CompositeValueStrategyGenerator.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/CompositeValueStrategyGenerator.java new file mode 100644 index 0000000000..6cbfaf550e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/CompositeValueStrategyGenerator.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile; + +public class CompositeValueStrategyGenerator extends TelemetryGenerator { + + TelemetryGenerator defaultGenerator; + TelemetryGenerator whGenerator; + TelemetryGenerator nhGenerator; + TelemetryGenerator hhGenerator; + + public CompositeValueStrategyGenerator(TelemetryProfile tp) { + super(tp); + var def = (CompositeValueStrategyDefinition) tp.getValueStrategy(); + defaultGenerator = TelemetryGeneratorFactory.create(new TelemetryProfile(tp.getKey(), def.getDefaultHours())); + if (def.getWorkHours() != null) { + whGenerator = TelemetryGeneratorFactory.create(new TelemetryProfile(tp.getKey(), def.getWorkHours())); + } + if (def.getNightHours() != null) { + nhGenerator = TelemetryGeneratorFactory.create(new TelemetryProfile(tp.getKey(), def.getNightHours())); + } + if (def.getHolidayHours() != null) { + hhGenerator = TelemetryGeneratorFactory.create(new TelemetryProfile(tp.getKey(), def.getHolidayHours())); + } + } + + @Override + public void addValue(long ts, ObjectNode values) { + if (hhGenerator != null && GeneratorTools.isHoliday(ts)) { + hhGenerator.addValue(ts, values); + } else if (nhGenerator != null && GeneratorTools.isNightHour(ts)) { + nhGenerator.addValue(ts, values); + } else if (whGenerator != null && GeneratorTools.isWorkHour(ts)) { + whGenerator.addValue(ts, values); + } else { + defaultGenerator.addValue(ts, values); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ConstantTelemetryGenerator.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ConstantTelemetryGenerator.java new file mode 100644 index 0000000000..59ca319f67 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ConstantTelemetryGenerator.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile; + +public class ConstantTelemetryGenerator extends TelemetryGenerator { + + private final ConstantValueStrategyDefinition strategy; + private JsonNode value; + + public ConstantTelemetryGenerator(TelemetryProfile telemetryProfile) { + super(telemetryProfile); + this.strategy = (ConstantValueStrategyDefinition) telemetryProfile.getValueStrategy(); + this.value = strategy.getValue(); + } + + @Override + public void addValue(long ts, ObjectNode values) { + values.set(key, value); + } + + @Override + public double getValue() { + if (value != null && value.isNumber()) { + return value.doubleValue(); + } else { + return super.getValue(); + } + } + + @Override + public void setValue(double value) { + // do nothing; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ConstantValueStrategyDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ConstantValueStrategyDefinition.java new file mode 100644 index 0000000000..ef18ef2c9b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ConstantValueStrategyDefinition.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class ConstantValueStrategyDefinition implements ValueStrategyDefinition { + private JsonNode value; + + @Override + public ValueStrategyDefinitionType getStrategyType() { + return ValueStrategyDefinitionType.CONSTANT; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/CounterTelemetryGenerator.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/CounterTelemetryGenerator.java new file mode 100644 index 0000000000..544d143893 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/CounterTelemetryGenerator.java @@ -0,0 +1,69 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Getter; +import lombok.Setter; +import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import static org.thingsboard.server.service.solutions.data.values.GeneratorTools.getMultiplier; +import static org.thingsboard.server.service.solutions.data.values.GeneratorTools.randomDouble; + +public class CounterTelemetryGenerator extends TelemetryGenerator { + + private final CounterValueStrategyDefinition strategy; + @Getter @Setter + private double value; + + public CounterTelemetryGenerator(TelemetryProfile telemetryProfile) { + super(telemetryProfile); + this.strategy = (CounterValueStrategyDefinition) telemetryProfile.getValueStrategy(); + this.value = getRandomStartValue(); + } + + @Override + public void addValue(long ts, ObjectNode values) { + double step = randomDouble(strategy.getMinIncrement(), strategy.getMaxIncrement()); + double multiplier = getMultiplier(ts, strategy.getHolidayMultiplier(), strategy.getWorkHoursMultiplier(), strategy.getNightHoursMultiplier()); + value += step * multiplier; + if (value > getRandomEndValue()) { + value = getRandomStartValue(); + } + put(values, value); + } + + private void put(ObjectNode values, double value) { + if (strategy.getPrecision() == 0) { + values.put(key, (int) value); + } else { + values.put(key, BigDecimal.valueOf(value) + .setScale(strategy.getPrecision(), RoundingMode.HALF_UP) + .doubleValue()); + } + } + + public double getRandomStartValue() { + return randomDouble(strategy.getMinStartValue(), strategy.getMaxStartValue()); + } + + public double getRandomEndValue() { + return randomDouble(strategy.getMinEndValue(), strategy.getMaxEndValue()); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/CounterValueStrategyDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/CounterValueStrategyDefinition.java new file mode 100644 index 0000000000..9010af51bd --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/CounterValueStrategyDefinition.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class CounterValueStrategyDefinition implements ValueStrategyDefinition { + + private int precision; + private double minStartValue; + private double maxStartValue; + private double minEndValue; + private double maxEndValue; + private double minIncrement; + private double maxIncrement; + private double holidayMultiplier; + private double workHoursMultiplier; + private double nightHoursMultiplier; + + @Override + public ValueStrategyDefinitionType getStrategyType() { + return ValueStrategyDefinitionType.COUNTER; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/DecrementTelemetryGenerator.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/DecrementTelemetryGenerator.java new file mode 100644 index 0000000000..bbcb6cedf7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/DecrementTelemetryGenerator.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile; + +import static org.thingsboard.server.service.solutions.data.values.GeneratorTools.randomDouble; + +public class DecrementTelemetryGenerator extends IncDecTelemetryGenerator { + + public DecrementTelemetryGenerator(TelemetryProfile telemetryProfile) { + super(telemetryProfile); + } + + @Override + public void addValue(long ts, ObjectNode values) { + double step = randomDouble(strategy.getMinDecrement(), strategy.getMaxDecrement()); + double newValue = value - step; + value = Math.max(newValue, endValue); + put(values, value); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/DecrementValueStrategyDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/DecrementValueStrategyDefinition.java new file mode 100644 index 0000000000..b5f254e2e5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/DecrementValueStrategyDefinition.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class DecrementValueStrategyDefinition extends IncDecValueStrategyDefinition { + + private double minDecrement; + private double maxDecrement; + + @Override + public ValueStrategyDefinitionType getStrategyType() { + return ValueStrategyDefinitionType.DECREMENT; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/EventTelemetryGenerator.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/EventTelemetryGenerator.java new file mode 100644 index 0000000000..82555f7ed4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/EventTelemetryGenerator.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile; + +import static org.thingsboard.server.service.solutions.data.values.GeneratorTools.randomLong; + + +public class EventTelemetryGenerator extends TelemetryGenerator { + + private final EventValueStrategyDefinition strategy; + private final long currentAnomaly; + private Object value; + + public EventTelemetryGenerator(TelemetryProfile telemetryProfile) { + super(telemetryProfile); + this.strategy = (EventValueStrategyDefinition) telemetryProfile.getValueStrategy(); + currentAnomaly = randomLong(0, strategy.getAnomalyChance()); + } + + @Override + public void addValue(long ts, ObjectNode values) { + boolean anomaly = false; + if (randomLong(0, strategy.getAnomalyChance()) == currentAnomaly) { + anomaly = true; + } + this.value = anomaly ? strategy.getAnomalyValue() : strategy.getNormalValue(); + if (value instanceof Boolean) { + values.put(key, (Boolean) value); + } else if (value instanceof Double) { + values.put(key, ((Double) value)); + } else if (value instanceof Integer) { + values.put(key, ((Integer) value)); + } else { + throw new RuntimeException("Not supported value for event telemetry generator: " + value); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/EventValueStrategyDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/EventValueStrategyDefinition.java new file mode 100644 index 0000000000..855004da02 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/EventValueStrategyDefinition.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class EventValueStrategyDefinition implements ValueStrategyDefinition { + private Object normalValue; + private Object anomalyValue; + private double anomalyChance; + + @Override + public ValueStrategyDefinitionType getStrategyType() { + return ValueStrategyDefinitionType.EVENT; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/GeneratorTools.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/GeneratorTools.java new file mode 100644 index 0000000000..918379faa6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/GeneratorTools.java @@ -0,0 +1,96 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import java.util.Calendar; +import java.util.Date; +import java.util.Random; +import java.util.TimeZone; + +public class GeneratorTools { + + private static final Random random = new Random(); + + public static long randomLong(double min, double max) { + return (long) randomDouble(min, max); + } + + public static double randomDouble(double min, double max) { + return min + random.nextDouble() * Math.abs(max - min); + } + + public static double getMultiplier(long ts, double holidayMultiplier, double workHoursMultiplier, double nightHoursMultiplier) { + Date date = new Date(ts); + Calendar c = Calendar.getInstance(); + c.setTime(date); + int dayOfWeek = c.get(Calendar.DAY_OF_WEEK); + int hour = c.get(Calendar.HOUR_OF_DAY); + double multiplier = 1.0; + if (dayOfWeek == 1 || dayOfWeek == 7) { + multiplier *= holidayMultiplier; + } + + if (hour > 8 && hour < 18) { + multiplier *= workHoursMultiplier; + } else if (hour < 6 || hour > 22) { + multiplier *= nightHoursMultiplier; + } + return multiplier; + } + + public static int getHour(TimeZone tz, long ts) { + Date date = new Date(ts); + Calendar c = Calendar.getInstance(); + c.setTimeZone(tz); + c.setTime(date); + return c.get(Calendar.HOUR_OF_DAY); + } + + public static int getMinute(TimeZone tz, long ts) { + Date date = new Date(ts); + Calendar c = Calendar.getInstance(); + c.setTimeZone(tz); + c.setTime(date); + return c.get(Calendar.MINUTE); + } + + public static boolean isHoliday(long ts) { + Date date = new Date(ts); + Calendar c = Calendar.getInstance(); + c.setTime(date); + int dayOfWeek = c.get(Calendar.DAY_OF_WEEK); + return dayOfWeek == 1 || dayOfWeek == 7; + } + + public static boolean isWorkHour(long ts) { + Date date = new Date(ts); + Calendar c = Calendar.getInstance(); + c.setTime(date); + int hour = c.get(Calendar.HOUR_OF_DAY); + + return hour > 8 && hour < 18; + } + + public static boolean isNightHour(long ts) { + Date date = new Date(ts); + Calendar c = Calendar.getInstance(); + c.setTime(date); + int hour = c.get(Calendar.HOUR_OF_DAY); + + return hour < 6 || hour >= 22; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncDecTelemetryGenerator.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncDecTelemetryGenerator.java new file mode 100644 index 0000000000..09d7e2dce2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncDecTelemetryGenerator.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Getter; +import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import static org.thingsboard.server.service.solutions.data.values.GeneratorTools.randomDouble; + +public abstract class IncDecTelemetryGenerator extends TelemetryGenerator { + + protected final T strategy; + @Getter + protected double value; + protected double endValue; + + @SuppressWarnings("unchecked") + public IncDecTelemetryGenerator(TelemetryProfile telemetryProfile) { + super(telemetryProfile); + this.strategy = (T) telemetryProfile.getValueStrategy(); + this.value = getRandomStartValue(); + this.endValue = getRandomEndValue(); + } + + public void setValue(double value) { + this.value = value; + this.endValue = getRandomEndValue(); + } + + protected void put(ObjectNode values, double value) { + if (strategy.getPrecision() == 0) { + values.put(key, (int) value); + } else { + values.put(key, BigDecimal.valueOf(value) + .setScale(strategy.getPrecision(), RoundingMode.HALF_UP) + .doubleValue()); + } + } + + public double getRandomStartValue() { + return randomDouble(strategy.getMinStartValue(), strategy.getMaxStartValue()); + } + + public double getRandomEndValue() { + return randomDouble(strategy.getMinEndValue(), strategy.getMaxEndValue()); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncDecValueStrategyDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncDecValueStrategyDefinition.java new file mode 100644 index 0000000000..adc3100dab --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncDecValueStrategyDefinition.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class IncDecValueStrategyDefinition implements ValueStrategyDefinition { + + private int precision; + private double minStartValue; + private double maxStartValue; + private double minEndValue; + private double maxEndValue; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncrementTelemetryGenerator.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncrementTelemetryGenerator.java new file mode 100644 index 0000000000..d18f36017f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncrementTelemetryGenerator.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile; + +import static org.thingsboard.server.service.solutions.data.values.GeneratorTools.randomDouble; + +public class IncrementTelemetryGenerator extends IncDecTelemetryGenerator { + + public IncrementTelemetryGenerator(TelemetryProfile telemetryProfile) { + super(telemetryProfile); + } + + @Override + public void addValue(long ts, ObjectNode values) { + double step = randomDouble(strategy.getMinIncrement(), strategy.getMaxIncrement()); + double newValue = value + step; + value = Math.min(newValue, endValue); + put(values, value); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncrementValueStrategyDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncrementValueStrategyDefinition.java new file mode 100644 index 0000000000..64d7bc0b4c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/IncrementValueStrategyDefinition.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class IncrementValueStrategyDefinition extends IncDecValueStrategyDefinition { + + private double minIncrement; + private double maxIncrement; + + @Override + public ValueStrategyDefinitionType getStrategyType() { + return ValueStrategyDefinitionType.INCREMENT; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/NaturalTelemetryGenerator.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/NaturalTelemetryGenerator.java new file mode 100644 index 0000000000..927eb5a76f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/NaturalTelemetryGenerator.java @@ -0,0 +1,94 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Getter; +import lombok.Setter; +import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import static org.thingsboard.server.service.solutions.data.values.GeneratorTools.getMultiplier; +import static org.thingsboard.server.service.solutions.data.values.GeneratorTools.randomDouble; + +public class NaturalTelemetryGenerator extends TelemetryGenerator { + + private final NaturalValueStrategyDefinition strategy; + @Getter @Setter + private double value; + private boolean isIncrement; + private double lowValue; + private double highValue; + + public NaturalTelemetryGenerator(TelemetryProfile telemetryProfile) { + super(telemetryProfile); + this.strategy = (NaturalValueStrategyDefinition) telemetryProfile.getValueStrategy(); + this.value = getRandomStartValue(); + this.lowValue = getRandomLowValue(); + this.highValue = getRandomHighValue(); + isIncrement = !strategy.isDecrementOnStart(); + } + + @Override + public void addValue(long ts, ObjectNode values) { + + double multiplier = getMultiplier(ts, strategy.getHolidayMultiplier(), strategy.getWorkHoursMultiplier(), strategy.getNightHoursMultiplier()); + + if (isIncrement) { + double step = randomDouble(strategy.getMinIncrement(), strategy.getMaxIncrement()); + value += step * multiplier; + if (value > highValue) { + value = highValue; + highValue = getRandomHighValue(); + isIncrement = false; + } + } else { + double step = randomDouble(strategy.getMinDecrement(), strategy.getMaxDecrement()); + value -= step * multiplier; + if (value < lowValue) { + value = lowValue; + lowValue = getRandomLowValue(); + isIncrement = true; + } + } + + put(values, value); + } + + private void put(ObjectNode values, double value) { + if (strategy.getPrecision() == 0) { + values.put(key, (int) value); + } else { + values.put(key, BigDecimal.valueOf(value) + .setScale(strategy.getPrecision(), RoundingMode.HALF_UP) + .doubleValue()); + } + } + + public double getRandomStartValue() { + return randomDouble(strategy.getMinStartValue(), strategy.getMaxStartValue()); + } + + public double getRandomLowValue() { + return randomDouble(strategy.getMinLowValue(), strategy.getMaxLowValue()); + } + + public double getRandomHighValue() { + return randomDouble(strategy.getMinHighValue(), strategy.getMaxHighValue()); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/NaturalValueStrategyDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/NaturalValueStrategyDefinition.java new file mode 100644 index 0000000000..3141baa47a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/NaturalValueStrategyDefinition.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class NaturalValueStrategyDefinition implements ValueStrategyDefinition { + private int precision; + private double minStartValue; + private double maxStartValue; + private double minLowValue; + private double maxLowValue; + private double minHighValue; + private double maxHighValue; + private double minIncrement; + private double maxIncrement; + private double minDecrement; + private double maxDecrement; + private double holidayMultiplier; + private double workHoursMultiplier; + private double nightHoursMultiplier; + private boolean decrementOnStart; + + @Override + public ValueStrategyDefinitionType getStrategyType() { + return ValueStrategyDefinitionType.NATURAL; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ScheduleValueStrategyDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ScheduleValueStrategyDefinition.java new file mode 100644 index 0000000000..4804863bd3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ScheduleValueStrategyDefinition.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class ScheduleValueStrategyDefinition implements ValueStrategyDefinition { + + private List schedule; + private String timeZone; + private ValueStrategyDefinition defaultDefinition; + + @Override + public ValueStrategyDefinitionType getStrategyType() { + return ValueStrategyDefinitionType.SCHEDULE; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ScheduleValueStrategyGenerator.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ScheduleValueStrategyGenerator.java new file mode 100644 index 0000000000..1d067a3044 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ScheduleValueStrategyGenerator.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TimeZone; + +public class ScheduleValueStrategyGenerator extends TelemetryGenerator { + + private TelemetryGenerator defaultGenerator; + private TimeZone timeZone; + private Map scheduleGenerators; + private TelemetryGenerator prevGenerator; + + public ScheduleValueStrategyGenerator(TelemetryProfile tp) { + super(tp); + var def = (ScheduleValueStrategyDefinition) tp.getValueStrategy(); + timeZone = TimeZone.getTimeZone(def.getTimeZone()); + defaultGenerator = TelemetryGeneratorFactory.create(new TelemetryProfile(tp.getKey(), def.getDefaultDefinition())); + scheduleGenerators = new LinkedHashMap<>(); + for (ValueStrategySchedule scheduleItem : def.getSchedule()) { + scheduleGenerators.put(scheduleItem, TelemetryGeneratorFactory.create(new TelemetryProfile(tp.getKey(), scheduleItem.getDefinition()))); + } + } + + @Override + public void addValue(long ts, ObjectNode values) { + int hour = GeneratorTools.getHour(timeZone, ts); + int minute = GeneratorTools.getMinute(timeZone, ts); + TelemetryGenerator generator = scheduleGenerators.entrySet().stream().filter(pair -> { + var schedule = pair.getKey(); + if (hour == schedule.getStartHour() && hour == schedule.getEndHour()) { + return schedule.getStartMinute() <= minute && minute <= schedule.getEndMinute(); + } else if (hour == schedule.getStartHour() && hour < schedule.getEndHour()) { + return schedule.getStartMinute() <= minute; + } else if (hour > schedule.getStartHour() && hour < schedule.getEndHour()) { + return true; + } else if (hour > schedule.getStartHour() && hour == schedule.getEndHour()) { + return minute <= schedule.getEndHour(); + } else { + return false; + } + }).map(Map.Entry::getValue).findFirst().orElse(defaultGenerator); + + if (prevGenerator != null && !prevGenerator.equals(generator)) { + generator.setValue(prevGenerator.getValue()); + } + + generator.addValue(ts, values); + + prevGenerator = generator; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/SequenceValueStrategyDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/SequenceValueStrategyDefinition.java new file mode 100644 index 0000000000..d523715d67 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/SequenceValueStrategyDefinition.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; + + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SequenceValueStrategyDefinition implements ValueStrategyDefinition { + private boolean random; + private ObjectNode telemetry; + + @Override + public ValueStrategyDefinitionType getStrategyType() { + return ValueStrategyDefinitionType.SEQUENCE; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/SequenceValueStrategyGenerator.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/SequenceValueStrategyGenerator.java new file mode 100644 index 0000000000..d7c75025f4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/SequenceValueStrategyGenerator.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile; + +import java.util.Random; + +public class SequenceValueStrategyGenerator extends TelemetryGenerator { + + private final SequenceValueStrategyDefinition strategy; + private int max; + private int index; + + public SequenceValueStrategyGenerator(TelemetryProfile telemetryProfile) { + super(telemetryProfile); + this.strategy = (SequenceValueStrategyDefinition) telemetryProfile.getValueStrategy(); + max = strategy.getTelemetry().fields().next().getValue().size() - 1; + index = strategy.isRandom() ? new Random().nextInt(max + 1) : 0; + + } + + @Override + public void addValue(long ts, ObjectNode values) { + strategy.getTelemetry().fields().forEachRemaining(entry -> { + String key = entry.getKey(); + values.set(key, entry.getValue().get(index)); + }); + index++; + if (index > max) { + index = 0; + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/TelemetryGenerator.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/TelemetryGenerator.java new file mode 100644 index 0000000000..5024f4e590 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/TelemetryGenerator.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; +import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile; + +@Data +public abstract class TelemetryGenerator { + protected final TelemetryProfile profile; + protected final String key; + + public TelemetryGenerator(TelemetryProfile telemetryProfile) { + this.profile = telemetryProfile; + this.key = telemetryProfile.getKey(); + } + + public double getValue() { + throw new RuntimeException("Not supported"); + } + + public void setValue(double value) { + throw new RuntimeException("Not supported"); + } + + public abstract void addValue(long ts, ObjectNode values); +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/TelemetryGeneratorFactory.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/TelemetryGeneratorFactory.java new file mode 100644 index 0000000000..17d29ce3e1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/TelemetryGeneratorFactory.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + + +import org.thingsboard.server.service.solutions.data.definition.TelemetryProfile; + +public class TelemetryGeneratorFactory { + + public static TelemetryGenerator create(TelemetryProfile tp) { + switch (tp.getValueStrategy().getStrategyType()) { + case COUNTER: + return new CounterTelemetryGenerator(tp); + case NATURAL: + return new NaturalTelemetryGenerator(tp); + case EVENT: + return new EventTelemetryGenerator(tp); + case CONSTANT: + return new ConstantTelemetryGenerator(tp); + case SEQUENCE: + return new SequenceValueStrategyGenerator(tp); + case COMPOSITE: + return new CompositeValueStrategyGenerator(tp); + case SCHEDULE: + return new ScheduleValueStrategyGenerator(tp); + case INCREMENT: + return new IncrementTelemetryGenerator(tp); + case DECREMENT: + return new DecrementTelemetryGenerator(tp); + default: + throw new RuntimeException("Not supported!"); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategyDefinition.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategyDefinition.java new file mode 100644 index 0000000000..34d5f9b5e7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategyDefinition.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = CounterValueStrategyDefinition.class, name = "counter"), + @JsonSubTypes.Type(value = NaturalValueStrategyDefinition.class, name = "natural"), + @JsonSubTypes.Type(value = EventValueStrategyDefinition.class, name = "event"), + @JsonSubTypes.Type(value = SequenceValueStrategyDefinition.class, name = "sequence"), + @JsonSubTypes.Type(value = ConstantValueStrategyDefinition.class, name = "constant"), + @JsonSubTypes.Type(value = CompositeValueStrategyDefinition.class, name = "composite"), + @JsonSubTypes.Type(value = ScheduleValueStrategyDefinition.class, name = "schedule"), + @JsonSubTypes.Type(value = IncrementValueStrategyDefinition.class, name = "inc"), + @JsonSubTypes.Type(value = DecrementValueStrategyDefinition.class, name = "dec")}) +public interface ValueStrategyDefinition { + + ValueStrategyDefinitionType getStrategyType(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategyDefinitionType.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategyDefinitionType.java new file mode 100644 index 0000000000..a561d6469e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategyDefinitionType.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +public enum ValueStrategyDefinitionType { + + COUNTER, NATURAL, EVENT, SEQUENCE, CONSTANT, COMPOSITE, SCHEDULE, INCREMENT, DECREMENT; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategySchedule.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategySchedule.java new file mode 100644 index 0000000000..0bb2ccf8fa --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/values/ValueStrategySchedule.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2026 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.solutions.data.values; + +import lombok.Data; + +@Data +public class ValueStrategySchedule { + + private int startHour; + private int startMinute; + + private int endHour; + private int endMinute; + + private ValueStrategyDefinition definition; + +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index aaf182905e..bde1873627 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -2253,3 +2253,12 @@ mqtt: # The actual delay is randomized within a range defined by multiplying the base delay by a factor between (1 - jitter_factor) and (1 + jitter_factor). # For example, a jitter_factor of 0.15 means the actual delay may vary by up to ±15% of the base delay. jitter_factor: "${TB_MQTT_CLIENT_RETRANSMISSION_JITTER_FACTOR:0.15}" +iot-hub: + base-url: "${IOT_HUB_BASE_URL:https://iot-hub.thingsboard.io}" # IoT Hub base URL for fetching published items, resources, etc. + connect-timeout-sec: "${IOT_HUB_CONNECT_TIMEOUT_SEC:5}" # timeout in seconds to connect to IoT Hub server. + read-timeout-sec: "${IOT_HUB_READ_TIMEOUT_SEC:10}" # timeout in seconds to read from IoT Hub server. + max-file-data-size-bytes: "${IOT_HUB_MAX_FILE_DATA_SIZE_BYTES:104857600}" # maximum size in bytes of a file-data payload fetched from IoT Hub. Requests advertising a larger Content-Length, or streams that exceed this size, are rejected to avoid out-of-memory issues on the platform side. + max-uncompressed-archive-bytes: "${IOT_HUB_MAX_UNCOMPRESSED_ARCHIVE_BYTES:209715200}" # maximum cumulative uncompressed size in bytes for a solution template archive. Extraction aborts past this threshold to mitigate zip-bomb attacks. + max-uncompressed-entry-bytes: "${IOT_HUB_MAX_UNCOMPRESSED_ENTRY_BYTES:52428800}" # maximum uncompressed size in bytes for any single entry inside a solution template archive. + max-archive-entry-count: "${IOT_HUB_MAX_ARCHIVE_ENTRY_COUNT:10000}" # maximum number of entries allowed inside a solution template archive. + max-install-timeout-ms: "${IOT_HUB_MAX_INSTALL_TIMEOUT_MS:60000}" # upper clamp in milliseconds for the post-install wait declared in a solution template's solution.json. Prevents a malicious or misconfigured template from pinning an HTTP worker thread. diff --git a/application/src/test/java/org/thingsboard/server/service/iot_hub/DefaultIotHubServiceTest.java b/application/src/test/java/org/thingsboard/server/service/iot_hub/DefaultIotHubServiceTest.java new file mode 100644 index 0000000000..c252b58103 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/iot_hub/DefaultIotHubServiceTest.java @@ -0,0 +1,369 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.iot_hub.DashboardInstalledItemDescriptor; +import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; +import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DefaultIotHubServiceTest { + + private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + private final SecurityUser user = mock(SecurityUser.class); + + @Mock + private IotHubRestClient iotHubRestClient; + @Mock + private IotHubInstalledItemService iotHubInstalledItemService; + + @Spy + @InjectMocks + private DefaultIotHubService service; + + private void mockTenant() { + when(user.getTenantId()).thenReturn(tenantId); + } + + private IotHubInstalledItem installedItem(UUID id) { + IotHubInstalledItem item = new IotHubInstalledItem(); + item.setId(new IotHubInstalledItemId(id)); + item.setTenantId(tenantId); + return item; + } + + private InstallPlanEntry willInstall(String versionId, boolean root) { + InstallPlanEntry entry = new InstallPlanEntry(); + entry.setItemId(UUID.randomUUID().toString()); + entry.setVersionId(versionId); + entry.setName(root ? "Root" : "Dep"); + entry.setStatus(InstallPlanEntry.Status.WILL_INSTALL); + entry.setRoot(root); + return entry; + } + + private ObjectNode version(String id, String itemId, String type, String name, String ver) { + ObjectNode node = JacksonUtil.newObjectNode(); + node.put("id", id); + node.put("itemId", itemId); + node.put("type", type); + node.put("name", name); + node.put("version", ver); + return node; + } + + @Test + void resolveInstallPlan_rootOnly_noRelated_marksWillInstall() { + mockTenant(); + String versionId = UUID.randomUUID().toString(); + String itemId = UUID.randomUUID().toString(); + when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(version(versionId, itemId, "DASHBOARD", "Root", "1.0")); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); + + InstallPlan plan = service.resolveInstallPlan(user, versionId); + + assertThat(plan.getEntries()).hasSize(1); + InstallPlanEntry root = plan.getEntries().get(0); + assertThat(root.isRoot()).isTrue(); + assertThat(root.getStatus()).isEqualTo(InstallPlanEntry.Status.WILL_INSTALL); + assertThat(root.getItemId()).isEqualTo(itemId); + } + + @Test + void resolveInstallPlan_ordersDependenciesBeforeRoot() { + mockTenant(); + String versionId = UUID.randomUUID().toString(); + String rootItemId = UUID.randomUUID().toString(); + String relatedItemId = UUID.randomUUID().toString(); + ObjectNode root = version(versionId, rootItemId, "DASHBOARD", "Root", "1.0"); + root.putArray("relatedItems").add(relatedItemId); + when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(root); + when(iotHubRestClient.getPublishedVersionByItemId(relatedItemId)) + .thenReturn(version(UUID.randomUUID().toString(), relatedItemId, "WIDGET", "Dep", "2.0")); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); + + InstallPlan plan = service.resolveInstallPlan(user, versionId); + + assertThat(plan.getEntries()).hasSize(2); + assertThat(plan.getEntries().get(0).getItemId()).isEqualTo(relatedItemId); + assertThat(plan.getEntries().get(0).isRoot()).isFalse(); + assertThat(plan.getEntries().get(1).getItemId()).isEqualTo(rootItemId); + assertThat(plan.getEntries().get(1).isRoot()).isTrue(); + } + + @Test + void resolveInstallPlan_missingRelated_recordedAsMissing() { + mockTenant(); + String versionId = UUID.randomUUID().toString(); + String rootItemId = UUID.randomUUID().toString(); + String relatedItemId = UUID.randomUUID().toString(); + ObjectNode root = version(versionId, rootItemId, "DASHBOARD", "Root", "1.0"); + root.putArray("relatedItems").add(relatedItemId); + when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(root); + when(iotHubRestClient.getPublishedVersionByItemId(relatedItemId)).thenReturn(null); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); + + InstallPlan plan = service.resolveInstallPlan(user, versionId); + + InstallPlanEntry missing = plan.getEntries().get(0); + assertThat(missing.getStatus()).isEqualTo(InstallPlanEntry.Status.MISSING); + assertThat(missing.getItemId()).isEqualTo(relatedItemId); + assertThat(missing.getErrorMessage()).isNotBlank(); + } + + @Test + void resolveInstallPlan_relatedFetchThrows_recordedAsMissing() { + mockTenant(); + String versionId = UUID.randomUUID().toString(); + String rootItemId = UUID.randomUUID().toString(); + String relatedItemId = UUID.randomUUID().toString(); + ObjectNode root = version(versionId, rootItemId, "DASHBOARD", "Root", "1.0"); + root.putArray("relatedItems").add(relatedItemId); + when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(root); + when(iotHubRestClient.getPublishedVersionByItemId(relatedItemId)).thenThrow(new RuntimeException("boom")); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); + + InstallPlan plan = service.resolveInstallPlan(user, versionId); + + assertThat(plan.getEntries().get(0).getStatus()).isEqualTo(InstallPlanEntry.Status.MISSING); + assertThat(plan.getEntries().get(0).getErrorMessage()).contains("boom"); + } + + @Test + void resolveInstallPlan_alreadyInstalledRoot_marksAlreadyInstalled() { + mockTenant(); + String versionId = UUID.randomUUID().toString(); + UUID rootItemId = UUID.randomUUID(); + when(iotHubRestClient.getVersionInfo(versionId)) + .thenReturn(version(versionId, rootItemId.toString(), "WIDGET", "Root", "1.0")); + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of(rootItemId)); + + InstallPlan plan = service.resolveInstallPlan(user, versionId); + + assertThat(plan.getEntries().get(0).getStatus()).isEqualTo(InstallPlanEntry.Status.ALREADY_INSTALLED); + } + + @Test + void resolveInstallPlan_nullVersion_throws() { + mockTenant(); + String versionId = UUID.randomUUID().toString(); + when(iotHubRestClient.getVersionInfo(versionId)).thenReturn(null); + + assertThatThrownBy(() -> service.resolveInstallPlan(user, versionId)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void installPlan_emptyPlan_returnsFailureWithoutInstalling() { + InstallPlanResult result = service.installPlan(user, new InstallPlan(null, List.of()), null, null); + + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getErrorMessage()).contains("empty"); + verify(iotHubInstalledItemService, never()).findInstalledItemIdsByTenantIdAndItemIdIn(any(), any()); + } + + @Test + void installPlan_missingEntry_collectedAndNotInstalled() { + mockTenant(); + InstallPlanEntry missing = new InstallPlanEntry(); + missing.setItemId(UUID.randomUUID().toString()); + missing.setStatus(InstallPlanEntry.Status.MISSING); + + InstallPlanResult result = service.installPlan(user, new InstallPlan(null, List.of(missing)), null, null); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getMissingItemIds()).containsExactly(missing.getItemId()); + assertThat(result.getRootDescriptor()).isNull(); + verify(iotHubRestClient, never()).getVersionInfo(anyString()); + // A plan with nothing to install never hits the DB to check what is already installed. + verify(iotHubInstalledItemService, never()).findInstalledItemIdsByTenantIdAndItemIdIn(any(), any()); + } + + @Test + void installPlan_stalePlan_skipsItemInstalledSinceResolution() { + mockTenant(); + UUID itemId = UUID.randomUUID(); + InstallPlanEntry entry = new InstallPlanEntry(); + entry.setItemId(itemId.toString()); + entry.setVersionId(UUID.randomUUID().toString()); + entry.setType("WIDGET"); + entry.setStatus(InstallPlanEntry.Status.WILL_INSTALL); + entry.setRoot(true); + // The item is reported as already installed at install time, even though the plan says WILL_INSTALL. + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of(itemId)); + + InstallPlanResult result = service.installPlan(user, new InstallPlan(null, List.of(entry)), null, null); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getEntries().get(0).getStatus()).isEqualTo(InstallPlanEntry.Status.ALREADY_INSTALLED); + // No marketplace fetch happened — the duplicate install was avoided. + verify(iotHubRestClient, never()).getVersionInfo(anyString()); + } + + @Test + void installPlan_duplicateItemId_installsOnce() throws Exception { + mockTenant(); + HttpServletRequest request = mock(HttpServletRequest.class); + UUID itemId = UUID.randomUUID(); + String firstVersionId = UUID.randomUUID().toString(); + String secondVersionId = UUID.randomUUID().toString(); + // Two entries point at the same item — a client-supplied plan is not de-duplicated, so the + // cascade itself must not install the same item twice. + InstallPlanEntry first = new InstallPlanEntry(); + first.setItemId(itemId.toString()); + first.setVersionId(firstVersionId); + first.setStatus(InstallPlanEntry.Status.WILL_INSTALL); + first.setRoot(true); + InstallPlanEntry duplicate = new InstallPlanEntry(); + duplicate.setItemId(itemId.toString()); + duplicate.setVersionId(secondVersionId); + duplicate.setStatus(InstallPlanEntry.Status.WILL_INSTALL); + duplicate.setRoot(false); + + // Nothing is installed yet when the plan is submitted. + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); + doReturn(installedItem(UUID.randomUUID())).when(service).doInstallVersion(eq(user), eq(firstVersionId), any(), any()); + + InstallPlanResult result = service.installPlan(user, new InstallPlan(firstVersionId, List.of(first, duplicate)), null, request); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getEntries()).hasSize(2); + assertThat(result.getEntries().get(0).getStatus()).isEqualTo(InstallPlanEntry.Status.WILL_INSTALL); + // The duplicate is recognised as freshly installed and skipped. + assertThat(result.getEntries().get(1).getStatus()).isEqualTo(InstallPlanEntry.Status.ALREADY_INSTALLED); + verify(service).doInstallVersion(eq(user), eq(firstVersionId), any(), any()); + verify(service, never()).doInstallVersion(eq(user), eq(secondVersionId), any(), any()); + } + + @Test + void installPlan_cascade_installsDependencyBeforeRoot_andRoutesDataToRootOnly() throws Exception { + mockTenant(); + HttpServletRequest request = mock(HttpServletRequest.class); + JsonNode data = JacksonUtil.newObjectNode().put("entityId", UUID.randomUUID().toString()); + String depVersionId = UUID.randomUUID().toString(); + String rootVersionId = UUID.randomUUID().toString(); + InstallPlanEntry dep = willInstall(depVersionId, false); + InstallPlanEntry root = willInstall(rootVersionId, true); + + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); + IotHubInstalledItem depItem = installedItem(UUID.randomUUID()); + IotHubInstalledItem rootItem = installedItem(UUID.randomUUID()); + DashboardInstalledItemDescriptor rootDescriptor = new DashboardInstalledItemDescriptor(); + rootItem.setDescriptor(rootDescriptor); + doReturn(depItem).when(service).doInstallVersion(eq(user), eq(depVersionId), any(), any()); + doReturn(rootItem).when(service).doInstallVersion(eq(user), eq(rootVersionId), any(), any()); + + InstallPlanResult result = service.installPlan(user, new InstallPlan(rootVersionId, List.of(dep, root)), data, request); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.isRolledBack()).isFalse(); + assertThat(result.getRootDescriptor()).isSameAs(rootDescriptor); + assertThat(result.getMissingItemIds()).isEmpty(); + assertThat(result.getEntries()).hasSize(2); + + // The dependency installs before the root, and only the root receives the user's install data. + InOrder inOrder = inOrder(service); + inOrder.verify(service).doInstallVersion(eq(user), eq(depVersionId), isNull(), eq(request)); + inOrder.verify(service).doInstallVersion(eq(user), eq(rootVersionId), eq(data), eq(request)); + verify(service, never()).deleteInstalledItem(any(), any()); + } + + @Test + void installPlan_dependencyFails_rollsBackInstalledItemsInReverseOrder() throws Exception { + mockTenant(); + HttpServletRequest request = mock(HttpServletRequest.class); + String dep1VersionId = UUID.randomUUID().toString(); + String dep2VersionId = UUID.randomUUID().toString(); + String rootVersionId = UUID.randomUUID().toString(); + InstallPlanEntry dep1 = willInstall(dep1VersionId, false); + InstallPlanEntry dep2 = willInstall(dep2VersionId, false); + InstallPlanEntry root = willInstall(rootVersionId, true); + + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); + IotHubInstalledItem dep1Item = installedItem(UUID.randomUUID()); + IotHubInstalledItem dep2Item = installedItem(UUID.randomUUID()); + doReturn(dep1Item).when(service).doInstallVersion(eq(user), eq(dep1VersionId), any(), any()); + doReturn(dep2Item).when(service).doInstallVersion(eq(user), eq(dep2VersionId), any(), any()); + doThrow(new IllegalStateException("install failed")).when(service).doInstallVersion(eq(user), eq(rootVersionId), any(), any()); + doNothing().when(service).deleteInstalledItem(eq(user), any()); + + InstallPlanResult result = service.installPlan(user, new InstallPlan(rootVersionId, List.of(dep1, dep2, root)), null, request); + + assertThat(result.isSuccess()).isFalse(); + assertThat(result.isRolledBack()).isTrue(); + assertThat(result.getErrorMessage()).contains("install failed"); + + // Successfully installed dependencies are rolled back in reverse install order (dep2 before dep1). + InOrder inOrder = inOrder(service); + inOrder.verify(service).deleteInstalledItem(user, dep2Item.getId()); + inOrder.verify(service).deleteInstalledItem(user, dep1Item.getId()); + } + + @Test + void installPlan_rollbackFailure_reportsNotFullyRolledBack() throws Exception { + mockTenant(); + HttpServletRequest request = mock(HttpServletRequest.class); + String depVersionId = UUID.randomUUID().toString(); + String rootVersionId = UUID.randomUUID().toString(); + InstallPlanEntry dep = willInstall(depVersionId, false); + InstallPlanEntry root = willInstall(rootVersionId, true); + + when(iotHubInstalledItemService.findInstalledItemIdsByTenantIdAndItemIdIn(eq(tenantId), any())).thenReturn(List.of()); + IotHubInstalledItem depItem = installedItem(UUID.randomUUID()); + doReturn(depItem).when(service).doInstallVersion(eq(user), eq(depVersionId), any(), any()); + doThrow(new IllegalStateException("install failed")).when(service).doInstallVersion(eq(user), eq(rootVersionId), any(), any()); + // Rolling back the one installed dependency itself fails — the result must report a partial rollback. + doThrow(new RuntimeException("delete failed")).when(service).deleteInstalledItem(user, depItem.getId()); + + InstallPlanResult result = service.installPlan(user, new InstallPlan(rootVersionId, List.of(dep, root)), null, request); + + assertThat(result.isSuccess()).isFalse(); + assertThat(result.isRolledBack()).isFalse(); + assertThat(result.getErrorMessage()).contains("install failed"); + } +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java index 36f6fe0feb..597dbd6293 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java @@ -25,7 +25,11 @@ public interface DeviceConnectivityService { JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException; + JsonNode getConnectivityInfo(String baseUrl) throws URISyntaxException; + Resource getPemCertFile(String protocol); Resource createGatewayDockerComposeFile(String baseUrl, Device device) throws URISyntaxException; + + Resource createGatewayDockerComposeFile(String baseUrl, Device device, DockerComposeParams params) throws URISyntaxException; } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DockerComposeParams.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DockerComposeParams.java new file mode 100644 index 0000000000..86708910d3 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DockerComposeParams.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device; + +public record DockerComposeParams(boolean includeVersion, String containerName, boolean includePortBindings, + boolean includeExtraHosts, boolean includeVolumesBind, + boolean includeVolumesDeclaration) { +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index 1abb6afe9e..e1eae63acc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -48,4 +48,5 @@ public class SystemParams { boolean allowKeyFiltersOrConditions; String nullsOrderStrategy; boolean edqsEnabled; + String iotHubBaseUrl; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java index e7776ce286..fbeac79982 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java @@ -15,8 +15,14 @@ */ package org.thingsboard.server.common.data.alarm; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; + public enum AlarmSeverity { CRITICAL, MAJOR, MINOR, WARNING, INDETERMINATE; + @Getter + private final String displayName = StringUtils.capitalize(name().toLowerCase()); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java index fb944344e3..6dbf567003 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java @@ -15,20 +15,29 @@ */ package org.thingsboard.server.common.data.cf; +import lombok.Getter; + import java.util.Collections; import java.util.EnumSet; import java.util.Set; public enum CalculatedFieldType { - SIMPLE, - SCRIPT, - GEOFENCING, - ALARM, - PROPAGATION, - RELATED_ENTITIES_AGGREGATION, - ENTITY_AGGREGATION; + SIMPLE("Simple"), + SCRIPT("Script"), + GEOFENCING("Geofencing"), + ALARM("Alarm"), + PROPAGATION("Propagation"), + RELATED_ENTITIES_AGGREGATION("Related entities aggregation"), + ENTITY_AGGREGATION("Time series data aggregation"); public static final Set all = Collections.unmodifiableSet(EnumSet.allOf(CalculatedFieldType.class)); + @Getter + private final String displayName; + + CalculatedFieldType(String displayName) { + this.displayName = displayName; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/IotHubInstalledItemId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/IotHubInstalledItemId.java new file mode 100644 index 0000000000..f9ea6c4443 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/IotHubInstalledItemId.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.UUID; + +public class IotHubInstalledItemId extends UUIDBased { + + @JsonCreator + public IotHubInstalledItemId(@JsonProperty("id") UUID id) { + super(id); + } + + public static IotHubInstalledItemId fromString(String id) { + return new IotHubInstalledItemId(UUID.fromString(id)); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/AlarmRuleInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/AlarmRuleInstalledItemDescriptor.java new file mode 100644 index 0000000000..2a7be1f5d3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/AlarmRuleInstalledItemDescriptor.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import lombok.Data; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; + +@Data +public class AlarmRuleInstalledItemDescriptor implements IotHubInstalledItemDescriptor { + + private CalculatedFieldId calculatedFieldId; + private EntityId entityId; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/CalculatedFieldInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/CalculatedFieldInstalledItemDescriptor.java new file mode 100644 index 0000000000..ceedbe2dea --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/CalculatedFieldInstalledItemDescriptor.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import lombok.Data; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; + +@Data +public class CalculatedFieldInstalledItemDescriptor implements IotHubInstalledItemDescriptor { + + private CalculatedFieldId calculatedFieldId; + private EntityId entityId; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DashboardInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DashboardInstalledItemDescriptor.java new file mode 100644 index 0000000000..260665b82f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DashboardInstalledItemDescriptor.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import lombok.Data; +import org.thingsboard.server.common.data.id.DashboardId; + +@Data +public class DashboardInstalledItemDescriptor implements IotHubInstalledItemDescriptor { + + private DashboardId dashboardId; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptor.java new file mode 100644 index 0000000000..e10eb65b25 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptor.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.List; +import java.util.Map; + +@Data +public class DeviceInstalledItemDescriptor implements IotHubInstalledItemDescriptor { + + private List createdEntityIds; + private DashboardId dashboardId; + private String selectedInstallMethod; + private Map installState; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItem.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItem.java new file mode 100644 index 0000000000..8152ec1b0d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItem.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +public class IotHubInstalledItem extends BaseData implements HasTenantId { + + private TenantId tenantId; + private UUID itemId; + private UUID itemVersionId; + private String itemName; + private String itemType; + private String version; + private IotHubInstalledItemDescriptor descriptor; + + public IotHubInstalledItem() { + super(); + } + + public IotHubInstalledItem(IotHubInstalledItemId id) { + super(id); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemDescriptor.java new file mode 100644 index 0000000000..6a0359ceb3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/IotHubInstalledItemDescriptor.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import java.io.Serializable; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + property = "type") +@JsonSubTypes({ + @Type(name = "WIDGET", value = WidgetInstalledItemDescriptor.class), + @Type(name = "DASHBOARD", value = DashboardInstalledItemDescriptor.class), + @Type(name = "CALCULATED_FIELD", value = CalculatedFieldInstalledItemDescriptor.class), + @Type(name = "ALARM_RULE", value = AlarmRuleInstalledItemDescriptor.class), + @Type(name = "RULE_CHAIN", value = RuleChainInstalledItemDescriptor.class), + @Type(name = "DEVICE", value = DeviceInstalledItemDescriptor.class), + @Type(name = "SOLUTION_TEMPLATE", value = SolutionTemplateInstalledItemDescriptor.class) +}) +public interface IotHubInstalledItemDescriptor extends Serializable { + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/RuleChainInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/RuleChainInstalledItemDescriptor.java new file mode 100644 index 0000000000..c71a4bc6f6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/RuleChainInstalledItemDescriptor.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleChainId; + +@Data +public class RuleChainInstalledItemDescriptor implements IotHubInstalledItemDescriptor { + + private RuleChainId ruleChainId; + private EntityId targetProfileId; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/SolutionTemplateInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/SolutionTemplateInstalledItemDescriptor.java new file mode 100644 index 0000000000..488fe3c410 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/SolutionTemplateInstalledItemDescriptor.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import lombok.Data; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.List; + +@Data +public class SolutionTemplateInstalledItemDescriptor implements IotHubInstalledItemDescriptor { + + private List createdEntityIds; + private List tenantTelemetryKeys; + private List tenantAttributeKeys; + private DashboardId dashboardId; + private CustomerId publicId; + private boolean mainDashboardPublic; + private String details; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/WidgetInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/WidgetInstalledItemDescriptor.java new file mode 100644 index 0000000000..5c4195cb51 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/WidgetInstalledItemDescriptor.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import lombok.Data; +import org.thingsboard.server.common.data.id.WidgetTypeId; + +@Data +public class WidgetInstalledItemDescriptor implements IotHubInstalledItemDescriptor { + + private WidgetTypeId widgetTypeId; + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptorTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptorTest.java new file mode 100644 index 0000000000..a6b59995a8 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/iot_hub/DeviceInstalledItemDescriptorTest.java @@ -0,0 +1,129 @@ +/** + * Copyright © 2016-2026 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.iot_hub; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DashboardId; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class DeviceInstalledItemDescriptorTest { + + private static final ObjectMapper mapper = new ObjectMapper(); + + @Test + void deserializeFromFrontendPayload() throws Exception { + // Exact payload the frontend sends after the fix + String json = """ + { + "type": "DEVICE", + "createdEntityIds": [ + {"entityType": "DEVICE_PROFILE", "id": "3eb9c330-2e57-11f1-9334-a385172e8e7d"}, + {"entityType": "DEVICE", "id": "8a90d5e0-2e5d-11f1-a802-c35a9af2ebde"}, + {"entityType": "DASHBOARD", "id": "8bc229f0-2e5d-11f1-a802-c35a9af2ebde"} + ], + "dashboardId": {"entityType": "DASHBOARD", "id": "8bc229f0-2e5d-11f1-a802-c35a9af2ebde"} + } + """; + + // Deserialize the same way the controller does (JsonNode → treeToValue) + JsonNode node = mapper.readTree(json); + DeviceInstalledItemDescriptor descriptor = mapper.treeToValue(node, DeviceInstalledItemDescriptor.class); + + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getCreatedEntityIds()).hasSize(3); + assertThat(descriptor.getCreatedEntityIds().get(0).getEntityType()).isEqualTo(EntityType.DEVICE_PROFILE); + assertThat(descriptor.getCreatedEntityIds().get(1).getEntityType()).isEqualTo(EntityType.DEVICE); + assertThat(descriptor.getCreatedEntityIds().get(2).getEntityType()).isEqualTo(EntityType.DASHBOARD); + assertThat(descriptor.getDashboardId()).isNotNull(); + assertThat(descriptor.getDashboardId().getId()).isEqualTo(UUID.fromString("8bc229f0-2e5d-11f1-a802-c35a9af2ebde")); + } + + @Test + void roundTripSerialization() throws Exception { + DeviceInstalledItemDescriptor original = new DeviceInstalledItemDescriptor(); + DashboardId dashboardId = new DashboardId(UUID.randomUUID()); + original.setDashboardId(dashboardId); + original.setCreatedEntityIds(java.util.List.of(dashboardId)); + + String json = mapper.writeValueAsString(original); + JsonNode node = mapper.readTree(json); + DeviceInstalledItemDescriptor deserialized = mapper.treeToValue(node, DeviceInstalledItemDescriptor.class); + + assertThat(deserialized.getDashboardId().getId()).isEqualTo(original.getDashboardId().getId()); + assertThat(deserialized.getCreatedEntityIds()).hasSize(1); + assertThat(deserialized.getCreatedEntityIds().get(0).getId()).isEqualTo(dashboardId.getId()); + } + + @Test + void deserializeWithInstallState() throws Exception { + String json = """ + { + "type": "DEVICE", + "createdEntityIds": [ + {"entityType": "DEVICE_PROFILE", "id": "3eb9c330-2e57-11f1-9334-a385172e8e7d"}, + {"entityType": "DEVICE", "id": "8a90d5e0-2e5d-11f1-a802-c35a9af2ebde"}, + {"entityType": "DASHBOARD", "id": "8bc229f0-2e5d-11f1-a802-c35a9af2ebde"} + ], + "dashboardId": {"entityType": "DASHBOARD", "id": "8bc229f0-2e5d-11f1-a802-c35a9af2ebde"}, + "selectedInstallMethod": "DIRECT_MQTT", + "installState": { + "Configuration": { + "formValues": {"deviceName": "ESP32 Dev Kit", "wifiSsid": "MyWiFi", "wifiPassword": "secret"} + }, + "ESP32 Dev Kit": { + "entityOutput": {"id": "8a90d5e0-2e5d-11f1-a802-c35a9af2ebde", "name": "ESP32 Dev Kit", "token": "abc123", "url": "/entities/devices/8a90d5e0-2e5d-11f1-a802-c35a9af2ebde"} + }, + "ESP32 Monitor": { + "entityOutput": {"id": "8bc229f0-2e5d-11f1-a802-c35a9af2ebde", "name": "ESP32 Monitor", "url": "/dashboards/8bc229f0-2e5d-11f1-a802-c35a9af2ebde"} + } + } + } + """; + + JsonNode node = mapper.readTree(json); + DeviceInstalledItemDescriptor descriptor = mapper.treeToValue(node, DeviceInstalledItemDescriptor.class); + + assertThat(descriptor).isNotNull(); + assertThat(descriptor.getSelectedInstallMethod()).isEqualTo("DIRECT_MQTT"); + assertThat(descriptor.getInstallState()).isNotNull(); + assertThat(descriptor.getInstallState()).hasSize(3); + + // Verify form values + JsonNode configState = descriptor.getInstallState().get("Configuration"); + assertThat(configState).isNotNull(); + assertThat(configState.get("formValues").get("deviceName").asText()).isEqualTo("ESP32 Dev Kit"); + assertThat(configState.get("formValues").get("wifiSsid").asText()).isEqualTo("MyWiFi"); + + // Verify entity output + JsonNode deviceState = descriptor.getInstallState().get("ESP32 Dev Kit"); + assertThat(deviceState).isNotNull(); + assertThat(deviceState.get("entityOutput").get("token").asText()).isEqualTo("abc123"); + + // Verify round-trip + String serialized = mapper.writeValueAsString(descriptor); + JsonNode roundTripped = mapper.readTree(serialized); + DeviceInstalledItemDescriptor deserialized = mapper.treeToValue(roundTripped, DeviceInstalledItemDescriptor.class); + assertThat(deserialized.getSelectedInstallMethod()).isEqualTo("DIRECT_MQTT"); + assertThat(deserialized.getInstallState()).hasSize(3); + assertThat(deserialized.getInstallState().get("Configuration").get("formValues").get("deviceName").asText()).isEqualTo("ESP32 Dev Kit"); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java index 70f5f48272..5ce73bfcb7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java @@ -48,6 +48,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Base64; import java.util.List; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @@ -155,11 +156,17 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService @Override public Resource createGatewayDockerComposeFile(String baseUrl, Device device) throws URISyntaxException { + DockerComposeParams params = new DockerComposeParams(true, "tb-gateway", true, true, true, true); + return createGatewayDockerComposeFile(baseUrl, device, params); + } + + @Override + public Resource createGatewayDockerComposeFile(String baseUrl, Device device, DockerComposeParams params) throws URISyntaxException { String mqttType = isEnabled(MQTTS) ? MQTTS : MQTT; DeviceConnectivityInfo properties = getConnectivity(mqttType); DeviceCredentials creds = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()); String host = getHost(baseUrl, properties, mqttType); - return DeviceConnectivityUtil.getGatewayDockerComposeFile(host, gatewayImageVersion, creds); + return DeviceConnectivityUtil.getGatewayDockerComposeFile(host, gatewayImageVersion, creds, params); } private DeviceConnectivityInfo getConnectivity(String protocol) { @@ -176,6 +183,23 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService return info != null && info.isEnabled(); } + @Override + public JsonNode getConnectivityInfo(String baseUrl) throws URISyntaxException { + String[] protocols = {HTTP, HTTPS, MQTT, MQTTS, COAP, COAPS}; + Map result = new LinkedHashMap<>(); + for (String protocol : protocols) { + DeviceConnectivityInfo info = getConnectivity(protocol); + if (info != null && info.isEnabled()) { + DeviceConnectivityInfo resolved = new DeviceConnectivityInfo(); + resolved.setEnabled(true); + resolved.setHost(getHost(baseUrl, info, protocol)); + resolved.setPort(getPort(info)); + result.put(protocol, resolved); + } + } + return JacksonUtil.valueToTree(result); + } + private Resource getCert(String path) { if (StringUtils.isBlank(path) || !ResourceUtils.resourceExists(this, path)) { return null; diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java new file mode 100644 index 0000000000..8e20a68eb0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemDao.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.iot_hub; + +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.Dao; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public interface IotHubInstalledItemDao extends Dao { + + Optional findByTenantIdAndId(TenantId tenantId, IotHubInstalledItemId installedItemId); + + PageData findByTenantId(TenantId tenantId, List itemTypes, UUID itemId, PageLink pageLink); + + List findInstalledItemIdsByTenantId(TenantId tenantId); + + List findInstalledItemIdsByTenantIdAndItemIdIn(TenantId tenantId, Collection itemIds); + + long countByTenantId(TenantId tenantId, String itemType); + + Map findInstalledItemCounts(TenantId tenantId, String itemType); + + void deleteByTenantId(TenantId tenantId); + + boolean deleteByTenantIdAndId(TenantId tenantId, IotHubInstalledItemId installedItemId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java new file mode 100644 index 0000000000..874e9bfcc5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemService.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.iot_hub; + +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public interface IotHubInstalledItemService { + + IotHubInstalledItem save(TenantId tenantId, IotHubInstalledItem item); + + IotHubInstalledItem findById(TenantId tenantId, IotHubInstalledItemId id); + + PageData findByTenantId(TenantId tenantId, List itemTypes, UUID itemId, PageLink pageLink); + + List findInstalledItemIdsByTenantId(TenantId tenantId); + + List findInstalledItemIdsByTenantIdAndItemIdIn(TenantId tenantId, Collection itemIds); + + long countByTenantId(TenantId tenantId, String itemType); + + Map findInstalledItemCounts(TenantId tenantId, String itemType); + + void deleteById(TenantId tenantId, IotHubInstalledItemId id); + + void deleteByTenantId(TenantId tenantId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java new file mode 100644 index 0000000000..7e86bcb0b9 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/iot_hub/IotHubInstalledItemServiceImpl.java @@ -0,0 +1,89 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.iot_hub; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.model.sql.IotHubInstalledItemEntity; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.thingsboard.server.dao.service.Validator.validatePageLink; + +@Service +@RequiredArgsConstructor +@Slf4j +class IotHubInstalledItemServiceImpl implements IotHubInstalledItemService { + + private final IotHubInstalledItemDao iotHubInstalledItemDao; + + @Override + public IotHubInstalledItem save(TenantId tenantId, IotHubInstalledItem item) { + log.debug("[{}] Saving IoT Hub installed item: {}", tenantId, item); + return iotHubInstalledItemDao.save(tenantId, item); + } + + @Override + public IotHubInstalledItem findById(TenantId tenantId, IotHubInstalledItemId id) { + return iotHubInstalledItemDao.findByTenantIdAndId(tenantId, id).orElse(null); + } + + @Override + public PageData findByTenantId(TenantId tenantId, List itemTypes, UUID itemId, PageLink pageLink) { + validatePageLink(pageLink, IotHubInstalledItemEntity.ALLOWED_SORT_PROPERTIES); + return iotHubInstalledItemDao.findByTenantId(tenantId, itemTypes, itemId, pageLink); + } + + @Override + public List findInstalledItemIdsByTenantId(TenantId tenantId) { + return iotHubInstalledItemDao.findInstalledItemIdsByTenantId(tenantId); + } + + @Override + public List findInstalledItemIdsByTenantIdAndItemIdIn(TenantId tenantId, Collection itemIds) { + return iotHubInstalledItemDao.findInstalledItemIdsByTenantIdAndItemIdIn(tenantId, itemIds); + } + + @Override + public long countByTenantId(TenantId tenantId, String itemType) { + return iotHubInstalledItemDao.countByTenantId(tenantId, itemType); + } + + @Override + public Map findInstalledItemCounts(TenantId tenantId, String itemType) { + return iotHubInstalledItemDao.findInstalledItemCounts(tenantId, itemType); + } + + @Override + public void deleteById(TenantId tenantId, IotHubInstalledItemId id) { + iotHubInstalledItemDao.deleteByTenantIdAndId(tenantId, id); + } + + @Override + public void deleteByTenantId(TenantId tenantId) { + iotHubInstalledItemDao.deleteByTenantId(tenantId); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 6f971e61f7..ed34a2e64c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -764,6 +764,18 @@ public class ModelConstants { public static final String API_KEY_ENABLED_COLUMN_NAME = "enabled"; public static final String API_KEY_DESCRIPTION_COLUMN_NAME = "description"; + /** + * IoT Hub installed item constants. + */ + public static final String IOT_HUB_INSTALLED_ITEM_TABLE_NAME = "iot_hub_installed_item"; + public static final String IOT_HUB_INSTALLED_ITEM_TENANT_ID_COLUMN = TENANT_ID_COLUMN; + public static final String IOT_HUB_INSTALLED_ITEM_ITEM_ID_COLUMN = "item_id"; + public static final String IOT_HUB_INSTALLED_ITEM_ITEM_VERSION_ID_COLUMN = "item_version_id"; + public static final String IOT_HUB_INSTALLED_ITEM_ITEM_NAME_COLUMN = "item_name"; + public static final String IOT_HUB_INSTALLED_ITEM_ITEM_TYPE_COLUMN = "item_type"; + public static final String IOT_HUB_INSTALLED_ITEM_VERSION_COLUMN = "version"; + public static final String IOT_HUB_INSTALLED_ITEM_DESCRIPTOR_COLUMN = "descriptor"; + protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN), max(TS_COLUMN)}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/IotHubInstalledItemEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/IotHubInstalledItemEntity.java new file mode 100644 index 0000000000..118a175a36 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/IotHubInstalledItemEntity.java @@ -0,0 +1,118 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.annotations.Type; +import org.hibernate.proxy.HibernateProxy; +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; +import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItemDescriptor; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +@Getter +@Setter +@ToString +@Entity +@Table(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_TABLE_NAME) +public class IotHubInstalledItemEntity extends BaseSqlEntity { + + public static final Set ALLOWED_SORT_PROPERTIES = Collections.unmodifiableSet( + new LinkedHashSet<>(List.of("createdTime", "itemName", "itemType", "version")) + ); + + @Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_TENANT_ID_COLUMN, nullable = false, columnDefinition = "UUID") + private UUID tenantId; + + @Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_ITEM_ID_COLUMN, nullable = false, columnDefinition = "UUID") + private UUID itemId; + + @Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_ITEM_VERSION_ID_COLUMN, nullable = false, columnDefinition = "UUID") + private UUID itemVersionId; + + @Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_ITEM_NAME_COLUMN, nullable = false) + private String itemName; + + @Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_ITEM_TYPE_COLUMN, nullable = false) + private String itemType; + + @Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_VERSION_COLUMN, nullable = false) + private String version; + + @Type(JsonBinaryType.class) + @Column(name = ModelConstants.IOT_HUB_INSTALLED_ITEM_DESCRIPTOR_COLUMN, nullable = false, columnDefinition = "JSONB") + private IotHubInstalledItemDescriptor descriptor; + + public IotHubInstalledItemEntity() { + } + + public IotHubInstalledItemEntity(IotHubInstalledItem item) { + super(item); + tenantId = getTenantUuid(item.getTenantId()); + itemId = item.getItemId(); + itemVersionId = item.getItemVersionId(); + itemName = item.getItemName(); + itemType = item.getItemType(); + version = item.getVersion(); + descriptor = item.getDescriptor(); + } + + @Override + public IotHubInstalledItem toData() { + var item = new IotHubInstalledItem(new IotHubInstalledItemId(id)); + item.setCreatedTime(createdTime); + item.setTenantId(TenantId.fromUUID(tenantId)); + item.setItemId(itemId); + item.setItemVersionId(itemVersionId); + item.setItemName(itemName); + item.setItemType(itemType); + item.setVersion(version); + item.setDescriptor(descriptor); + return item; + } + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); + Class> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + IotHubInstalledItemEntity that = (IotHubInstalledItemEntity) o; + return getId() != null && Objects.equals(getId(), that.getId()); + } + + @Override + public final int hashCode() { + return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java new file mode 100644 index 0000000000..393e40ea86 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/IotHubInstalledItemRepository.java @@ -0,0 +1,82 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.iot_hub; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.model.sql.IotHubInstalledItemEntity; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +interface IotHubInstalledItemRepository extends JpaRepository { + + Optional findByTenantIdAndId(UUID tenantId, UUID id); + + @Query("SELECT DISTINCT item.itemId FROM IotHubInstalledItemEntity item WHERE item.tenantId = :tenantId") + List findInstalledItemIdsByTenantId(@Param("tenantId") UUID tenantId); + + @Query("SELECT DISTINCT item.itemId FROM IotHubInstalledItemEntity item " + + "WHERE item.tenantId = :tenantId AND item.itemId IN :itemIds") + List findInstalledItemIdsByTenantIdAndItemIdIn(@Param("tenantId") UUID tenantId, + @Param("itemIds") Collection itemIds); + + @Query(""" + SELECT item FROM IotHubInstalledItemEntity item + WHERE item.tenantId = :tenantId + AND (:itemTypes IS NULL OR item.itemType IN :itemTypes) + AND (:itemId IS NULL OR item.itemId = :itemId) + AND (:textSearch IS NULL + OR ilike(item.itemName, CONCAT('%', :textSearch, '%')) = true + OR ilike(item.itemType, CONCAT('%', :textSearch, '%')) = true + OR ilike(item.version, CONCAT('%', :textSearch, '%')) = true) + """) + Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("itemTypes") List itemTypes, @Param("itemId") UUID itemId, @Param("textSearch") String textSearch, Pageable pageable); + + @Query(""" + SELECT COUNT(item) FROM IotHubInstalledItemEntity item + WHERE item.tenantId = :tenantId + AND (:itemType IS NULL OR item.itemType = :itemType) + """) + long countByTenantId(@Param("tenantId") UUID tenantId, @Param("itemType") String itemType); + + @Query(""" + SELECT item.itemId, COUNT(item) FROM IotHubInstalledItemEntity item + WHERE item.tenantId = :tenantId + AND item.itemType = :itemType + GROUP BY item.itemId + """) + List findInstalledItemCounts(@Param("tenantId") UUID tenantId, @Param("itemType") String itemType); + + @Transactional + @Modifying + @Query(value = "DELETE FROM iot_hub_installed_item WHERE tenant_id = :tenantId", nativeQuery = true) + void deleteByTenantId(@Param("tenantId") UUID tenantId); + + @Transactional + @Modifying + @Query("DELETE FROM IotHubInstalledItemEntity item WHERE item.tenantId = :tenantId AND item.id IN (:ids)") + int deleteByTenantIdAndIdIn(@Param("tenantId") UUID tenantId, @Param("ids") Set ids); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java new file mode 100644 index 0000000000..5cd305b5ad --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/iot_hub/JpaIotHubInstalledItemDao.java @@ -0,0 +1,126 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.iot_hub; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.IotHubInstalledItemId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.iot_hub.IotHubInstalledItem; +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.dao.DaoUtil; +import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemDao; +import org.thingsboard.server.dao.model.sql.IotHubInstalledItemEntity; +import org.thingsboard.server.dao.sql.JpaAbstractDao; +import org.thingsboard.server.dao.util.SqlDao; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +@SqlDao +@Component +@RequiredArgsConstructor +class JpaIotHubInstalledItemDao extends JpaAbstractDao implements IotHubInstalledItemDao { + + private final IotHubInstalledItemRepository repository; + + @Override + public Optional findByTenantIdAndId(TenantId tenantId, IotHubInstalledItemId installedItemId) { + return repository.findByTenantIdAndId(tenantId.getId(), installedItemId.getId()).map(DaoUtil::getData); + } + + @Override + public PageData findByTenantId(TenantId tenantId, List itemTypes, UUID itemId, PageLink pageLink) { + return DaoUtil.toPageData(repository.findByTenantId( + tenantId.getId(), + itemTypes == null || itemTypes.isEmpty() ? null : itemTypes, + itemId, + StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), + toPageRequest(pageLink)) + ); + } + + @Override + public List findInstalledItemIdsByTenantId(TenantId tenantId) { + return repository.findInstalledItemIdsByTenantId(tenantId.getId()); + } + + @Override + public List findInstalledItemIdsByTenantIdAndItemIdIn(TenantId tenantId, Collection itemIds) { + return repository.findInstalledItemIdsByTenantIdAndItemIdIn(tenantId.getId(), itemIds); + } + + @Override + public long countByTenantId(TenantId tenantId, String itemType) { + return repository.countByTenantId(tenantId.getId(), itemType); + } + + @Override + public Map findInstalledItemCounts(TenantId tenantId, String itemType) { + List results = repository.findInstalledItemCounts(tenantId.getId(), itemType); + Map counts = new HashMap<>(); + for (Object[] row : results) { + counts.put((UUID) row[0], (Long) row[1]); + } + return counts; + } + + @Override + public void deleteByTenantId(TenantId tenantId) { + repository.deleteByTenantId(tenantId.getId()); + } + + @Override + public boolean deleteByTenantIdAndId(TenantId tenantId, IotHubInstalledItemId installedItemId) { + return repository.deleteByTenantIdAndIdIn(tenantId.getId(), Set.of(installedItemId.getId())) > 0; + } + + private static PageRequest toPageRequest(PageLink pageLink) { + Sort sort; + SortOrder sortOrder = pageLink.getSortOrder(); + if (sortOrder == null) { + sort = Sort.by(Sort.Direction.DESC, "createdTime"); + } else { + sort = Sort.by( + Sort.Direction.fromString(sortOrder.getDirection().name()), + sortOrder.getProperty() + ).and(Sort.by(Sort.Direction.ASC, "id")); + } + return PageRequest.of(pageLink.getPage(), pageLink.getPageSize(), sort); + } + + @Override + protected Class getEntityClass() { + return IotHubInstalledItemEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return repository; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 392c422792..b20913c510 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -39,6 +39,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; +import org.thingsboard.server.dao.iot_hub.IotHubInstalledItemService; import org.thingsboard.server.dao.mobile.QrCodeSettingService; import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -86,6 +87,8 @@ public class TenantServiceImpl extends AbstractCachedEntityService existsTenantCache; @@ -171,6 +174,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService + ) {} + + get baseUrl(): string { + return getCurrentAuthState(this.store)?.iotHubBaseUrl || ''; + } + + public resolveResourceUrl(path: string): string { + return iotHubResourceUrl(this.baseUrl, path); + } + + public getPublishedVersions(query: MpItemVersionQuery, config?: IotHubRequestConfig): Observable> { + if (query.options.tbVersion == null) { + query.options.tbVersion = tbVersionToInt(env.tbVersion); + } + if (query.options.peOnly == null) { + query.options.peOnly = false; + } + return this.http.get>( + `${this.baseUrl}/api/versions/published${query.toQuery()}`, + { params: this.buildParams(config) } + ); + } + + public getFilterInfo(itemType: ItemType, config?: IotHubRequestConfig): Observable { + const url = `${this.baseUrl}/api/item-listing/filterInfo/${itemType}` + + `?peOnly=false&tbVersion=${tbVersionToInt(env.tbVersion)}`; + return this.http.get(url, { params: this.buildParams(config) }); + } + + public getWidgetCategories(textSearch?: string, scadaFirst?: boolean, + config?: IotHubRequestConfig): Observable { + const queryParams: string[] = [ + `peOnly=false`, + `tbVersion=${tbVersionToInt(env.tbVersion)}` + ]; + if (textSearch?.trim()) { + queryParams.push(`textSearch=${encodeURIComponent(textSearch.trim())}`); + } + if (scadaFirst != null) { + queryParams.push(`scadaFirst=${scadaFirst}`); + } + const url = `${this.baseUrl}/api/item-listing/widgetCategories?${queryParams.join('&')}`; + return this.http.get(url, { params: this.buildParams(config) }); + } + + public getVersionInfo(versionId: string, config?: IotHubRequestConfig): Observable { + return this.http.get( + `${this.baseUrl}/api/versions/${versionId}`, + { params: this.buildParams(config) } + ); + } + + public getPublishedVersion(itemId: string, config?: IotHubRequestConfig): Observable { + return this.http.get( + `${this.baseUrl}/api/items/${itemId}/published`, + { params: this.buildParams(config) } + ); + } + + /** + * Resolves the listing item-version that matches the caller's edition + * and platform version. + * + * Backend: GET /api/listings/public/by-slug/{slug}/item-version + * + * On success → 200 with `MpItemVersionView`. + * On miss → 404 with a `ListingItemVersionNotFound` body shape + * (`noMatchingVersions`, `peRequired`, or + * `minTbVersionRequired`). Callers should inspect + * `HttpErrorResponse.error` to differentiate. + */ + public getListingItemVersion(slug: string, config?: IotHubRequestConfig): Observable { + const queryParams = [ + 'ce=true', + `tbVersion=${tbVersionToInt(env.tbVersion)}` + ]; + return this.http.get( + `${this.baseUrl}/api/listings/public/by-slug/${encodeURIComponent(slug)}/item-version?${queryParams.join('&')}`, + { params: this.buildParams(config) } + ); + } + + public getVersionReadme(versionId: string, config?: IotHubRequestConfig): Observable { + return this.http.get(`${this.baseUrl}/api/versions/${versionId}/readme`, { + params: this.buildParams(config), + responseType: 'text' + }); + } + + public getVersionFileData(versionId: string, config?: IotHubRequestConfig): Observable { + return this.http.get(`${this.baseUrl}/api/versions/${versionId}/fileData`, { + params: this.buildParams(config), + responseType: 'blob' + }); + } + + public getConnectivitySettings(config?: IotHubRequestConfig): Observable { + return this.http.get( + `/api/iot-hub/connectivity`, + { params: this.buildParams(config) } + ); + } + + public registerDeviceInstall( + versionId: string, + descriptor: { type?: string; createdEntityIds: { entityType: string; id: string }[]; dashboardId?: { entityType: string; id: string }; selectedInstallMethod?: string; installState?: Record }, + config?: IotHubRequestConfig + ): Observable { + return this.http.post( + `/api/iot-hub/device/register?versionId=${versionId}`, + { type: 'DEVICE', ...descriptor }, + { params: this.buildParams(config) } + ); + } + + public installItemVersion(versionId: string, config?: IotHubRequestConfig, data?: any): Observable { + return this.http.post( + `/api/iot-hub/versions/${versionId}/install`, + data || null, + { params: this.buildParams(config) } + ); + } + + public resolveInstallPlan(versionId: string, config?: IotHubRequestConfig): Observable { + return this.http.get( + `/api/iot-hub/versions/${versionId}/installPlan`, + { params: this.buildParams(config) } + ); + } + + public installPlan(plan: InstallPlan, data?: any, config?: IotHubRequestConfig): Observable { + return this.http.post( + `/api/iot-hub/installPlan`, + { plan, data: data ?? null }, + { params: this.buildParams(config) } + ); + } + + public updateItemVersion(installedItemId: string, versionId: string, config?: IotHubRequestConfig, force?: boolean): Observable { + let params = this.buildParams(config); + if (force) { + params = params.set('force', 'true'); + } + return this.http.post( + `/api/iot-hub/installedItems/${installedItemId}/update/${versionId}`, + null, + { params } + ); + } + + public getInstalledItemIds(config?: IotHubRequestConfig): Observable { + return this.http.get( + `/api/iot-hub/installedItems/itemIds`, + { params: this.buildParams(config) } + ); + } + + public getInstalledItemCounts(itemType: string, config?: IotHubRequestConfig): Observable> { + return this.http.get>( + `/api/iot-hub/installedItems/counts?itemType=${itemType}`, + { params: this.buildParams(config) } + ); + } + + public getInstalledItems(pageLink: PageLink, itemTypes?: string | string[], + itemId?: string, config?: IotHubRequestConfig): Observable> { + let query = pageLink.toQuery(); + if (itemTypes) { + const types = Array.isArray(itemTypes) ? itemTypes : [itemTypes]; + types.forEach(t => query += `&itemTypes=${t}`); + } + if (itemId) { + query += `&itemId=${itemId}`; + } + return this.http.get>( + `/api/iot-hub/installedItems${query}`, + { params: this.buildParams(config) } + ); + } + + public getInstalledItemsCount(itemType?: string, config?: IotHubRequestConfig): Observable { + let url = '/api/iot-hub/installedItems/count'; + if (itemType) { + url += `?itemType=${itemType}`; + } + return this.http.get(url, { params: this.buildParams(config) }); + } + + public deleteInstalledItem(installedItemId: string, config?: IotHubRequestConfig): Observable { + return this.http.delete( + `/api/iot-hub/installedItems/${installedItemId}`, + { params: this.buildParams(config) } + ); + } + + public getItemsPublishedVersions(itemIds: string[], config?: IotHubRequestConfig): Observable { + return this.http.post( + `${this.baseUrl}/api/versions/publishedVersions`, + itemIds, + { params: this.buildParams(config) } + ); + } + + public getCreatorProfile(creatorId: string, config?: IotHubRequestConfig): Observable { + return this.http.get( + `${this.baseUrl}/api/creators/${creatorId}/profile`, + { params: this.buildParams(config) } + ); + } + + private buildParams(config?: IotHubRequestConfig): HttpParams { + return new InterceptorHttpParams( + new InterceptorConfig(config?.ignoreLoading ?? false, config?.ignoreErrors ?? false) + ); + } +} diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index ab2160ec0f..443debdc43 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -30,6 +30,7 @@ export interface MenuSection { pages?: Array; opened?: boolean; rootOnly?: boolean; + isNew?: boolean; customTranslate?: boolean; } @@ -109,7 +110,8 @@ export enum MenuId { version_control = 'version_control', api_usage = 'api_usage', trendz_settings = 'trendz_settings', - ai_models = 'ai_models' + ai_models = 'ai_models', + iot_hub = 'iot_hub' } declare type MenuFilter = (authState: AuthState) => boolean; @@ -740,6 +742,17 @@ export const menuSectionMap = new Map([ path: '/settings/trendz', icon: 'trendz-settings' } + ], + [ + MenuId.iot_hub, + { + id: MenuId.iot_hub, + name: 'iot-hub.iot-hub', + type: 'link', + path: '/iot-hub', + icon: 'hub', + isNew: true + } ] ]); @@ -834,6 +847,7 @@ const defaultUserMenuMap = new Map([ ] }, {id: MenuId.dashboards}, + {id: MenuId.iot_hub}, { id: MenuId.entities, pages: [ diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 8f721b87dd..8e8c699fe4 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -179,7 +179,6 @@ import * as HistorySelectorComponent from '@shared/components/time/history-selec import * as EntityGatewaySelectComponent from '@shared/components/entity/entity-gateway-select.component'; import * as ContactComponent from '@shared/components/contact.component'; import * as OtaPackageAutocompleteComponent from '@shared/components/ota-package/ota-package-autocomplete.component'; -import * as WidgetsBundleSearchComponent from '@shared/components/widgets-bundle-search.component'; import * as CopyButtonComponent from '@shared/components/button/copy-button.component'; import * as TogglePasswordComponent from '@shared/components/button/toggle-password.component'; import * as WidgetButtonComponent from '@shared/components/button/widget-button.component'; @@ -326,7 +325,6 @@ import * as DashboardStateDialogComponent from '@home/components/dashboard-page/ import * as EmbedDashboardDialogComponent from '@home/components/widget/dialog/embed-dashboard-dialog.component'; import * as EdgeDownlinkTableComponent from '@home/components/edge/edge-downlink-table.component'; import * as EdgeDownlinkTableHeaderComponent from '@home/components/edge/edge-downlink-table-header.component'; -import * as DisplayWidgetTypesPanelComponent from '@home/components/dashboard-page/widget-types-panel.component'; import * as AlarmDurationPredicateValueComponent from '@home/components/profile/alarm/alarm-duration-predicate-value.component'; import * as DashboardImageDialogComponent from '@home/components/dashboard-page/dashboard-image-dialog.component'; import * as WidgetContainerComponent from '@home/components/widget/widget-container.component'; @@ -530,7 +528,6 @@ class ModulesMap implements IModulesMap { '@shared/components/entity/entity-gateway-select.component': EntityGatewaySelectComponent, '@shared/components/contact.component': ContactComponent, '@shared/components/ota-package/ota-package-autocomplete.component': OtaPackageAutocompleteComponent, - '@shared/components/widgets-bundle-search.component': WidgetsBundleSearchComponent, '@shared/components/button/copy-button.component': CopyButtonComponent, '@shared/components/button/toggle-password.component': TogglePasswordComponent, '@shared/components/button/widget-button.component': WidgetButtonComponent, @@ -679,7 +676,6 @@ class ModulesMap implements IModulesMap { '@home/components/widget/dialog/embed-dashboard-dialog.component': EmbedDashboardDialogComponent, '@home/components/edge/edge-downlink-table.component': EdgeDownlinkTableComponent, '@home/components/edge/edge-downlink-table-header.component': EdgeDownlinkTableHeaderComponent, - '@home/components/dashboard-page/widget-types-panel.component': DisplayWidgetTypesPanelComponent, '@home/components/profile/alarm/alarm-duration-predicate-value.component': AlarmDurationPredicateValueComponent, '@home/components/dashboard-page/dashboard-image-dialog.component': DashboardImageDialogComponent, '@home/components/widget/widget-container.component': WidgetContainerComponent, diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index f5043bdcee..9e95a55938 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -70,6 +70,8 @@ import { AlarmRulesTabsComponent } from '@home/pages/alarm/alarm-rules-tabs.comp import { Router } from '@angular/router'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { AlarmRulesComponent } from '@home/components/alarm-rules/alarm-rules.component'; +import { ItemType } from '@shared/models/iot-hub/iot-hub-item.models'; +import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service'; export type AlarmRuleTableEntity = CalculatedFieldAlarmRule | CalculatedFieldAlarmRuleInfo; @@ -97,6 +99,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig true, onAction: () => this.importCalculatedField() + }, + { + name: this.translate.instant('iot-hub.add-from-iot-hub'), + icon: 'hub', + isEnabled: () => true, + onAction: () => this.addAlarmRuleFromIotHub() } ]; @@ -333,6 +342,14 @@ export class AlarmRulesTableConfig extends EntityTableConfig { + if (result?.descriptor) { + this.updateData(); + } + }); + } + private importCalculatedField(): void { this.importExportService.openCalculatedFieldImportDialog('alarm-rule.import', 'alarm-rule.file') .pipe( diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts index ce0aecb598..a882868728 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts @@ -37,6 +37,7 @@ import { DatePipe } from '@angular/common'; import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config"; import { UtilsService } from "@core/services/utils.service"; import { ActivatedRoute, Router } from "@angular/router"; +import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service'; @Component({ selector: 'tb-alarm-rules-table', @@ -71,7 +72,8 @@ export class AlarmRulesTableComponent { private utilsService: UtilsService, private destroyRef: DestroyRef, private route: ActivatedRoute, - private router: Router + private router: Router, + private iotHubActions: IotHubActionsService ) { this.pageMode = !!this.route.snapshot.data.isPage; effect(() => { @@ -91,6 +93,7 @@ export class AlarmRulesTableComponent { this.entityDebugSettingsService, this.utilsService, this.router, + this.iotHubActions, this.pageMode, ); this.cd.markForCheck(); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index aac49a3f37..8d99028163 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -72,6 +72,8 @@ import { EntityAction } from '@home/models/entity/entity-component.models'; import { CalculatedFieldComponent } from '@home/components/calculated-fields/calculated-field.component'; import { Router } from '@angular/router'; import { CalculatedFieldsTabsComponent } from '@home/pages/calculated-fields/calculated-fields-tabs.component'; +import { ItemType } from '@shared/models/iot-hub/iot-hub-item.models'; +import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service'; export type CalculatedFieldsTableEntity = CalculatedField | CalculatedFieldInfo; @@ -101,6 +103,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig true, onAction: () => this.importCalculatedField() + }, + { + name: this.translate.instant('iot-hub.add-from-iot-hub'), + icon: 'hub', + isEnabled: () => true, + onAction: () => this.addCalculatedFieldFromIotHub() } ]; @@ -330,6 +339,14 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + if (result?.descriptor) { + this.updateData(); + } + }); + } + private importCalculatedField(): void { this.importExportService.openCalculatedFieldImportDialog() .pipe( diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index feba407f52..beb02f96e2 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -34,6 +34,7 @@ import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/ import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { ImportExportService } from '@shared/import-export/import-export.service'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; +import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service'; import { DatePipe } from '@angular/common'; import { UtilsService } from "@core/services/utils.service"; import { ActivatedRoute, Router } from '@angular/router'; @@ -71,7 +72,8 @@ export class CalculatedFieldsTableComponent { private utilsService: UtilsService, private destroyRef: DestroyRef, private route: ActivatedRoute, - private router: Router + private router: Router, + private iotHubActions: IotHubActionsService ) { this.pageMode = !!this.route.snapshot.data.isPage; effect(() => { @@ -91,6 +93,7 @@ export class CalculatedFieldsTableComponent { this.entityDebugSettingsService, this.utilsService, this.router, + this.iotHubActions, this.pageMode, ); this.cd.markForCheck(); diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html index 46b2bbb9ae..c62c29555e 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html @@ -114,394 +114,325 @@ matTooltip="{{'dashboard.edit-mode' | translate}}" matTooltipPosition="below"> edit - - edit{{'dashboard.edit-mode' | translate}} - } - @if (isEdit) { - - done - - done{{ 'action.save' | translate }} - - close - - close{{ 'action.cancel' | translate }} - - } - @if (currentDashboardId && isEdit && isTenantAdmin()) { - - history - - - history - {{'version-control.versions' | translate}} - - } - @if (isEdit) { - - filter_list - - - filter_list - {{ 'filter.filters' | translate }} - - } - @if (isEdit) { - - devices_other - - - devices_other - {{ 'entity.aliases-short' | translate }} - - } - @if (isEdit) { - - settings - - - settings - {{ 'dashboard.settings' | translate }} - - } - - wallpaper - - @if (isEdit) { - - - - - } @else { - @if (displayDashboardTimewindow()) { - > - - > - - } - } - @if (isEdit) { - - add - - - add - {{ 'dashboard.add-widget-button-text' | translate }} - - } - - - - - @if (!isEdit && !widgetEditMode && !embedded && displayDashboardsSelect()) { - - - } - - - - - - - - @if (!widgetEditMode) { - - {{ translatedDashboardTitle }} - - dashboard.title - - - - } - - @if (layouts.right.show) { - - - - - } - - - - @if (isEdit) { - - {{ 'dashboard.breakpoint-value' | translate : {value: currentBreakpointValue } }} - - } - - - @if (!readonly && (hideToolbar || widgetEditMode)) { - - - edit - - - done - - close - - } - @if (!embedded) { - - Powered by ThingsBoard v.{{ thingsboardVersion }} - - } - - - @if (!isEditingWidgetClosed) { - - - @if (tbEditWidget.widgetConfig?.hasBasicMode) { - - {{ 'widget.basic-mode' | translate }} - {{ 'widget.advanced-mode' | translate }} - - } - - - - - - } - @if (!isAddingWidgetClosed && !widgetEditMode) { - + edit{{'dashboard.edit-mode' | translate}} + } + @if (isEdit) { + + done + + done{{ 'action.save' | translate }} + + close + + close{{ 'action.cancel' | translate }} + + } + @if (currentDashboardId && isEdit && isTenantAdmin()) { + + history + + + history + {{'version-control.versions' | translate}} + + } + @if (isEdit) { + + filter_list + + + filter_list + {{ 'filter.filters' | translate }} + + + devices_other + + + devices_other + {{ 'entity.aliases-short' | translate }} + + + settings + + + settings + {{ 'dashboard.settings' | translate }} + + } + + wallpaper + + @if (isEdit) { + + + + + } @else { + @if (displayDashboardTimewindow()) { + > + + > + + } + } + @if (isEdit) { + + add + + + add + {{ 'dashboard.add-widget-button-text' | translate }} + + } + + + + + @if (!isEdit && !widgetEditMode && !embedded && displayDashboardsSelect()) { + + + } + + + + + + + + @if (!widgetEditMode) { + + {{ translatedDashboardTitle }} + + dashboard.title + + + + } + + @if (layouts.right.show) { + + + + + } + + + + @if (isEdit) { + + {{ 'dashboard.breakpoint-value' | translate : {value: currentBreakpointValue } }} + + } + + + @if (!readonly && (hideToolbar || widgetEditMode)) { + + + edit + + + done + + + close + + + } + @if (!embedded) { + + Powered by ThingsBoard v.{{ thingsboardVersion }} + + } + + + @if (!isEditingWidgetClosed) { + - - - arrow_back - - - @if (isAddingWidget && dashboardWidgetSelectComponent) { - - - - - } - @if (isAddingWidget) { - - @if (dashboardWidgetSelectComponent?.selectWidgetMode === 'bundles' && !dashboardWidgetSelectComponent?.widgetsBundle) { - - file_upload{{ 'dashboard.import-widget' | translate }} - } - @if (dashboardWidgetSelectComponent?.selectWidgetMode === 'bundles' && !dashboardWidgetSelectComponent?.widgetsBundle) { - - {{ 'widgets-bundle.widgets-bundles' | translate }} - {{ 'widget.all-widgets' | translate }} - - } - @if (dashboardWidgetSelectComponent?.widgetTypes.size > 1) { - - filter_list - - } - @if (dashboardWidgetSelectComponent?.hasDeprecated) { - - {{ 'widget.all' | translate }} - {{ 'widget.actual' | translate }} - {{ 'widget.deprecated' | translate }} - - } - - } - @if (isAddingWidget) { - - - } - - } - - - - + (closeDetails)="onEditWidgetClosed()"> + + @if (tbEditWidget.widgetConfig?.hasBasicMode) { + + {{ 'widget.basic-mode' | translate }} + {{ 'widget.advanced-mode' | translate }} + + } + + + + + + } + @if (!isAddingWidgetClosed && !widgetEditMode && isAddingWidget) { + + + } + + + + diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss index 7b0d56b2e6..b1383603e0 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss @@ -195,3 +195,4 @@ div.tb-dashboard-page { } } + diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 7b9ef5e5e3..26a0ef2761 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -30,7 +30,6 @@ import { OnInit, Optional, Renderer2, - StaticProvider, ViewChild, ViewContainerRef, ViewEncapsulation, @@ -79,7 +78,6 @@ import { WidgetConfig, WidgetInfo, WidgetPosition, - widgetType, widgetTypesData } from '@shared/models/widget.models'; import { environment as env } from '@env/environment'; @@ -127,13 +125,6 @@ import { ImportExportService } from '@shared/import-export/import-export.service import { AuthState } from '@app/core/auth/auth.models'; import { FiltersDialogComponent, FiltersDialogData } from '@home/components/filter/filters-dialog.component'; import { Filters } from '@shared/models/query/query.models'; -import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; -import { ComponentPortal } from '@angular/cdk/portal'; -import { - DISPLAY_WIDGET_TYPES_PANEL_DATA, - DisplayWidgetTypesPanelComponent, - DisplayWidgetTypesPanelData -} from '@home/components/dashboard-page/widget-types-panel.component'; import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/dashboard-widget-select.component'; import { MobileService } from '@core/services/mobile.service'; @@ -245,7 +236,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC forceDashboardMobileMode = false; isAddingWidget = false; isAddingWidgetClosed = true; - filterWidgetTypes: widgetType[] = null; isToolbarOpened = false; isToolbarOpenedAnimate = false; @@ -381,7 +371,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC private renderer: Renderer2, private ngZone: NgZone, @Optional() @Inject('embeddedValue') private embeddedValue, - private overlay: Overlay, private viewContainerRef: ViewContainerRef, private cd: ChangeDetectorRef, public elRef: ElementRef, @@ -1689,59 +1678,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC return widgetContextActions; } - clearSelectedWidgetBundle() { - this.dashboardWidgetSelectComponent.search = ''; - this.dashboardWidgetSelectComponent.widgetsBundle = null; - this.dashboardWidgetSelectComponent.selectWidgetMode = 'bundles'; - } - - editWidgetsTypesToDisplay($event: Event) { - if ($event) { - $event.stopPropagation(); - } - const target = $event.target || $event.currentTarget; - const config = new OverlayConfig(); - config.backdropClass = 'cdk-overlay-transparent-backdrop'; - config.hasBackdrop = true; - const connectedPosition: ConnectedPosition = { - originX: 'end', - originY: 'bottom', - overlayX: 'end', - overlayY: 'top' - }; - config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) - .withPositions([connectedPosition]); - - const overlayRef = this.overlay.create(config); - overlayRef.backdropClick().subscribe(() => { - overlayRef.dispose(); - }); - - const filterWidgetTypes = this.dashboardWidgetSelectComponent.filterWidgetTypes; - const widgetTypesList = Array.from(this.dashboardWidgetSelectComponent.widgetTypes.values()).map(type => - ({type, display: filterWidgetTypes === null ? true : filterWidgetTypes.includes(type)})); - - const providers: StaticProvider[] = [ - { - provide: DISPLAY_WIDGET_TYPES_PANEL_DATA, - useValue: { - types: widgetTypesList, - typesUpdated: (newTypes) => { - this.filterWidgetTypes = newTypes.filter(type => type.display).map(type => type.type); - this.cd.markForCheck(); - } - } as DisplayWidgetTypesPanelData - }, - { - provide: OverlayRef, - useValue: overlayRef - } - ]; - const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); - overlayRef.attach(new ComponentPortal(DisplayWidgetTypesPanelComponent, this.viewContainerRef, injector)); - this.cd.markForCheck(); - } - public updateDashboardImage($event: Event) { if ($event) { $event.stopPropagation(); diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html index adc4e2ca0c..7d2ea9cdb5 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html @@ -16,55 +16,232 @@ --> - @if (selectWidgetMode === 'bundles') { - @if (widgetsBundle) { - - } @else { - - - - - - - {{item.title}} - - - @if (isSystem(item)) { - sys - } - i - + + + @if (showTopLevelWidgetModeToggle()) { + + + + + + + now_widgets + {{ 'widget.installed' | translate }} - - - + + + +