diff --git a/application/src/main/data/json/system/widget_bundles/status_indicators.json b/application/src/main/data/json/system/widget_bundles/status_indicators.json index 3c8192493b..135fd69938 100644 --- a/application/src/main/data/json/system/widget_bundles/status_indicators.json +++ b/application/src/main/data/json/system/widget_bundles/status_indicators.json @@ -2,15 +2,15 @@ "widgetsBundle": { "alias": "status_indicators", "title": "Status indicators", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAAjVBMVEUAAADu7u7u7u7g4OD///9c35Dw8PCt78fz/ffW9+PHx8dw457r+/Hj4+Pl+u6QkJDZ+OWsrKzM9tzf+er4/vuE56xYWFi6urp0dHTV1dXS9uCZ67qCgoKF56tm4Zeenp7C89VmZmZKSkq48c7A89Wj7cFLS0s9PT08PDyF56zi+euP6bN65aWZ67lZWVkXV4nvAAAABHRSTlMA799f7FlksgAABdJJREFUeNrt3I12mjAYBuBt/YAkJCJgCVQEFLX+7v4vbwHGQEnbeQwOXF7XVjjuvDxiCN04fHv5bvaaHy/fRB7Q8gN6znfheESLCT3HFI4HtGiIhlTREA0ZJSQi5ZJ6iKSlV8i7u4qAAYTiUfwEUzVE3qIeQkKWOku+4zvixEuydZcsVA2Rt6iGxCR1VhCIx9YJiANhylVD5C2qIUu+5StYsZ9sSVxRQXagGiJvUQvhbgSMc+CMRSYnplgKVUPkLb0ffmOmGiJv0fOIhmjIVTREQ54FMquiGiJv6REy8aocFEPkLdPeILM373dQj5CZVweph8wmvojXZCoWD0ghpGl5u2qhCiEIT5Ak04lSCJ3SWTfIPyiDzKazzJKE+kghZPZG90Y3J+9AVUHoJDNkOc6mCiET+iptSdBUFWSCLGnFAt4UQnxkyIM1REM0RENuhXjShvn4ICCbEHM0QgjCksD4IDSRNeTjgzzNYNcQDflHEJNzgDRIwYwD3h+EcAAeugBOyPuBLAMHyNYMiBuznQnAUrMHSBiEwHaRaFlGZYtr3grJv4CA64AbgpOGHLYMII7cVD2kqOCixQldCCIAJ0rTGyHobHezOHQgTkggEJDIiVk/ELeGkKLFYTdCQJ5rSMxDt9wjwGLH7W+PxBUEmHh2G4Tu7W5O/gXEXDkrRt7jFUC5151eIOYqfWdkm+5AJCVuqGKMHBsIY+KLmNW3MsRUCmlaXAIQ1cOcMz2PaIiGaMgzQ6gta7DGBwFEu0Ej/J0drHk3Gzo+SCaf2ccHeZrBriEaoiGfQuizQCZY2jBHU4WQA32VQ6gyCOCZJ/1vhSlVCEFT6YlQktOJMgiaTii6Dp1gtVc+THw0k2SCVV5UQyeSzEApBJA/lYTqC88uoyEaoiHtlu/Qc34IxyNaXn6Yveb7i3A8okVHR+fT6DsM/GX0HQZuylPN7BqiIWU05P+CmBGDCJowAvK4d0DUtsghP93A5GAWq8QXA+KUz0hafC/Xln9CCMw7IEpb5JClG5kBC+LQWa4ix4mLimWcxgHZ8jhO3SAgcfwe7cjWCW6E9N9yoRbvVcBTFjrEIakTFBVxGBGxaO6cpSsSQP24BdJfixyyIksSkNAtK1Z8VVYUa7gDK0JckSV5hy27A6K4RQ5hnAABnsYRi5hYMKPq6jvOouInEyHuFqKIALkR0n+L2VlVXwsnDQ9dFYffr1v0PNJEQzREQzREQzSkjI9F6PghuVEGK4Egf51v5kU21hqjh0JeK4h1N8TPj7ZxmWRv4ZFB8CYx5EmO2WggyGoUUsucjgFC58bX2eOhQySMxLbtpEuhg4ZYSVuw2KwP9fYi7FmLC8+cDhaC7ZZig5HsINB6iZ0NFJK3LprD8FEO56TZKUgtBPlY5E4I2v/ZGRaCz0Iz+89OoUohdjX87oJQW/o2y4OsP2isEIKNKndBascJX5+jFNfLW5aH0YX7XEvQsCC+UWVzMbL3RjunuQdNst9DJRsWBFUfefzFSUoyx9cfRjwsCFgXQxfvO4buQRcVH6/z0MYIeFaO6ve6YUgptPk7GAYHaZInl9ttn2z7krJBUGe4kPbuOG3WtF6NrUV7pwwe4tvNvEg/ngmTTBUEW0WoaghNPj+Raii5Ggj9ve9VQ46SsxQ5xVYBaTa+H8iJwsehxzFAMkPk3NodyMsty8q9Ns0qG4YNgWxxyqEOtezmQDXH7RdZMHBIK/6+cy9FaDIaCNoY3czp6CDUlp+e+CODtB2JnbQW/HFBvKt/g8B5LdvcD0G5JZI9BLKu58XOTGjdD5kbZbxHQFCx1QvaHf42vRfSbONjBvvmnHcnmfOGwsggH2a8ELS2MvoEkPIXFNsbPaSeUPyxQ7J6+hg75GxUWYwdkhtVjmOHHIwq2dghYFVDZPRHLYDDcXH0nmAe6WFmt1qfVvTabC9Kyuf2xYuGDAFvLYKhE7ougtovGv9FNc9zmZOGaIiG3Bl9h4EB3mHgF3NeY+W3xB1xAAAAAElFTkSuQmCC", + "image": "tb-image:c3RhdHVzX2luZGljYXRvcnNfc3lzdGVtX2J1bmRsZV9pbWFnZS5wbmc=:IlN0YXR1cyBpbmRpY2F0b3JzIiBzeXN0ZW0gYnVuZGxlIGltYWdl;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAAjVBMVEUAAADu7u7u7u7g4OD///9c35Dw8PCt78fz/ffW9+PHx8dw457r+/Hj4+Pl+u6QkJDZ+OWsrKzM9tzf+er4/vuE56xYWFi6urp0dHTV1dXS9uCZ67qCgoKF56tm4Zeenp7C89VmZmZKSkq48c7A89Wj7cFLS0s9PT08PDyF56zi+euP6bN65aWZ67lZWVkXV4nvAAAABHRSTlMA799f7FlksgAABdJJREFUeNrt3I12mjAYBuBt/YAkJCJgCVQEFLX+7v4vbwHGQEnbeQwOXF7XVjjuvDxiCN04fHv5bvaaHy/fRB7Q8gN6znfheESLCT3HFI4HtGiIhlTREA0ZJSQi5ZJ6iKSlV8i7u4qAAYTiUfwEUzVE3qIeQkKWOku+4zvixEuydZcsVA2Rt6iGxCR1VhCIx9YJiANhylVD5C2qIUu+5StYsZ9sSVxRQXagGiJvUQvhbgSMc+CMRSYnplgKVUPkLb0ffmOmGiJv0fOIhmjIVTREQ54FMquiGiJv6REy8aocFEPkLdPeILM373dQj5CZVweph8wmvojXZCoWD0ghpGl5u2qhCiEIT5Ak04lSCJ3SWTfIPyiDzKazzJKE+kghZPZG90Y3J+9AVUHoJDNkOc6mCiET+iptSdBUFWSCLGnFAt4UQnxkyIM1REM0RENuhXjShvn4ICCbEHM0QgjCksD4IDSRNeTjgzzNYNcQDflHEJNzgDRIwYwD3h+EcAAeugBOyPuBLAMHyNYMiBuznQnAUrMHSBiEwHaRaFlGZYtr3grJv4CA64AbgpOGHLYMII7cVD2kqOCixQldCCIAJ0rTGyHobHezOHQgTkggEJDIiVk/ELeGkKLFYTdCQJ5rSMxDt9wjwGLH7W+PxBUEmHh2G4Tu7W5O/gXEXDkrRt7jFUC5151eIOYqfWdkm+5AJCVuqGKMHBsIY+KLmNW3MsRUCmlaXAIQ1cOcMz2PaIiGaMgzQ6gta7DGBwFEu0Ej/J0drHk3Gzo+SCaf2ccHeZrBriEaoiGfQuizQCZY2jBHU4WQA32VQ6gyCOCZJ/1vhSlVCEFT6YlQktOJMgiaTii6Dp1gtVc+THw0k2SCVV5UQyeSzEApBJA/lYTqC88uoyEaoiHtlu/Qc34IxyNaXn6Yveb7i3A8okVHR+fT6DsM/GX0HQZuylPN7BqiIWU05P+CmBGDCJowAvK4d0DUtsghP93A5GAWq8QXA+KUz0hafC/Xln9CCMw7IEpb5JClG5kBC+LQWa4ix4mLimWcxgHZ8jhO3SAgcfwe7cjWCW6E9N9yoRbvVcBTFjrEIakTFBVxGBGxaO6cpSsSQP24BdJfixyyIksSkNAtK1Z8VVYUa7gDK0JckSV5hy27A6K4RQ5hnAABnsYRi5hYMKPq6jvOouInEyHuFqKIALkR0n+L2VlVXwsnDQ9dFYffr1v0PNJEQzREQzREQzSkjI9F6PghuVEGK4Egf51v5kU21hqjh0JeK4h1N8TPj7ZxmWRv4ZFB8CYx5EmO2WggyGoUUsucjgFC58bX2eOhQySMxLbtpEuhg4ZYSVuw2KwP9fYi7FmLC8+cDhaC7ZZig5HsINB6iZ0NFJK3LprD8FEO56TZKUgtBPlY5E4I2v/ZGRaCz0Iz+89OoUohdjX87oJQW/o2y4OsP2isEIKNKndBascJX5+jFNfLW5aH0YX7XEvQsCC+UWVzMbL3RjunuQdNst9DJRsWBFUfefzFSUoyx9cfRjwsCFgXQxfvO4buQRcVH6/z0MYIeFaO6ve6YUgptPk7GAYHaZInl9ttn2z7krJBUGe4kPbuOG3WtF6NrUV7pwwe4tvNvEg/ngmTTBUEW0WoaghNPj+Raii5Ggj9ve9VQ46SsxQ5xVYBaTa+H8iJwsehxzFAMkPk3NodyMsty8q9Ns0qG4YNgWxxyqEOtezmQDXH7RdZMHBIK/6+cy9FaDIaCNoY3czp6CDUlp+e+CODtB2JnbQW/HFBvKt/g8B5LdvcD0G5JZI9BLKu58XOTGjdD5kbZbxHQFCx1QvaHf42vRfSbONjBvvmnHcnmfOGwsggH2a8ELS2MvoEkPIXFNsbPaSeUPyxQ7J6+hg75GxUWYwdkhtVjmOHHIwq2dghYFVDZPRHLYDDcXH0nmAe6WFmt1qfVvTabC9Kyuf2xYuGDAFvLYKhE7ougtovGv9FNc9zmZOGaIiG3Bl9h4EB3mHgF3NeY+W3xB1xAAAAAElFTkSuQmCC", "description": "Contains widgets displaying battery level and signal strength.", "order": 9000, - "externalId": null, "name": "Status indicators" }, "widgetTypeFqns": [ "battery_level", "signal_strength", - "progress_bar" + "progress_bar", + "status_widget" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/status_widget.json b/application/src/main/data/json/system/widget_types/status_widget.json new file mode 100644 index 0000000000..97dc721c0f --- /dev/null +++ b/application/src/main/data/json/system/widget_types/status_widget.json @@ -0,0 +1,26 @@ +{ + "fqn": "status_widget", + "name": "Status widget", + "deprecated": false, + "image": "tb-image:c3RhdHVzLXdpZGdldF8oMykuc3Zn:IlN0YXR1cyB3aWRnZXQiIHN5c3RlbSB3aWRnZXQgaW1hZ2U=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTYwIiByeD0iNS40NTY2OSIgZmlsbD0iIzNGNTJERCIvPgo8cGF0aCBkPSJNNTIuNDE2NiAyMC43NUgxNy41ODMzVjE3LjU4MzNINTIuNDE2NlYyMC43NVpNMTkuMTY2NiA1MC44MzMzSDI1LjQ5OTlDMjUuNDk5OSA0Ni4wODMzIDIyLjMzMzMgNDIuOTE2NiAyMi4zMzMzIDQyLjkxNjZDMzEuODMzMyAzNi41ODMzIDMzLjQxNjYgMjIuMzMzMyAzMy40MTY2IDIyLjMzMzNIMTkuMTY2NlY1MC44MzMzWk01MC44MzMzIDIyLjMzMzNIMzYuNTgzM0MzNi41ODMzIDIyLjMzMzMgMzguMTY2NiAzNi41ODMzIDQ3LjY2NjYgNDIuOTE2NkM0Ny42NjY2IDQyLjkxNjYgNDQuNDk5OSA0Ni4wODMzIDQ0LjQ5OTkgNTAuODMzM0g1MC44MzMzVjIyLjMzMzNaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTkuNTA2OCAxMTIuNzY1TDIxLjQ4OTMgMTA1LjA0N0gyMi41MjgzTDIyLjQxMjEgMTA3LjAzNkwyMC4yOTMgMTE1SDE5LjIxMjlMMTkuNTA2OCAxMTIuNzY1Wk0xOC4wMjM0IDEwNS4wNDdMMTkuNjI5OSAxMTIuNzAzTDE5Ljc3MzQgMTE1SDE4LjYyNUwxNi4zMjEzIDEwNS4wNDdIMTguMDIzNFpNMjQuNzc3MyAxMTIuNjgzTDI2LjM2MzMgMTA1LjA0N0gyOC4wNzIzTDI1Ljc2ODYgMTE1SDI0LjYyMDFMMjQuNzc3MyAxMTIuNjgzWk0yMi45MzE2IDEwNS4wNDdMMjQuODkzNiAxMTIuNzg1TDI1LjE4MDcgMTE1SDI0LjEwMDZMMjIuMDE1NiAxMDcuMDM2TDIxLjkwNjIgMTA1LjA0N0gyMi45MzE2Wk0zMS4xOTM0IDEwNy42MDRWMTE1SDI5LjUzOTFWMTA3LjYwNEgzMS4xOTM0Wk0yOS40Mjk3IDEwNS42NjJDMjkuNDI5NyAxMDUuNDExIDI5LjUxMTcgMTA1LjIwNCAyOS42NzU4IDEwNS4wNEMyOS44NDQ0IDEwNC44NzEgMzAuMDc2OCAxMDQuNzg3IDMwLjM3MyAxMDQuNzg3QzMwLjY2NDcgMTA0Ljc4NyAzMC44OTQ5IDEwNC44NzEgMzEuMDYzNSAxMDUuMDRDMzEuMjMyMSAxMDUuMjA0IDMxLjMxNjQgMTA1LjQxMSAzMS4zMTY0IDEwNS42NjJDMzEuMzE2NCAxMDUuOTA4IDMxLjIzMjEgMTA2LjExMyAzMS4wNjM1IDEwNi4yNzdDMzAuODk0OSAxMDYuNDQxIDMwLjY2NDcgMTA2LjUyMyAzMC4zNzMgMTA2LjUyM0MzMC4wNzY4IDEwNi41MjMgMjkuODQ0NCAxMDYuNDQxIDI5LjY3NTggMTA2LjI3N0MyOS41MTE3IDEwNi4xMTMgMjkuNDI5NyAxMDUuOTA4IDI5LjQyOTcgMTA1LjY2MlpNMzQuODgxOCAxMDkuMTgzVjExNUgzMy4yMzQ0VjEwNy42MDRIMzQuNzg2MUwzNC44ODE4IDEwOS4xODNaTTM0LjU4NzkgMTExLjAyOEwzNC4wNTQ3IDExMS4wMjFDMzQuMDU5MiAxMTAuNDk3IDM0LjEzMjIgMTEwLjAxNyAzNC4yNzM0IDEwOS41NzlDMzQuNDE5MyAxMDkuMTQyIDM0LjYxOTggMTA4Ljc2NiAzNC44NzUgMTA4LjQ1MUMzNS4xMzQ4IDEwOC4xMzcgMzUuNDQ0NyAxMDcuODk1IDM1LjgwNDcgMTA3LjcyN0MzNi4xNjQ3IDEwNy41NTMgMzYuNTY1OCAxMDcuNDY3IDM3LjAwNzggMTA3LjQ2N0MzNy4zNjMzIDEwNy40NjcgMzcuNjg0NiAxMDcuNTE3IDM3Ljk3MTcgMTA3LjYxN0MzOC4yNjMzIDEwNy43MTMgMzguNTExNyAxMDcuODcgMzguNzE2OCAxMDguMDg5QzM4LjkyNjQgMTA4LjMwOCAzOS4wODU5IDEwOC41OTIgMzkuMTk1MyAxMDguOTQzQzM5LjMwNDcgMTA5LjI5IDM5LjM1OTQgMTA5LjcxNiAzOS4zNTk0IDExMC4yMjJWMTE1SDM3LjcwNTFWMTEwLjIxNUMzNy43MDUxIDEwOS44NTkgMzcuNjUyNyAxMDkuNTc5IDM3LjU0NzkgMTA5LjM3NEMzNy40NDc2IDEwOS4xNjQgMzcuMjk5NSAxMDkuMDE2IDM3LjEwMzUgMTA4LjkzQzM2LjkxMjEgMTA4LjgzOSAzNi42NzI5IDEwOC43OTMgMzYuMzg1NyAxMDguNzkzQzM2LjEwMzIgMTA4Ljc5MyAzNS44NTAzIDEwOC44NTIgMzUuNjI3IDEwOC45NzFDMzUuNDAzNiAxMDkuMDg5IDM1LjIxNDUgMTA5LjI1MSAzNS4wNTk2IDEwOS40NTZDMzQuOTA5MiAxMDkuNjYxIDM0Ljc5MyAxMDkuODk4IDM0LjcxMDkgMTEwLjE2N0MzNC42Mjg5IDExMC40MzYgMzQuNTg3OSAxMTAuNzIzIDM0LjU4NzkgMTExLjAyOFpNNDUuODAyNyAxMTMuNDY5VjEwNC41SDQ3LjQ1N1YxMTVINDUuOTZMNDUuODAyNyAxMTMuNDY5Wk00MC45OTAyIDExMS4zODRWMTExLjI0QzQwLjk5MDIgMTEwLjY4IDQxLjA1NjMgMTEwLjE2OSA0MS4xODg1IDEwOS43MDlDNDEuMzIwNiAxMDkuMjQ0IDQxLjUxMiAxMDguODQ1IDQxLjc2MjcgMTA4LjUxM0M0Mi4wMTMzIDEwOC4xNzUgNDIuMzE4NyAxMDcuOTE4IDQyLjY3ODcgMTA3Ljc0QzQzLjAzODcgMTA3LjU1OCA0My40NDQzIDEwNy40NjcgNDMuODk1NSAxMDcuNDY3QzQ0LjM0MjEgMTA3LjQ2NyA0NC43MzQgMTA3LjU1MyA0NS4wNzEzIDEwNy43MjdDNDUuNDA4NSAxMDcuOSA0NS42OTU2IDEwOC4xNDggNDUuOTMyNiAxMDguNDcyQzQ2LjE2OTYgMTA4Ljc5MSA0Ni4zNTg3IDEwOS4xNzQgNDYuNSAxMDkuNjJDNDYuNjQxMyAxMTAuMDYyIDQ2Ljc0MTUgMTEwLjU1NCA0Ni44MDA4IDExMS4wOTdWMTExLjU1NUM0Ni43NDE1IDExMi4wODMgNDYuNjQxMyAxMTIuNTY2IDQ2LjUgMTEzLjAwNEM0Ni4zNTg3IDExMy40NDEgNDYuMTY5NiAxMTMuODIgNDUuOTMyNiAxMTQuMTM5QzQ1LjY5NTYgMTE0LjQ1OCA0NS40MDYyIDExNC43MDQgNDUuMDY0NSAxMTQuODc3QzQ0LjcyNzIgMTE1LjA1IDQ0LjMzMyAxMTUuMTM3IDQzLjg4MTggMTE1LjEzN0M0My40MzUyIDExNS4xMzcgNDMuMDMxOSAxMTUuMDQzIDQyLjY3MTkgMTE0Ljg1NkM0Mi4zMTY0IDExNC42NyA0Mi4wMTMzIDExNC40MDggNDEuNzYyNyAxMTQuMDdDNDEuNTEyIDExMy43MzMgNDEuMzIwNiAxMTMuMzM3IDQxLjE4ODUgMTEyLjg4MUM0MS4wNTYzIDExMi40MjEgNDAuOTkwMiAxMTEuOTIyIDQwLjk5MDIgMTExLjM4NFpNNDIuNjM3NyAxMTEuMjRWMTExLjM4NEM0Mi42Mzc3IDExMS43MjEgNDIuNjY3MyAxMTIuMDM1IDQyLjcyNjYgMTEyLjMyN0M0Mi43OTA0IDExMi42MTkgNDIuODg4MyAxMTIuODc2IDQzLjAyMDUgMTEzLjFDNDMuMTUyNyAxMTMuMzE4IDQzLjMyMzYgMTEzLjQ5MiA0My41MzMyIDExMy42MTlDNDMuNzQ3NCAxMTMuNzQyIDQ0LjAwMjYgMTEzLjgwNCA0NC4yOTg4IDExMy44MDRDNDQuNjcyNSAxMTMuODA0IDQ0Ljk4MDEgMTEzLjcyMiA0NS4yMjE3IDExMy41NThDNDUuNDYzMiAxMTMuMzk0IDQ1LjY1MjMgMTEzLjE3MyA0NS43ODkxIDExMi44OTVDNDUuOTMwMyAxMTIuNjEyIDQ2LjAyNiAxMTIuMjk4IDQ2LjA3NjIgMTExLjk1MVYxMTAuNzE0QzQ2LjA0ODggMTEwLjQ0NSA0NS45OTE5IDExMC4xOTQgNDUuOTA1MyAxMDkuOTYyQzQ1LjgyMzIgMTA5LjcyOSA0NS43MTE2IDEwOS41MjcgNDUuNTcwMyAxMDkuMzU0QzQ1LjQyOSAxMDkuMTc2IDQ1LjI1MzYgMTA5LjAzOSA0NS4wNDM5IDEwOC45NDNDNDQuODM4OSAxMDguODQzIDQ0LjU5NTEgMTA4Ljc5MyA0NC4zMTI1IDEwOC43OTNDNDQuMDExNyAxMDguNzkzIDQzLjc1NjUgMTA4Ljg1NyA0My41NDY5IDEwOC45ODRDNDMuMzM3MiAxMDkuMTEyIDQzLjE2NDEgMTA5LjI4NyA0My4wMjczIDEwOS41MTFDNDIuODk1MiAxMDkuNzM0IDQyLjc5NzIgMTA5Ljk5NCA0Mi43MzM0IDExMC4yOUM0Mi42Njk2IDExMC41ODYgNDIuNjM3NyAxMTAuOTAzIDQyLjYzNzcgMTExLjI0Wk00OS4xMTUyIDExMS4zODRWMTExLjIyN0M0OS4xMTUyIDExMC42OTMgNDkuMTkyNyAxMTAuMTk5IDQ5LjM0NzcgMTA5Ljc0M0M0OS41MDI2IDEwOS4yODMgNDkuNzI1OSAxMDguODg0IDUwLjAxNzYgMTA4LjU0N0M1MC4zMTM4IDEwOC4yMDUgNTAuNjczOCAxMDcuOTQxIDUxLjA5NzcgMTA3Ljc1NEM1MS41MjYgMTA3LjU2MiA1Mi4wMDkxIDEwNy40NjcgNTIuNTQ2OSAxMDcuNDY3QzUzLjA4OTIgMTA3LjQ2NyA1My41NzIzIDEwNy41NjIgNTMuOTk2MSAxMDcuNzU0QzU0LjQyNDUgMTA3Ljk0MSA1NC43ODY4IDEwOC4yMDUgNTUuMDgzIDEwOC41NDdDNTUuMzc5MiAxMDguODg0IDU1LjYwNDggMTA5LjI4MyA1NS43NTk4IDEwOS43NDNDNTUuOTE0NyAxMTAuMTk5IDU1Ljk5MjIgMTEwLjY5MyA1NS45OTIyIDExMS4yMjdWMTExLjM4NEM1NS45OTIyIDExMS45MTcgNTUuOTE0NyAxMTIuNDExIDU1Ljc1OTggMTEyLjg2N0M1NS42MDQ4IDExMy4zMjMgNTUuMzc5MiAxMTMuNzIyIDU1LjA4MyAxMTQuMDYzQzU0Ljc4NjggMTE0LjQwMSA1NC40MjY4IDExNC42NjUgNTQuMDAyOSAxMTQuODU2QzUzLjU3OTEgMTE1LjA0MyA1My4wOTgzIDExNS4xMzcgNTIuNTYwNSAxMTUuMTM3QzUyLjAxODIgMTE1LjEzNyA1MS41MzI5IDExNS4wNDMgNTEuMTA0NSAxMTQuODU2QzUwLjY4MDcgMTE0LjY2NSA1MC4zMjA2IDExNC40MDEgNTAuMDI0NCAxMTQuMDYzQzQ5LjcyODIgMTEzLjcyMiA0OS41MDI2IDExMy4zMjMgNDkuMzQ3NyAxMTIuODY3QzQ5LjE5MjcgMTEyLjQxMSA0OS4xMTUyIDExMS45MTcgNDkuMTE1MiAxMTEuMzg0Wk01MC43NjI3IDExMS4yMjdWMTExLjM4NEM1MC43NjI3IDExMS43MTYgNTAuNzk2OSAxMTIuMDMxIDUwLjg2NTIgMTEyLjMyN0M1MC45MzM2IDExMi42MjMgNTEuMDQwNyAxMTIuODgzIDUxLjE4NjUgMTEzLjEwNkM1MS4zMzI0IDExMy4zMyA1MS41MTkyIDExMy41MDUgNTEuNzQ3MSAxMTMuNjMzQzUxLjk3NDkgMTEzLjc2IDUyLjI0NjEgMTEzLjgyNCA1Mi41NjA1IDExMy44MjRDNTIuODY1OSAxMTMuODI0IDUzLjEzMDIgMTEzLjc2IDUzLjM1MzUgMTEzLjYzM0M1My41ODE0IDExMy41MDUgNTMuNzY4MiAxMTMuMzMgNTMuOTE0MSAxMTMuMTA2QzU0LjA1OTkgMTEyLjg4MyA1NC4xNjcgMTEyLjYyMyA1NC4yMzU0IDExMi4zMjdDNTQuMzA4MyAxMTIuMDMxIDU0LjM0NDcgMTExLjcxNiA1NC4zNDQ3IDExMS4zODRWMTExLjIyN0M1NC4zNDQ3IDExMC44OTggNTQuMzA4MyAxMTAuNTg5IDU0LjIzNTQgMTEwLjI5N0M1NC4xNjcgMTEwLjAwMSA1NC4wNTc2IDEwOS43MzkgNTMuOTA3MiAxMDkuNTExQzUzLjc2MTQgMTA5LjI4MyA1My41NzQ1IDEwOS4xMDUgNTMuMzQ2NyAxMDguOTc4QzUzLjEyMzQgMTA4Ljg0NSA1Mi44NTY4IDEwOC43NzkgNTIuNTQ2OSAxMDguNzc5QzUyLjIzNyAxMDguNzc5IDUxLjk2ODEgMTA4Ljg0NSA1MS43NDAyIDEwOC45NzhDNTEuNTE2OSAxMDkuMTA1IDUxLjMzMjQgMTA5LjI4MyA1MS4xODY1IDEwOS41MTFDNTEuMDQwNyAxMDkuNzM5IDUwLjkzMzYgMTEwLjAwMSA1MC44NjUyIDExMC4yOTdDNTAuNzk2OSAxMTAuNTg5IDUwLjc2MjcgMTEwLjg5OCA1MC43NjI3IDExMS4yMjdaTTU5LjYzMjggMTEzLjM1OUw2MS4zNDE4IDEwNy42MDRINjIuMzk0NUw2Mi4xMDc0IDEwOS4zMjZMNjAuMzg0OCAxMTVINTkuNDQxNEw1OS42MzI4IDExMy4zNTlaTTU4LjYyNzkgMTA3LjYwNEw1OS45NjA5IDExMy4zODdMNjAuMDcwMyAxMTVINTkuMDE3Nkw1Ny4wMTQ2IDEwNy42MDRINTguNjI3OVpNNjMuOTk0MSAxMTMuMzE4TDY1LjI4NjEgMTA3LjYwNEg2Ni44OTI2TDY0Ljg5NjUgMTE1SDYzLjg0MzhMNjMuOTk0MSAxMTMuMzE4Wk02Mi41NzIzIDEwNy42MDRMNjQuMjYwNyAxMTMuMjkxTDY0LjQ3MjcgMTE1SDYzLjUyOTNMNjEuNzg2MSAxMDkuMzE5TDYxLjQ5OSAxMDcuNjA0SDYyLjU3MjNaTTczLjc2MzcgMTA0LjVWMTE1SDcyLjEwOTRWMTA0LjVINzMuNzYzN1pNNzkuMDc5MSAxMTUuMTM3Qzc4LjUzMjIgMTE1LjEzNyA3OC4wMzc4IDExNS4wNDggNzcuNTk1NyAxMTQuODdDNzcuMTU4MiAxMTQuNjg4IDc2Ljc4NDUgMTE0LjQzNSA3Ni40NzQ2IDExNC4xMTFDNzYuMTY5MyAxMTMuNzg4IDc1LjkzNDYgMTEzLjQwNyA3NS43NzA1IDExMi45N0M3NS42MDY0IDExMi41MzIgNzUuNTI0NCAxMTIuMDYxIDc1LjUyNDQgMTExLjU1NVYxMTEuMjgxQzc1LjUyNDQgMTEwLjcwMiA3NS42MDg3IDExMC4xNzggNzUuNzc3MyAxMDkuNzA5Qzc1Ljk0NiAxMDkuMjQgNzYuMTgwNyAxMDguODM5IDc2LjQ4MTQgMTA4LjUwNkM3Ni43ODIyIDEwOC4xNjkgNzcuMTM3NyAxMDcuOTExIDc3LjU0NzkgMTA3LjczM0M3Ny45NTggMTA3LjU1NiA3OC40MDIzIDEwNy40NjcgNzguODgwOSAxMDcuNDY3Qzc5LjQwOTUgMTA3LjQ2NyA3OS44NzIxIDEwNy41NTYgODAuMjY4NiAxMDcuNzMzQzgwLjY2NSAxMDcuOTExIDgwLjk5MzIgMTA4LjE2MiA4MS4yNTI5IDEwOC40ODVDODEuNTE3MyAxMDguODA0IDgxLjcxMzIgMTA5LjE4NSA4MS44NDA4IDEwOS42MjdDODEuOTczIDExMC4wNjkgODIuMDM5MSAxMTAuNTU3IDgyLjAzOTEgMTExLjA5VjExMS43OTRINzYuMzI0MlYxMTAuNjExSDgwLjQxMjFWMTEwLjQ4MUM4MC40MDMgMTEwLjE4NSA4MC4zNDM4IDEwOS45MDcgODAuMjM0NCAxMDkuNjQ3QzgwLjEyOTYgMTA5LjM4OCA3OS45Njc4IDEwOS4xNzggNzkuNzQ5IDEwOS4wMTlDNzkuNTMwMyAxMDguODU5IDc5LjIzODYgMTA4Ljc3OSA3OC44NzQgMTA4Ljc3OUM3OC42MDA2IDEwOC43NzkgNzguMzU2OCAxMDguODM5IDc4LjE0MjYgMTA4Ljk1N0M3Ny45MzI5IDEwOS4wNzEgNzcuNzU3NSAxMDkuMjM3IDc3LjYxNjIgMTA5LjQ1NkM3Ny40NzQ5IDEwOS42NzUgNzcuMzY1NiAxMDkuOTM5IDc3LjI4ODEgMTEwLjI0OUM3Ny4yMTUyIDExMC41NTQgNzcuMTc4NyAxMTAuODk4IDc3LjE3ODcgMTExLjI4MVYxMTEuNTU1Qzc3LjE3ODcgMTExLjg3OCA3Ny4yMjIgMTEyLjE3OSA3Ny4zMDg2IDExMi40NTdDNzcuMzk5NyAxMTIuNzMgNzcuNTMxOSAxMTIuOTcgNzcuNzA1MSAxMTMuMTc1Qzc3Ljg3ODMgMTEzLjM4IDc4LjA4NzkgMTEzLjU0MiA3OC4zMzQgMTEzLjY2Qzc4LjU4MDEgMTEzLjc3NCA3OC44NjA0IDExMy44MzEgNzkuMTc0OCAxMTMuODMxQzc5LjU3MTMgMTEzLjgzMSA3OS45MjQ1IDExMy43NTEgODAuMjM0NCAxMTMuNTkyQzgwLjU0NDMgMTEzLjQzMiA4MC44MTMyIDExMy4yMDcgODEuMDQxIDExMi45MTVMODEuOTA5MiAxMTMuNzU2QzgxLjc0OTcgMTEzLjk4OCA4MS41NDIzIDExNC4yMTIgODEuMjg3MSAxMTQuNDI2QzgxLjAzMTkgMTE0LjYzNSA4MC43MTk3IDExNC44MDYgODAuMzUwNiAxMTQuOTM4Qzc5Ljk4NiAxMTUuMDcxIDc5LjU2MjIgMTE1LjEzNyA3OS4wNzkxIDExNS4xMzdaTTg1LjgwMjcgMTE1SDg0LjE1NTNWMTA2Ljg5M0M4NC4xNTUzIDEwNi4zNDEgODQuMjU3OCAxMDUuODc5IDg0LjQ2MjkgMTA1LjUwNUM4NC42NzI1IDEwNS4xMjcgODQuOTcxIDEwNC44NDIgODUuMzU4NCAxMDQuNjVDODUuNzQ1OCAxMDQuNDU0IDg2LjIwMzggMTA0LjM1NiA4Ni43MzI0IDEwNC4zNTZDODYuODk2NSAxMDQuMzU2IDg3LjA1ODMgMTA0LjM2OCA4Ny4yMTc4IDEwNC4zOTFDODcuMzc3MyAxMDQuNDA5IDg3LjUzMjIgMTA0LjQzOCA4Ny42ODI2IDEwNC40NzlMODcuNjQxNiAxMDUuNzUxQzg3LjU1MDUgMTA1LjcyOCA4Ny40NTAyIDEwNS43MTIgODcuMzQwOCAxMDUuNzAzQzg3LjIzNiAxMDUuNjk0IDg3LjEyMjEgMTA1LjY4OSA4Ni45OTkgMTA1LjY4OUM4Ni43NDg0IDEwNS42ODkgODYuNTMxOSAxMDUuNzM3IDg2LjM0OTYgMTA1LjgzM0M4Ni4xNzE5IDEwNS45MjQgODYuMDM1MiAxMDYuMDU5IDg1LjkzOTUgMTA2LjIzNkM4NS44NDgzIDEwNi40MTQgODUuODAyNyAxMDYuNjMzIDg1LjgwMjcgMTA2Ljg5M1YxMTVaTTg3LjMyNzEgMTA3LjYwNFYxMDguODA3SDgzLjAyMDVWMTA3LjYwNEg4Ny4zMjcxWk05Mi4xNjQxIDEwNy42MDRWMTA4LjgwN0g4Ny45OTQxVjEwNy42MDRIOTIuMTY0MVpNODkuMTk3MyAxMDUuNzkySDkwLjg0NDdWMTEyLjk1NkM5MC44NDQ3IDExMy4xODQgOTAuODc2NiAxMTMuMzU5IDkwLjk0MDQgMTEzLjQ4MkM5MS4wMDg4IDExMy42MDEgOTEuMTAyMiAxMTMuNjgxIDkxLjIyMDcgMTEzLjcyMkM5MS4zMzkyIDExMy43NjMgOTEuNDc4MiAxMTMuNzgzIDkxLjYzNzcgMTEzLjc4M0M5MS43NTE2IDExMy43ODMgOTEuODYxIDExMy43NzYgOTEuOTY1OCAxMTMuNzYzQzkyLjA3MDYgMTEzLjc0OSA5Mi4xNTQ5IDExMy43MzUgOTIuMjE4OCAxMTMuNzIyTDkyLjIyNTYgMTE0Ljk3OUM5Mi4wODg5IDExNS4wMjEgOTEuOTI5NCAxMTUuMDU3IDkxLjc0NzEgMTE1LjA4OUM5MS41NjkzIDExNS4xMjEgOTEuMzY0MyAxMTUuMTM3IDkxLjEzMTggMTE1LjEzN0M5MC43NTM2IDExNS4xMzcgOTAuNDE4NiAxMTUuMDcxIDkwLjEyNyAxMTQuOTM4Qzg5LjgzNTMgMTE0LjgwMiA4OS42MDc0IDExNC41ODEgODkuNDQzNCAxMTQuMjc1Qzg5LjI3OTMgMTEzLjk3IDg5LjE5NzMgMTEzLjU2NCA4OS4xOTczIDExMy4wNTlWMTA1Ljc5MlpNMTAwLjQ0MyAxMTMuODI0QzEwMC43MTIgMTEzLjgyNCAxMDAuOTU0IDExMy43NzIgMTAxLjE2OCAxMTMuNjY3QzEwMS4zODcgMTEzLjU1OCAxMDEuNTYyIDExMy40MDcgMTAxLjY5NCAxMTMuMjE2QzEwMS44MzEgMTEzLjAyNCAxMDEuOTA2IDExMi44MDMgMTAxLjkyIDExMi41NTNIMTAzLjQ3MkMxMDMuNDYzIDExMy4wMzEgMTAzLjMyMSAxMTMuNDY2IDEwMy4wNDggMTEzLjg1OEMxMDIuNzc0IDExNC4yNSAxMDIuNDEyIDExNC41NjIgMTAxLjk2MSAxMTQuNzk1QzEwMS41MSAxMTUuMDIzIDEwMS4wMTEgMTE1LjEzNyAxMDAuNDY0IDExNS4xMzdDOTkuODk4OCAxMTUuMTM3IDk5LjQwNjYgMTE1LjA0MSA5OC45ODczIDExNC44NUM5OC41NjggMTE0LjY1NCA5OC4yMTk0IDExNC4zODUgOTcuOTQxNCAxMTQuMDQzQzk3LjY2MzQgMTEzLjcwMSA5Ny40NTM4IDExMy4zMDcgOTcuMzEyNSAxMTIuODZDOTcuMTc1OCAxMTIuNDE0IDk3LjEwNzQgMTExLjkzNSA5Ny4xMDc0IDExMS40MjVWMTExLjE4NkM5Ny4xMDc0IDExMC42NzUgOTcuMTc1OCAxMTAuMTk3IDk3LjMxMjUgMTA5Ljc1Qzk3LjQ1MzggMTA5LjI5OSA5Ny42NjM0IDEwOC45MDIgOTcuOTQxNCAxMDguNTYxQzk4LjIxOTQgMTA4LjIxOSA5OC41NjggMTA3Ljk1MiA5OC45ODczIDEwNy43NjFDOTkuNDA2NiAxMDcuNTY1IDk5Ljg5NjUgMTA3LjQ2NyAxMDAuNDU3IDEwNy40NjdDMTAxLjA0OSAxMDcuNDY3IDEwMS41NjkgMTA3LjU4NSAxMDIuMDE2IDEwNy44MjJDMTAyLjQ2MiAxMDguMDU1IDEwMi44MTMgMTA4LjM4MSAxMDMuMDY4IDEwOC44QzEwMy4zMjggMTA5LjIxNSAxMDMuNDYzIDEwOS42OTggMTAzLjQ3MiAxMTAuMjQ5SDEwMS45MkMxMDEuOTA2IDEwOS45NzYgMTAxLjgzOCAxMDkuNzI5IDEwMS43MTUgMTA5LjUxMUMxMDEuNTk2IDEwOS4yODcgMTAxLjQyOCAxMDkuMTEgMTAxLjIwOSAxMDguOTc4QzEwMC45OTUgMTA4Ljg0NSAxMDAuNzM3IDEwOC43NzkgMTAwLjQzNyAxMDguNzc5QzEwMC4xMDQgMTA4Ljc3OSA5OS44MjgxIDEwOC44NDggOTkuNjA5NCAxMDguOTg0Qzk5LjM5MDYgMTA5LjExNyA5OS4yMTk3IDEwOS4yOTkgOTkuMDk2NyAxMDkuNTMxQzk4Ljk3MzYgMTA5Ljc1OSA5OC44ODQ4IDExMC4wMTcgOTguODMwMSAxMTAuMzA0Qzk4Ljc3OTkgMTEwLjU4NiA5OC43NTQ5IDExMC44OCA5OC43NTQ5IDExMS4xODZWMTExLjQyNUM5OC43NTQ5IDExMS43MyA5OC43Nzk5IDExMi4wMjYgOTguODMwMSAxMTIuMzEzQzk4Ljg4MDIgMTEyLjYwMSA5OC45NjY4IDExMi44NTggOTkuMDg5OCAxMTMuMDg2Qzk5LjIxNzQgMTEzLjMwOSA5OS4zOTA2IDExMy40ODkgOTkuNjA5NCAxMTMuNjI2Qzk5LjgyODEgMTEzLjc1OCAxMDAuMTA2IDExMy44MjQgMTAwLjQ0MyAxMTMuODI0Wk0xMDQuNjcyIDExMS4zODRWMTExLjIyN0MxMDQuNjcyIDExMC42OTMgMTA0Ljc0OSAxMTAuMTk5IDEwNC45MDQgMTA5Ljc0M0MxMDUuMDU5IDEwOS4yODMgMTA1LjI4MyAxMDguODg0IDEwNS41NzQgMTA4LjU0N0MxMDUuODcgMTA4LjIwNSAxMDYuMjMgMTA3Ljk0MSAxMDYuNjU0IDEwNy43NTRDMTA3LjA4MyAxMDcuNTYyIDEwNy41NjYgMTA3LjQ2NyAxMDguMTA0IDEwNy40NjdDMTA4LjY0NiAxMDcuNDY3IDEwOS4xMjkgMTA3LjU2MiAxMDkuNTUzIDEwNy43NTRDMTA5Ljk4MSAxMDcuOTQxIDExMC4zNDMgMTA4LjIwNSAxMTAuNjQgMTA4LjU0N0MxMTAuOTM2IDEwOC44ODQgMTExLjE2MSAxMDkuMjgzIDExMS4zMTYgMTA5Ljc0M0MxMTEuNDcxIDExMC4xOTkgMTExLjU0OSAxMTAuNjkzIDExMS41NDkgMTExLjIyN1YxMTEuMzg0QzExMS41NDkgMTExLjkxNyAxMTEuNDcxIDExMi40MTEgMTExLjMxNiAxMTIuODY3QzExMS4xNjEgMTEzLjMyMyAxMTAuOTM2IDExMy43MjIgMTEwLjY0IDExNC4wNjNDMTEwLjM0MyAxMTQuNDAxIDEwOS45ODMgMTE0LjY2NSAxMDkuNTYgMTE0Ljg1NkMxMDkuMTM2IDExNS4wNDMgMTA4LjY1NSAxMTUuMTM3IDEwOC4xMTcgMTE1LjEzN0MxMDcuNTc1IDExNS4xMzcgMTA3LjA5IDExNS4wNDMgMTA2LjY2MSAxMTQuODU2QzEwNi4yMzcgMTE0LjY2NSAxMDUuODc3IDExNC40MDEgMTA1LjU4MSAxMTQuMDYzQzEwNS4yODUgMTEzLjcyMiAxMDUuMDU5IDExMy4zMjMgMTA0LjkwNCAxMTIuODY3QzEwNC43NDkgMTEyLjQxMSAxMDQuNjcyIDExMS45MTcgMTA0LjY3MiAxMTEuMzg0Wk0xMDYuMzE5IDExMS4yMjdWMTExLjM4NEMxMDYuMzE5IDExMS43MTYgMTA2LjM1NCAxMTIuMDMxIDEwNi40MjIgMTEyLjMyN0MxMDYuNDkgMTEyLjYyMyAxMDYuNTk3IDExMi44ODMgMTA2Ljc0MyAxMTMuMTA2QzEwNi44ODkgMTEzLjMzIDEwNy4wNzYgMTEzLjUwNSAxMDcuMzA0IDExMy42MzNDMTA3LjUzMiAxMTMuNzYgMTA3LjgwMyAxMTMuODI0IDEwOC4xMTcgMTEzLjgyNEMxMDguNDIzIDExMy44MjQgMTA4LjY4NyAxMTMuNzYgMTA4LjkxIDExMy42MzNDMTA5LjEzOCAxMTMuNTA1IDEwOS4zMjUgMTEzLjMzIDEwOS40NzEgMTEzLjEwNkMxMDkuNjE3IDExMi44ODMgMTA5LjcyNCAxMTIuNjIzIDEwOS43OTIgMTEyLjMyN0MxMDkuODY1IDExMi4wMzEgMTA5LjkwMSAxMTEuNzE2IDEwOS45MDEgMTExLjM4NFYxMTEuMjI3QzEwOS45MDEgMTEwLjg5OCAxMDkuODY1IDExMC41ODkgMTA5Ljc5MiAxMTAuMjk3QzEwOS43MjQgMTEwLjAwMSAxMDkuNjE0IDEwOS43MzkgMTA5LjQ2NCAxMDkuNTExQzEwOS4zMTggMTA5LjI4MyAxMDkuMTMxIDEwOS4xMDUgMTA4LjkwMyAxMDguOTc4QzEwOC42OCAxMDguODQ1IDEwOC40MTMgMTA4Ljc3OSAxMDguMTA0IDEwOC43NzlDMTA3Ljc5NCAxMDguNzc5IDEwNy41MjUgMTA4Ljg0NSAxMDcuMjk3IDEwOC45NzhDMTA3LjA3NCAxMDkuMTA1IDEwNi44ODkgMTA5LjI4MyAxMDYuNzQzIDEwOS41MTFDMTA2LjU5NyAxMDkuNzM5IDEwNi40OSAxMTAuMDAxIDEwNi40MjIgMTEwLjI5N0MxMDYuMzU0IDExMC41ODkgMTA2LjMxOSAxMTAuODk4IDEwNi4zMTkgMTExLjIyN1pNMTE0LjgzNCAxMDkuMDEyVjExNUgxMTMuMTg3VjEwNy42MDRIMTE0Ljc1OUwxMTQuODM0IDEwOS4wMTJaTTExNy4wOTcgMTA3LjU1NkwxMTcuMDgzIDEwOS4wODdDMTE2Ljk4MyAxMDkuMDY5IDExNi44NzMgMTA5LjA1NSAxMTYuNzU1IDEwOS4wNDZDMTE2LjY0MSAxMDkuMDM3IDExNi41MjcgMTA5LjAzMiAxMTYuNDEzIDEwOS4wMzJDMTE2LjEzMSAxMDkuMDMyIDExNS44ODIgMTA5LjA3MyAxMTUuNjY4IDEwOS4xNTVDMTE1LjQ1NCAxMDkuMjMzIDExNS4yNzQgMTA5LjM0NyAxMTUuMTI4IDEwOS40OTdDMTE0Ljk4NyAxMDkuNjQzIDExNC44NzcgMTA5LjgyMSAxMTQuOCAxMTAuMDNDMTE0LjcyMiAxMTAuMjQgMTE0LjY3NyAxMTAuNDc1IDExNC42NjMgMTEwLjczNEwxMTQuMjg3IDExMC43NjJDMTE0LjI4NyAxMTAuMjk3IDExNC4zMzMgMTA5Ljg2NiAxMTQuNDI0IDEwOS40N0MxMTQuNTE1IDEwOS4wNzMgMTE0LjY1MiAxMDguNzI1IDExNC44MzQgMTA4LjQyNEMxMTUuMDIxIDEwOC4xMjMgMTE1LjI1MyAxMDcuODg4IDExNS41MzEgMTA3LjcyQzExNS44MTQgMTA3LjU1MSAxMTYuMTQgMTA3LjQ2NyAxMTYuNTA5IDEwNy40NjdDMTE2LjYwOSAxMDcuNDY3IDExNi43MTYgMTA3LjQ3NiAxMTYuODMgMTA3LjQ5NEMxMTYuOTQ5IDEwNy41MTIgMTE3LjAzNyAxMDcuNTMzIDExNy4wOTcgMTA3LjU1NlpNMTIwLjAxMyAxMDkuMTgzVjExNUgxMTguMzY1VjEwNy42MDRIMTE5LjkxN0wxMjAuMDEzIDEwOS4xODNaTTExOS43MTkgMTExLjAyOEwxMTkuMTg2IDExMS4wMjFDMTE5LjE5IDExMC40OTcgMTE5LjI2MyAxMTAuMDE3IDExOS40MDQgMTA5LjU3OUMxMTkuNTUgMTA5LjE0MiAxMTkuNzUxIDEwOC43NjYgMTIwLjAwNiAxMDguNDUxQzEyMC4yNjYgMTA4LjEzNyAxMjAuNTc2IDEwNy44OTUgMTIwLjkzNiAxMDcuNzI3QzEyMS4yOTYgMTA3LjU1MyAxMjEuNjk3IDEwNy40NjcgMTIyLjEzOSAxMDcuNDY3QzEyMi40OTQgMTA3LjQ2NyAxMjIuODE1IDEwNy41MTcgMTIzLjEwMyAxMDcuNjE3QzEyMy4zOTQgMTA3LjcxMyAxMjMuNjQzIDEwNy44NyAxMjMuODQ4IDEwOC4wODlDMTI0LjA1NyAxMDguMzA4IDEyNC4yMTcgMTA4LjU5MiAxMjQuMzI2IDEwOC45NDNDMTI0LjQzNiAxMDkuMjkgMTI0LjQ5IDEwOS43MTYgMTI0LjQ5IDExMC4yMjJWMTE1SDEyMi44MzZWMTEwLjIxNUMxMjIuODM2IDEwOS44NTkgMTIyLjc4NCAxMDkuNTc5IDEyMi42NzkgMTA5LjM3NEMxMjIuNTc4IDEwOS4xNjQgMTIyLjQzIDEwOS4wMTYgMTIyLjIzNCAxMDguOTNDMTIyLjA0MyAxMDguODM5IDEyMS44MDQgMTA4Ljc5MyAxMjEuNTE3IDEwOC43OTNDMTIxLjIzNCAxMDguNzkzIDEyMC45ODEgMTA4Ljg1MiAxMjAuNzU4IDEwOC45NzFDMTIwLjUzNSAxMDkuMDg5IDEyMC4zNDUgMTA5LjI1MSAxMjAuMTkgMTA5LjQ1NkMxMjAuMDQgMTA5LjY2MSAxMTkuOTI0IDEwOS44OTggMTE5Ljg0MiAxMTAuMTY3QzExOS43NiAxMTAuNDM2IDExOS43MTkgMTEwLjcyMyAxMTkuNzE5IDExMS4wMjhaTTEyOS42ODMgMTE1LjEzN0MxMjkuMTM2IDExNS4xMzcgMTI4LjY0MSAxMTUuMDQ4IDEyOC4xOTkgMTE0Ljg3QzEyNy43NjIgMTE0LjY4OCAxMjcuMzg4IDExNC40MzUgMTI3LjA3OCAxMTQuMTExQzEyNi43NzMgMTEzLjc4OCAxMjYuNTM4IDExMy40MDcgMTI2LjM3NCAxMTIuOTdDMTI2LjIxIDExMi41MzIgMTI2LjEyOCAxMTIuMDYxIDEyNi4xMjggMTExLjU1NVYxMTEuMjgxQzEyNi4xMjggMTEwLjcwMiAxMjYuMjEyIDExMC4xNzggMTI2LjM4MSAxMDkuNzA5QzEyNi41NDkgMTA5LjI0IDEyNi43ODQgMTA4LjgzOSAxMjcuMDg1IDEwOC41MDZDMTI3LjM4NiAxMDguMTY5IDEyNy43NDEgMTA3LjkxMSAxMjguMTUxIDEwNy43MzNDMTI4LjU2MiAxMDcuNTU2IDEyOS4wMDYgMTA3LjQ2NyAxMjkuNDg0IDEwNy40NjdDMTMwLjAxMyAxMDcuNDY3IDEzMC40NzYgMTA3LjU1NiAxMzAuODcyIDEwNy43MzNDMTMxLjI2OSAxMDcuOTExIDEzMS41OTcgMTA4LjE2MiAxMzEuODU2IDEwOC40ODVDMTMyLjEyMSAxMDguODA0IDEzMi4zMTcgMTA5LjE4NSAxMzIuNDQ0IDEwOS42MjdDMTMyLjU3NiAxMTAuMDY5IDEzMi42NDMgMTEwLjU1NyAxMzIuNjQzIDExMS4wOVYxMTEuNzk0SDEyNi45MjhWMTEwLjYxMUgxMzEuMDE2VjExMC40ODFDMTMxLjAwNyAxMTAuMTg1IDEzMC45NDcgMTA5LjkwNyAxMzAuODM4IDEwOS42NDdDMTMwLjczMyAxMDkuMzg4IDEzMC41NzEgMTA5LjE3OCAxMzAuMzUzIDEwOS4wMTlDMTMwLjEzNCAxMDguODU5IDEyOS44NDIgMTA4Ljc3OSAxMjkuNDc4IDEwOC43NzlDMTI5LjIwNCAxMDguNzc5IDEyOC45NiAxMDguODM5IDEyOC43NDYgMTA4Ljk1N0MxMjguNTM2IDEwOS4wNzEgMTI4LjM2MSAxMDkuMjM3IDEyOC4yMiAxMDkuNDU2QzEyOC4wNzggMTA5LjY3NSAxMjcuOTY5IDEwOS45MzkgMTI3Ljg5MiAxMTAuMjQ5QzEyNy44MTkgMTEwLjU1NCAxMjcuNzgyIDExMC44OTggMTI3Ljc4MiAxMTEuMjgxVjExMS41NTVDMTI3Ljc4MiAxMTEuODc4IDEyNy44MjYgMTEyLjE3OSAxMjcuOTEyIDExMi40NTdDMTI4LjAwMyAxMTIuNzMgMTI4LjEzNSAxMTIuOTcgMTI4LjMwOSAxMTMuMTc1QzEyOC40ODIgMTEzLjM4IDEyOC42OTEgMTEzLjU0MiAxMjguOTM4IDExMy42NkMxMjkuMTg0IDExMy43NzQgMTI5LjQ2NCAxMTMuODMxIDEyOS43NzggMTEzLjgzMUMxMzAuMTc1IDExMy44MzEgMTMwLjUyOCAxMTMuNzUxIDEzMC44MzggMTEzLjU5MkMxMzEuMTQ4IDExMy40MzIgMTMxLjQxNyAxMTMuMjA3IDEzMS42NDUgMTEyLjkxNUwxMzIuNTEzIDExMy43NTZDMTMyLjM1MyAxMTMuOTg4IDEzMi4xNDYgMTE0LjIxMiAxMzEuODkxIDExNC40MjZDMTMxLjYzNSAxMTQuNjM1IDEzMS4zMjMgMTE0LjgwNiAxMzAuOTU0IDExNC45MzhDMTMwLjU5IDExNS4wNzEgMTMwLjE2NiAxMTUuMTM3IDEyOS42ODMgMTE1LjEzN1pNMTM1LjgzMiAxMDkuMDEyVjExNUgxMzQuMTg1VjEwNy42MDRIMTM1Ljc1N0wxMzUuODMyIDEwOS4wMTJaTTEzOC4wOTUgMTA3LjU1NkwxMzguMDgxIDEwOS4wODdDMTM3Ljk4MSAxMDkuMDY5IDEzNy44NzEgMTA5LjA1NSAxMzcuNzUzIDEwOS4wNDZDMTM3LjYzOSAxMDkuMDM3IDEzNy41MjUgMTA5LjAzMiAxMzcuNDExIDEwOS4wMzJDMTM3LjEyOSAxMDkuMDMyIDEzNi44OCAxMDkuMDczIDEzNi42NjYgMTA5LjE1NUMxMzYuNDUyIDEwOS4yMzMgMTM2LjI3MiAxMDkuMzQ3IDEzNi4xMjYgMTA5LjQ5N0MxMzUuOTg1IDEwOS42NDMgMTM1Ljg3NSAxMDkuODIxIDEzNS43OTggMTEwLjAzQzEzNS43MiAxMTAuMjQgMTM1LjY3NSAxMTAuNDc1IDEzNS42NjEgMTEwLjczNEwxMzUuMjg1IDExMC43NjJDMTM1LjI4NSAxMTAuMjk3IDEzNS4zMzEgMTA5Ljg2NiAxMzUuNDIyIDEwOS40N0MxMzUuNTEzIDEwOS4wNzMgMTM1LjY1IDEwOC43MjUgMTM1LjgzMiAxMDguNDI0QzEzNi4wMTkgMTA4LjEyMyAxMzYuMjUxIDEwNy44ODggMTM2LjUyOSAxMDcuNzJDMTM2LjgxMiAxMDcuNTUxIDEzNy4xMzggMTA3LjQ2NyAxMzcuNTA3IDEwNy40NjdDMTM3LjYwNyAxMDcuNDY3IDEzNy43MTQgMTA3LjQ3NiAxMzcuODI4IDEwNy40OTRDMTM3Ljk0NyAxMDcuNTEyIDEzOC4wMzUgMTA3LjUzMyAxMzguMDk1IDEwNy41NTZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjMuNjY5OSAxMzEuNVYxMzEuOTY5QzIzLjY2OTkgMTMyLjYxMyAyMy41ODU5IDEzMy4xOTEgMjMuNDE4IDEzMy43MDNDMjMuMjUgMTM0LjIxNSAyMy4wMDk4IDEzNC42NSAyMi42OTczIDEzNS4wMUMyMi4zODg3IDEzNS4zNjkgMjIuMDE3NiAxMzUuNjQ1IDIxLjU4NCAxMzUuODM2QzIxLjE1MDQgMTM2LjAyMyAyMC42Njk5IDEzNi4xMTcgMjAuMTQyNiAxMzYuMTE3QzE5LjYxOTEgMTM2LjExNyAxOS4xNDA2IDEzNi4wMjMgMTguNzA3IDEzNS44MzZDMTguMjc3MyAxMzUuNjQ1IDE3LjkwNDMgMTM1LjM2OSAxNy41ODc5IDEzNS4wMUMxNy4yNzE1IDEzNC42NSAxNy4wMjU0IDEzNC4yMTUgMTYuODQ5NiAxMzMuNzAzQzE2LjY3NzcgMTMzLjE5MSAxNi41OTE4IDEzMi42MTMgMTYuNTkxOCAxMzEuOTY5VjEzMS41QzE2LjU5MTggMTMwLjg1NSAxNi42Nzc3IDEzMC4yNzkgMTYuODQ5NiAxMjkuNzcxQzE3LjAyMTUgMTI5LjI2IDE3LjI2MzcgMTI4LjgyNCAxNy41NzYyIDEyOC40NjVDMTcuODkyNiAxMjguMTAyIDE4LjI2NTYgMTI3LjgyNiAxOC42OTUzIDEyNy42MzlDMTkuMTI4OSAxMjcuNDQ3IDE5LjYwNzQgMTI3LjM1MiAyMC4xMzA5IDEyNy4zNTJDMjAuNjU4MiAxMjcuMzUyIDIxLjEzODcgMTI3LjQ0NyAyMS41NzIzIDEyNy42MzlDMjIuMDA1OSAxMjcuODI2IDIyLjM3ODkgMTI4LjEwMiAyMi42OTE0IDEyOC40NjVDMjMuMDAzOSAxMjguODI0IDIzLjI0NDEgMTI5LjI2IDIzLjQxMjEgMTI5Ljc3MUMyMy41ODQgMTMwLjI3OSAyMy42Njk5IDEzMC44NTUgMjMuNjY5OSAxMzEuNVpNMjIuMTk5MiAxMzEuOTY5VjEzMS40ODhDMjIuMTk5MiAxMzEuMDEyIDIyLjE1MjMgMTMwLjU5MiAyMi4wNTg2IDEzMC4yMjlDMjEuOTY4OCAxMjkuODYxIDIxLjgzNCAxMjkuNTU1IDIxLjY1NDMgMTI5LjMwOUMyMS40Nzg1IDEyOS4wNTkgMjEuMjYxNyAxMjguODcxIDIxLjAwMzkgMTI4Ljc0NkMyMC43NDYxIDEyOC42MTcgMjAuNDU1MSAxMjguNTUzIDIwLjEzMDkgMTI4LjU1M0MxOS44MDY2IDEyOC41NTMgMTkuNTE3NiAxMjguNjE3IDE5LjI2MzcgMTI4Ljc0NkMxOS4wMDk4IDEyOC44NzEgMTguNzkzIDEyOS4wNTkgMTguNjEzMyAxMjkuMzA5QzE4LjQzNzUgMTI5LjU1NSAxOC4zMDI3IDEyOS44NjEgMTguMjA5IDEzMC4yMjlDMTguMTE1MiAxMzAuNTkyIDE4LjA2ODQgMTMxLjAxMiAxOC4wNjg0IDEzMS40ODhWMTMxLjk2OUMxOC4wNjg0IDEzMi40NDUgMTguMTE1MiAxMzIuODY3IDE4LjIwOSAxMzMuMjM0QzE4LjMwMjcgMTMzLjYwMiAxOC40Mzk1IDEzMy45MTIgMTguNjE5MSAxMzQuMTY2QzE4LjgwMjcgMTM0LjQxNiAxOS4wMjE1IDEzNC42MDUgMTkuMjc1NCAxMzQuNzM0QzE5LjUyOTMgMTM0Ljg1OSAxOS44MTg0IDEzNC45MjIgMjAuMTQyNiAxMzQuOTIyQzIwLjQ3MDcgMTM0LjkyMiAyMC43NjE3IDEzNC44NTkgMjEuMDE1NiAxMzQuNzM0QzIxLjI2OTUgMTM0LjYwNSAyMS40ODQ0IDEzNC40MTYgMjEuNjYwMiAxMzQuMTY2QzIxLjgzNTkgMTMzLjkxMiAyMS45Njg4IDEzMy42MDIgMjIuMDU4NiAxMzMuMjM0QzIyLjE1MjMgMTMyLjg2NyAyMi4xOTkyIDEzMi40NDUgMjIuMTk5MiAxMzEuOTY5Wk0yOC44ODA5IDEzMi44MThIMjYuNjYwMlYxMzEuNjUySDI4Ljg4MDlDMjkuMjY3NiAxMzEuNjUyIDI5LjU4MDEgMTMxLjU5IDI5LjgxODQgMTMxLjQ2NUMzMC4wNTY2IDEzMS4zNCAzMC4yMzA1IDEzMS4xNjggMzAuMzM5OCAxMzAuOTQ5QzMwLjQ1MzEgMTMwLjcyNyAzMC41MDk4IDEzMC40NzMgMzAuNTA5OCAxMzAuMTg4QzMwLjUwOTggMTI5LjkxOCAzMC40NTMxIDEyOS42NjYgMzAuMzM5OCAxMjkuNDMyQzMwLjIzMDUgMTI5LjE5MyAzMC4wNTY2IDEyOS4wMDIgMjkuODE4NCAxMjguODU3QzI5LjU4MDEgMTI4LjcxMyAyOS4yNjc2IDEyOC42NDEgMjguODgwOSAxMjguNjQxSDI3LjExMTNWMTM2SDI1LjY0MDZWMTI3LjQ2OUgyOC44ODA5QzI5LjU0MSAxMjcuNDY5IDMwLjEwMTYgMTI3LjU4NiAzMC41NjI1IDEyNy44MkMzMS4wMjczIDEyOC4wNTEgMzEuMzgwOSAxMjguMzcxIDMxLjYyMyAxMjguNzgxQzMxLjg2NTIgMTI5LjE4OCAzMS45ODYzIDEyOS42NTIgMzEuOTg2MyAxMzAuMTc2QzMxLjk4NjMgMTMwLjcyNyAzMS44NjUyIDEzMS4xOTkgMzEuNjIzIDEzMS41OTRDMzEuMzgwOSAxMzEuOTg4IDMxLjAyNzMgMTMyLjI5MSAzMC41NjI1IDEzMi41MDJDMzAuMTAxNiAxMzIuNzEzIDI5LjU0MSAxMzIuODE4IDI4Ljg4MDkgMTMyLjgxOFpNMzkuMzg4NyAxMzQuODM0VjEzNkgzNC44NTk0VjEzNC44MzRIMzkuMzg4N1pNMzUuMjc1NCAxMjcuNDY5VjEzNkgzMy44MDQ3VjEyNy40NjlIMzUuMjc1NFpNMzguNzk2OSAxMzEuMDMxVjEzMi4xOEgzNC44NTk0VjEzMS4wMzFIMzguNzk2OVpNMzkuMzU5NCAxMjcuNDY5VjEyOC42NDFIMzQuODU5NFYxMjcuNDY5SDM5LjM1OTRaTTQ3Ljg1NzQgMTI3LjQ2OVYxMzZINDYuMzg2N0w0Mi41NjA1IDEyOS44ODlWMTM2SDQxLjA4OThWMTI3LjQ2OUg0Mi41NjA1TDQ2LjM5ODQgMTMzLjU5MlYxMjcuNDY5SDQ3Ljg1NzRaTTU1LjY5MzQgMTM0LjgzNFYxMzZINTEuMTY0MVYxMzQuODM0SDU1LjY5MzRaTTUxLjU4MDEgMTI3LjQ2OVYxMzZINTAuMTA5NFYxMjcuNDY5SDUxLjU4MDFaTTU1LjEwMTYgMTMxLjAzMVYxMzIuMThINTEuMTY0MVYxMzEuMDMxSDU1LjEwMTZaTTU1LjY2NDEgMTI3LjQ2OVYxMjguNjQxSDUxLjE2NDFWMTI3LjQ2OUg1NS42NjQxWk01OS44NzMgMTM2SDU4LjA1MDhMNTguMDYyNSAxMzQuODM0SDU5Ljg3M0M2MC40MDA0IDEzNC44MzQgNjAuODQxOCAxMzQuNzE5IDYxLjE5NzMgMTM0LjQ4OEM2MS41NTY2IDEzNC4yNTggNjEuODI2MiAxMzMuOTI4IDYyLjAwNTkgMTMzLjQ5OEM2Mi4xODk1IDEzMy4wNjggNjIuMjgxMiAxMzIuNTU3IDYyLjI4MTIgMTMxLjk2M1YxMzEuNUM2Mi4yODEyIDEzMS4wMzkgNjIuMjI4NSAxMzAuNjMxIDYyLjEyMyAxMzAuMjc1QzYyLjAyMTUgMTI5LjkyIDYxLjg2OTEgMTI5LjYyMSA2MS42NjYgMTI5LjM3OUM2MS40NjY4IDEyOS4xMzcgNjEuMjIwNyAxMjguOTUzIDYwLjkyNzcgMTI4LjgyOEM2MC42Mzg3IDEyOC43MDMgNjAuMzA0NyAxMjguNjQxIDU5LjkyNTggMTI4LjY0MUg1OC4wMTU2VjEyNy40NjlINTkuOTI1OEM2MC40OTIyIDEyNy40NjkgNjEuMDA5OCAxMjcuNTY0IDYxLjQ3ODUgMTI3Ljc1NkM2MS45NDczIDEyNy45NDMgNjIuMzUxNiAxMjguMjE1IDYyLjY5MTQgMTI4LjU3QzYzLjAzNTIgMTI4LjkyNiA2My4yOTg4IDEyOS4zNTIgNjMuNDgyNCAxMjkuODQ4QzYzLjY2NiAxMzAuMzQ0IDYzLjc1NzggMTMwLjg5OCA2My43NTc4IDEzMS41MTJWMTMxLjk2M0M2My43NTc4IDEzMi41NzYgNjMuNjY2IDEzMy4xMzEgNjMuNDgyNCAxMzMuNjI3QzYzLjI5ODggMTM0LjEyMyA2My4wMzUyIDEzNC41NDkgNjIuNjkxNCAxMzQuOTA0QzYyLjM0NzcgMTM1LjI1NiA2MS45Mzc1IDEzNS41MjcgNjEuNDYwOSAxMzUuNzE5QzYwLjk4ODMgMTM1LjkwNiA2MC40NTkgMTM2IDU5Ljg3MyAxMzZaTTU4Ljg2NTIgMTI3LjQ2OVYxMzZINTcuMzk0NVYxMjcuNDY5SDU4Ljg2NTJaIiBmaWxsPSJ3aGl0ZSIgZmlsbC1vcGFjaXR5PSIwLjgiLz4KPC9zdmc+Cg==", + "description": "Displays current status.", + "descriptor": { + "type": "rpc", + "sizeX": 2, + "sizeY": 2, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '180px',\n previewHeight: '180px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-status-widget-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-status-widget-basic-config", + "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Status widget\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"actions\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"1.6\"},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"titleIcon\":\"mdi:lightbulb-outline\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"configMode\":\"basic\",\"targetDevice\":null,\"titleColor\":null,\"borderRadius\":null}" + }, + "tags": [ + "status", + "status widget" + ] +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index cea449af31..5f1de4e3e3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -647,7 +647,7 @@ public class TelemetryController extends BaseController { Map> telemetryRequest; JsonElement telemetryJson; try { - telemetryJson = new JsonParser().parse(requestBody); + telemetryJson = JsonParser.parseString(requestBody); } catch (Exception e) { return getImmediateDeferredResult("Unable to parse timeseries payload: Invalid JSON body!", HttpStatus.BAD_REQUEST); } diff --git a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java index 7f1df3fdd1..5cae75ef0d 100644 --- a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java @@ -147,7 +147,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest void checkDiffBetweenLatestTsForDevicesAndAsset(List devices, Asset asset) throws ExecutionException, InterruptedException, TimeoutException { TsKvEntry assetTsKvEntry = getTsKvLatest(asset.getId(), GENERIC_CUMULATIVE_OBJ); Assert.assertTrue(assetTsKvEntry.getJsonValue().isPresent()); - JsonObject assetJsonObject = new JsonParser().parse(assetTsKvEntry.getJsonValue().get()).getAsJsonObject(); + JsonObject assetJsonObject = JsonParser.parseString(assetTsKvEntry.getJsonValue().get()).getAsJsonObject(); for (Device device : devices) { Long assetValue = assetJsonObject.get(device.getName()).getAsLong(); TsKvEntry deviceLatest = getTsKvLatest(device.getId(), TOTALIZER); @@ -182,7 +182,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest JsonObject getJsonObject(String key, long value, Optional tsKvEntryOpt) { JsonObject jsonObject = new JsonObject(); if (tsKvEntryOpt.isPresent()) { - jsonObject = new JsonParser().parse(tsKvEntryOpt.get()).getAsJsonObject(); + jsonObject = JsonParser.parseString(tsKvEntryOpt.get()).getAsJsonObject(); } jsonObject.addProperty(key, value); return jsonObject; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java index 54a21da51e..00a5510788 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -67,6 +67,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY; @@ -147,9 +148,9 @@ public class LwM2MTestClient { initializer.setClassForObject(SECURITY, Security.class); initializer.setInstancesForObject(SECURITY, instances); // SERVER - Server lwm2mServer = new Server(shortServerId, 300); + Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60)); lwm2mServer.setId(serverId); - Server serverBs = new Server(shortServerIdBs0, 300); + Server serverBs = new Server(shortServerIdBs0, TimeUnit.MINUTES.toSeconds(60)); serverBs.setId(serverIdBs); instances = new LwM2mInstanceEnabler[]{serverBs, lwm2mServer}; initializer.setClassForObject(SERVER, Server.class); @@ -163,7 +164,7 @@ public class LwM2MTestClient { // SECURITY initializer.setInstancesForObject(SECURITY, security); // SERVER - Server lwm2mServer = new Server(shortServerId, 300); + Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60)); lwm2mServer.setId(serverId); initializer.setInstancesForObject(SERVER, lwm2mServer ); } diff --git a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java index 42da1d740f..72ae3a0871 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java @@ -66,7 +66,6 @@ import java.util.stream.Collectors; public class JsonConverter { private static final Gson GSON = new Gson(); - private static final JsonParser JSON_PARSER = new JsonParser(); private static final String CAN_T_PARSE_VALUE = "Can't parse value: "; private static final String DEVICE_PROPERTY = "device"; @@ -111,7 +110,7 @@ public class JsonConverter { public static ClaimDeviceMsg convertToClaimDeviceProto(DeviceId deviceId, String json) { long durationMs = 0L; if (json != null && !json.isEmpty()) { - return convertToClaimDeviceProto(deviceId, JSON_PARSER.parse(json)); + return convertToClaimDeviceProto(deviceId, JsonParser.parseString(json)); } return buildClaimDeviceMsg(deviceId, DataConstants.DEFAULT_SECRET_KEY, durationMs); } @@ -160,7 +159,7 @@ public class JsonConverter { result.addProperty("id", msg.getRequestId()); } result.addProperty("method", msg.getMethodName()); - result.add("params", JSON_PARSER.parse(msg.getParams())); + result.add("params", JsonParser.parseString(msg.getParams())); return result; } @@ -367,7 +366,7 @@ public class JsonConverter { json.addProperty(name, entry.getLongV()); break; case JSON_V: - json.add(name, JSON_PARSER.parse(entry.getJsonV())); + json.add(name, JsonParser.parseString(entry.getJsonV())); break; } } @@ -388,7 +387,7 @@ public class JsonConverter { result.add(de.getKv().getKey(), new JsonPrimitive(de.getKv().getStringV())); break; case JSON_V: - result.add(de.getKv().getKey(), JSON_PARSER.parse(de.getKv().getJsonV())); + result.add(de.getKv().getKey(), JsonParser.parseString(de.getKv().getJsonV())); break; default: throw new IllegalArgumentException("Unsupported data type: " + de.getKv().getType()); @@ -412,7 +411,7 @@ public class JsonConverter { result.add(de.getKey(), new JsonPrimitive(de.getStrValue().get())); break; case JSON: - result.add(de.getKey(), JSON_PARSER.parse(de.getJsonValue().get())); + result.add(de.getKey(), JsonParser.parseString(de.getJsonValue().get())); break; default: throw new IllegalArgumentException("Unsupported data type: " + de.getDataType()); @@ -422,7 +421,7 @@ public class JsonConverter { public static JsonElement toJson(TransportProtos.ToServerRpcResponseMsg msg) { if (StringUtils.isEmpty(msg.getError())) { - return JSON_PARSER.parse(msg.getPayload()); + return JsonParser.parseString(msg.getPayload()); } else { JsonObject errorMsg = new JsonObject(); errorMsg.addProperty("error", msg.getError()); @@ -456,7 +455,7 @@ public class JsonConverter { result.addProperty("credentialsValue", payload.getCredentialsValue()); break; case MQTT_BASIC: - result.add("credentialsValue", JSON_PARSER.parse(payload.getCredentialsValue()).getAsJsonObject()); + result.add("credentialsValue", JsonParser.parseString(payload.getCredentialsValue()).getAsJsonObject()); break; case LWM2M_CREDENTIALS: break; @@ -572,7 +571,7 @@ public class JsonConverter { } public static JsonElement parse(String json) { - return JSON_PARSER.parse(json); + return JsonParser.parseString(json); } public static T parse(String json, Class clazz) { @@ -600,7 +599,7 @@ public class JsonConverter { } public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(String json) { - JsonElement jsonElement = JSON_PARSER.parse(json); + JsonElement jsonElement = JsonParser.parseString(json); if (jsonElement.isJsonObject()) { return buildProvisionRequestMsg(jsonElement.getAsJsonObject()); } else { diff --git a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/ProtoConverter.java b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/ProtoConverter.java index b90b5f054f..2aa73928e5 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/ProtoConverter.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/ProtoConverter.java @@ -39,7 +39,6 @@ import java.util.List; public class ProtoConverter { public static final Gson GSON = new Gson(); - public static final JsonParser JSON_PARSER = new JsonParser(); public static TransportProtos.PostTelemetryMsg convertToTelemetryProto(byte[] payload) throws InvalidProtocolBufferException, IllegalArgumentException { TransportProtos.TsKvListProto protoPayload = TransportProtos.TsKvListProto.parseFrom(payload); @@ -165,7 +164,7 @@ public class ProtoConverter { break; case JSON_V: try { - JSON_PARSER.parse(keyValueProto.getJsonV()); + JsonParser.parseString(keyValueProto.getJsonV()); } catch (Exception e) { throw new IllegalArgumentException("Can't parse value: " + keyValueProto.getJsonV() + " for key: " + key + "!"); } @@ -184,7 +183,7 @@ public class ProtoConverter { rpcRequestJson.addProperty("requestId", toDeviceRpcRequestMsg.getRequestId()); String params = toDeviceRpcRequestMsg.getParams(); try { - JsonElement paramsElement = JSON_PARSER.parse(params); + JsonElement paramsElement = JsonParser.parseString(params); rpcRequestJson.add("params", paramsElement); DynamicMessage dynamicRpcRequest = DynamicProtoUtils.jsonToDynamicMessage(rpcRequestDynamicMessageBuilder, GSON.toJson(rpcRequestJson)); return dynamicRpcRequest.toByteArray(); diff --git a/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java b/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java index 773ebbea3d..eb11a11f5c 100644 --- a/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java +++ b/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java @@ -26,8 +26,6 @@ import java.util.ArrayList; public class JsonConverterTest { - private final JsonParser JSON_PARSER = new JsonParser(); - @BeforeEach public void before() { JsonConverter.setTypeCastEnabled(true); @@ -35,55 +33,55 @@ public class JsonConverterTest { @Test public void testParseBigDecimalAsLong() { - var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 1E+1}"), 0L); + var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 1E+1}"), 0L); Assert.assertEquals(10L, result.get(0L).get(0).getLongValue().get().longValue()); } @Test public void testParseBigDecimalAsDouble() { - var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 101E-1}"), 0L); + var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 101E-1}"), 0L); Assert.assertEquals(10.1, result.get(0L).get(0).getDoubleValue().get(), 0.0); } @Test public void testParseAttributesBigDecimalAsLong() { - var result = new ArrayList<>(JsonConverter.convertToAttributes(JSON_PARSER.parse("{\"meterReadingDelta\": 1E1}"))); + var result = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString("{\"meterReadingDelta\": 1E1}"))); Assert.assertEquals(10L, result.get(0).getLongValue().get().longValue()); } @Test public void testParseAsDoubleWithZero() { - var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 42.0}"), 0L); + var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 42.0}"), 0L); Assert.assertEquals(42.0, result.get(0L).get(0).getDoubleValue().get(), 0.0); } @Test public void testParseAsDouble() { - var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 1.1}"), 0L); + var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 1.1}"), 0L); Assert.assertEquals(1.1, result.get(0L).get(0).getDoubleValue().get(), 0.0); } @Test public void testParseAsLong() { - var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 11}"), 0L); + var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 11}"), 0L); Assert.assertEquals(11L, result.get(0L).get(0).getLongValue().get().longValue()); } @Test public void testParseBigDecimalAsStringOutOfLongRange() { - var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L); + var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L); Assert.assertEquals("99701010061400066000", result.get(0L).get(0).getStrValue().get()); } @Test public void testParseBigDecimalAsStringOutOfLongRange2() { - var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 99701010061400066001}"), 0L); + var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 99701010061400066001}"), 0L); Assert.assertEquals("99701010061400066001", result.get(0L).get(0).getStrValue().get()); } @Test public void testParseBigDecimalAsStringOutOfLongRange3() { - var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 1E19}"), 0L); + var result = JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 1E19}"), 0L); Assert.assertEquals("10000000000000000000", result.get(0L).get(0).getStrValue().get()); } @@ -91,7 +89,7 @@ public class JsonConverterTest { public void testParseBigDecimalOutOfLongRangeWithoutParsing() { JsonConverter.setTypeCastEnabled(false); Assertions.assertThrows(JsonSyntaxException.class, () -> { - JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 89701010051400054084}"), 0L); + JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 89701010051400054084}"), 0L); }); } @@ -99,7 +97,7 @@ public class JsonConverterTest { public void testParseBigDecimalOutOfLongRangeWithoutParsing2() { JsonConverter.setTypeCastEnabled(false); Assertions.assertThrows(JsonSyntaxException.class, () -> { - JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L); + JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L); }); } } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java index e7100cac30..50dadd8e87 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java @@ -45,7 +45,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, Request inbound, Descriptors.Descriptor telemetryMsgDescriptor) throws AdaptorException { String payload = validatePayload(sessionId, inbound, false); try { - return JsonConverter.convertToTelemetryProto(new JsonParser().parse(payload)); + return JsonConverter.convertToTelemetryProto(JsonParser.parseString(payload)); } catch (IllegalStateException | JsonSyntaxException ex) { throw new AdaptorException(ex); } @@ -55,7 +55,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { public TransportProtos.PostAttributeMsg convertToPostAttributes(UUID sessionId, Request inbound, Descriptors.Descriptor attributesMsgDescriptor) throws AdaptorException { String payload = validatePayload(sessionId, inbound, false); try { - return JsonConverter.convertToAttributesProto(new JsonParser().parse(payload)); + return JsonConverter.convertToAttributesProto(JsonParser.parseString(payload)); } catch (IllegalStateException | JsonSyntaxException ex) { throw new AdaptorException(ex); } @@ -70,7 +70,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound, Descriptors.Descriptor rpcResponseMsgDescriptor) throws AdaptorException { Optional requestId = CoapTransportResource.getRequestId(inbound); String payload = validatePayload(sessionId, inbound, false); - JsonObject response = new JsonParser().parse(payload).getAsJsonObject(); + JsonObject response = JsonParser.parseString(payload).getAsJsonObject(); return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.orElseThrow(() -> new AdaptorException("Request id is missing!"))) .setPayload(response.toString()).build(); } @@ -78,7 +78,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { @Override public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(UUID sessionId, Request inbound) throws AdaptorException { String payload = validatePayload(sessionId, inbound, false); - return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), 0); + return JsonConverter.convertToServerRpcRequest(JsonParser.parseString(payload), 0); } @Override diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java index f686dc63cf..189620afa1 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java @@ -45,7 +45,7 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor { public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, Request inbound, Descriptors.Descriptor telemetryMsgDescriptor) throws AdaptorException { ProtoConverter.validateDescriptor(telemetryMsgDescriptor); try { - return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), telemetryMsgDescriptor))); + return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), telemetryMsgDescriptor))); } catch (Exception e) { throw new AdaptorException(e); } @@ -55,7 +55,7 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor { public TransportProtos.PostAttributeMsg convertToPostAttributes(UUID sessionId, Request inbound, Descriptors.Descriptor attributesMsgDescriptor) throws AdaptorException { ProtoConverter.validateDescriptor(attributesMsgDescriptor); try { - return JsonConverter.convertToAttributesProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), attributesMsgDescriptor))); + return JsonConverter.convertToAttributesProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), attributesMsgDescriptor))); } catch (Exception e) { throw new AdaptorException(e); } @@ -74,7 +74,7 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor { } else { ProtoConverter.validateDescriptor(rpcResponseMsgDescriptor); try { - JsonElement response = new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), rpcResponseMsgDescriptor)); + JsonElement response = JsonParser.parseString(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), rpcResponseMsgDescriptor)); return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.orElseThrow(() -> new AdaptorException("Request id is missing!"))) .setPayload(response.toString()).build(); } catch (Exception e) { diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java index e9674d767e..58a5629dae 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java @@ -185,7 +185,7 @@ public class DeviceApiController implements TbTransportService { transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { TransportService transportService = transportContext.getTransportService(); - transportService.process(sessionInfo, JsonConverter.convertToAttributesProto(new JsonParser().parse(json)), + transportService.process(sessionInfo, JsonConverter.convertToAttributesProto(JsonParser.parseString(json)), new HttpOkCallback(responseWriter)); })); return responseWriter; @@ -205,7 +205,7 @@ public class DeviceApiController implements TbTransportService { transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { TransportService transportService = transportContext.getTransportService(); - transportService.process(sessionInfo, JsonConverter.convertToTelemetryProto(new JsonParser().parse(json)), + transportService.process(sessionInfo, JsonConverter.convertToTelemetryProto(JsonParser.parseString(json)), new HttpOkCallback(responseWriter)); })); return responseWriter; @@ -301,7 +301,7 @@ public class DeviceApiController implements TbTransportService { DeferredResult responseWriter = new DeferredResult(); transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { - JsonObject request = new JsonParser().parse(json).getAsJsonObject(); + JsonObject request = JsonParser.parseString(json).getAsJsonObject(); TransportService transportService = transportContext.getTransportService(); transportService.registerSyncSession(sessionInfo, new HttpSessionListener(responseWriter, transportContext.getTransportService(), sessionInfo), diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java index a25ed11d94..86df96f40e 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java @@ -50,7 +50,6 @@ import static org.thingsboard.server.gen.transport.TransportProtos.KeyValueType. public class LwM2mTransportServerHelper { private final LwM2mTransportContext context; - private final static JsonParser JSON_PARSER = new JsonParser(); public void sendParametersOnThingsboardAttribute(List result, SessionInfoProto sessionInfo) { PostAttributeMsg.Builder request = PostAttributeMsg.newBuilder(); @@ -231,7 +230,7 @@ public class LwM2mTransportServerHelper { return kv.getStringV(); case JSON_V: try { - return JSON_PARSER.parse(kv.getJsonV()); + return JsonParser.parseString(kv.getJsonV()); } catch (Exception e) { return null; } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index 436fb479e9..b52764187d 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -90,7 +90,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab private static final Logger LOG = LoggerFactory.getLogger(RedisRegistrationStore.class); // Redis key prefixes - private static final String REG_EP = "REG:EP:"; // (Endpoint => Registration) + public static final String REG_EP = "REG:EP:"; // (Endpoint => Registration) private static final String REG_EP_REGID_IDX = "EP:REGID:"; // secondary index key (Registration ID => Endpoint) private static final String REG_EP_ADDR_IDX = "EP:ADDR:"; // secondary index key (Socket Address => Endpoint) private static final String REG_EP_IDENTITY = "EP:IDENTITY:"; // secondary index key (Identity => Endpoint) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java index ff72850c45..f837931b19 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java @@ -15,17 +15,24 @@ */ package org.thingsboard.server.transport.lwm2m.server.store; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.SecurityMode; import org.eclipse.leshan.core.peer.OscoreIdentity; import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException; import org.eclipse.leshan.server.security.SecurityInfo; +import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.integration.redis.util.RedisLockRegistry; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.JavaSerDesUtil; import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MSecurityInfo; import java.util.concurrent.locks.Lock; +import static org.thingsboard.server.transport.lwm2m.server.store.TbLwM2mRedisRegistrationStore.REG_EP; + +@Slf4j public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore { private static final String SEC_EP = "SEC#EP#"; private static final String LOCK_EP = "LOCK#EP#"; @@ -49,11 +56,31 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore { if (data == null || data.length == 0) { return null; } else { - if (SecurityMode.NO_SEC.equals(((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(data)).getSecurityMode())) { + TbLwM2MSecurityInfo tbLwM2MSecurityInfo = JavaSerDesUtil.decode(data); + if (tbLwM2MSecurityInfo != null) { + if (SecurityMode.NO_SEC.equals(tbLwM2MSecurityInfo.getSecurityMode())){ + + // for tests: redis connect NoSec (securityInfo == null) + log.info("lwm2m redis securityStore (decode -ok). Endpoint: [{}], secMode: [NoSec] key: [{}], data [{}]", endpoint, SEC_EP, data); + + return SecurityInfo.newPreSharedKeyInfo(SecurityMode.NO_SEC.toString(), SecurityMode.NO_SEC.toString(), + SecurityMode.NO_SEC.toString().getBytes()); + } else { + return tbLwM2MSecurityInfo.getSecurityInfo(); + } + } else if (SecurityMode.NO_SEC.equals(getSecurityModeByRegistration (connection, endpoint))){ + + // for tests: redis connect NoSec (securityInfo == null) + log.info("lwm2m redis securityStore (decode - bad, registration - unsecure). Endpoint: [{}], secMode: [NoSec] key: [{}], data [{}]", endpoint, SEC_EP, data); + return SecurityInfo.newPreSharedKeyInfo(SecurityMode.NO_SEC.toString(), SecurityMode.NO_SEC.toString(), SecurityMode.NO_SEC.toString().getBytes()); } else { - return ((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(data)).getSecurityInfo(); + + // for tests: redis connect NoSec (securityInfo == null) + log.info("lwm2m redis securityStore (decode - bad, registration is not unsecure) - return null. Endpoint: [{}], key: [{}], data [{}]", endpoint, SEC_EP, data); + + return null; } } } finally { @@ -112,6 +139,11 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore { } byte[] previousData = connection.getSet((SEC_EP + tbSecurityInfo.getEndpoint()).getBytes(), tbSecurityInfoSerialized); + + // for tests: redis connect NoSec (securityInfo == null) + log.info("lwm2m redis connect. Endpoint: [{}], secMode: [{}] key: [{}], tbSecurityInfoSerialized [{}]", + tbSecurityInfo.getEndpoint(), tbSecurityInfo.getSecurityMode().name(), SEC_EP, tbSecurityInfoSerialized); + if (previousData != null && info != null) { String previousIdentity = ((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(previousData)).getSecurityInfo().getPskIdentity(); if (previousIdentity != null && !previousIdentity.equals(info.getPskIdentity())) { @@ -168,4 +200,17 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore { private String toLockKey(String endpoint) { return LOCK_EP + endpoint; } + + private SecurityMode getSecurityModeByRegistration (RedisConnection connection, String endpoint) { + try { + byte[] data = connection.get((REG_EP + endpoint).getBytes()); + JsonNode registrationNode = JacksonUtil.fromString(new String(data != null ? data : new byte[0]), JsonNode.class); + String typeModeStr = registrationNode.get("transportdata").get("identity").get("type").asText(); + return "unsecure".equals(typeModeStr) ? SecurityMode.NO_SEC : null; + } catch (Exception e) { + log.error("Redis: Failed get SecurityMode by Registration, endpoint: [{}]", endpoint, e); + return null; + } + + } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java index c8524ee6ce..90174616e6 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java @@ -59,7 +59,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); try { - return JsonConverter.convertToTelemetryProto(new JsonParser().parse(payload)); + return JsonConverter.convertToTelemetryProto(JsonParser.parseString(payload)); } catch (IllegalStateException | JsonSyntaxException ex) { log.debug("Failed to decode post telemetry request", ex); throw new AdaptorException(ex); @@ -70,7 +70,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { public TransportProtos.PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); try { - return JsonConverter.convertToAttributesProto(new JsonParser().parse(payload)); + return JsonConverter.convertToAttributesProto(JsonParser.parseString(payload)); } catch (IllegalStateException | JsonSyntaxException ex) { log.debug("Failed to decode post attributes request", ex); throw new AdaptorException(ex); @@ -162,7 +162,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { public static JsonElement validateJsonPayload(UUID sessionId, ByteBuf payloadData) throws AdaptorException { String payload = validatePayload(sessionId, payloadData, false); try { - return new JsonParser().parse(payload); + return JsonParser.parseString(payload); } catch (JsonSyntaxException ex) { log.debug("Payload is in incorrect format: {}", payload); throw new AdaptorException(ex); @@ -175,7 +175,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); result.setRequestId(getRequestId(topicName, topicBase)); String payload = inbound.payload().toString(UTF8); - JsonElement requestBody = new JsonParser().parse(payload); + JsonElement requestBody = JsonParser.parseString(payload); Set clientKeys = toStringSet(requestBody, "clientKeys"); Set sharedKeys = toStringSet(requestBody, "sharedKeys"); if (clientKeys != null) { @@ -208,7 +208,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); try { int requestId = getRequestId(topicName, topicBase); - return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), requestId); + return JsonConverter.convertToServerRpcRequest(JsonParser.parseString(payload), requestId); } catch (IllegalStateException | JsonSyntaxException ex) { log.debug("Failed to decode to server rpc request", ex); throw new AdaptorException(ex); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java index ad86096a29..25113dedca 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java @@ -50,7 +50,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { byte[] bytes = toBytes(inbound.payload()); Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor()); try { - return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor))); + return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor))); } catch (Exception e) { log.debug("Failed to decode post telemetry request", e); throw new AdaptorException(e); @@ -63,7 +63,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { byte[] bytes = toBytes(inbound.payload()); Descriptors.Descriptor attributesDynamicMessageDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getAttributesDynamicMessageDescriptor()); try { - return JsonConverter.convertToAttributesProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, attributesDynamicMessageDescriptor))); + return JsonConverter.convertToAttributesProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, attributesDynamicMessageDescriptor))); } catch (Exception e) { log.debug("Failed to decode post attributes request", e); throw new AdaptorException(e); @@ -102,7 +102,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { Descriptors.Descriptor rpcResponseDynamicMessageDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getRpcResponseDynamicMessageDescriptor()); try { int requestId = getRequestId(topicName, topicBase); - JsonElement response = new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, rpcResponseDynamicMessageDescriptor)); + JsonElement response = JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, rpcResponseDynamicMessageDescriptor)); return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setPayload(response.toString()).build(); } catch (Exception e) { log.debug("Failed to decode rpc response", e); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java index 2d58785cdd..05658fe97a 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java @@ -94,7 +94,7 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler tsKv) { JsonObject json = new JsonObject(); for (KeyValueProto kv : tsKv) { @@ -43,7 +41,7 @@ public class JsonUtils { json.addProperty(kv.getKey(), kv.getStringV()); break; case JSON_V: - json.add(kv.getKey(), jsonParser.parse(kv.getJsonV())); + json.add(kv.getKey(), JsonParser.parseString(kv.getJsonV())); break; } } @@ -51,7 +49,7 @@ public class JsonUtils { } public static JsonElement parse(String params) { - return jsonParser.parse(params); + return JsonParser.parseString(params); } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java b/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java index 3f32eb54b4..4b95ae1aac 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java @@ -51,6 +51,7 @@ import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.TimeUnit; import static org.eclipse.leshan.client.object.Security.noSec; import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; @@ -95,7 +96,7 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable { LwM2mModel model = new StaticModel(models); ObjectsInitializer initializer = new ObjectsInitializer(model); initializer.setInstancesForObject(SECURITY, security); - initializer.setInstancesForObject(SERVER, new Server(123, 300)); + initializer.setInstancesForObject(SERVER, new Server(123, TimeUnit.MINUTES.toSeconds(60))); initializer.setInstancesForObject(DEVICE, this); initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java index 4c20199c8e..9b1ab4bd22 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java @@ -61,6 +61,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY; @@ -118,7 +119,7 @@ public class LwM2MTestClient { // SECURITY initializer.setInstancesForObject(SECURITY, security); // SERVER - Server lwm2mServer = new Server(shortServerId, 300); + Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60)); lwm2mServer.setId(serverId); initializer.setInstancesForObject(SERVER, lwm2mServer); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java index 4b957db9cd..1aad815dfa 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java @@ -72,7 +72,6 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; public class TbSaveToCustomCassandraTableNode implements TbNode { private static final String TABLE_PREFIX = "cs_tb_"; - private static final JsonParser parser = new JsonParser(); private static final String ENTITY_ID = "$entityId"; private TbSaveToCustomCassandraTableNodeConfiguration config; @@ -168,7 +167,7 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { } private ListenableFuture save(TbMsg msg, TbContext ctx) { - JsonElement data = parser.parse(msg.getData()); + JsonElement data = JsonParser.parseString(msg.getData()); if (!data.isJsonObject()) { throw new IllegalStateException("Invalid message structure, it is not a JSON Object:" + data); } else { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java index 32404cdf81..b2d16434d5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java @@ -25,7 +25,6 @@ import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; @@ -75,7 +74,6 @@ public class TbGpsGeofencingActionNode extends AbstractGeofencingNode entityStates = new HashMap<>(); private final Gson gson = new Gson(); - private final JsonParser parser = new JsonParser(); @Override public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { @@ -88,7 +86,7 @@ public class TbGpsGeofencingActionNode extends AbstractGeofencingNode ({isAuthenticated: results[0], isUserLoaded: results[1]})), - distinctUntilChanged(), - filter((data) => data.isUserLoaded ), + filter((data) => data.isUserLoaded), + distinctUntilChanged((a, b) => isEqual(a, b)), + tap((data) => { + let userLang = getCurrentAuthState(this.store).userDetails?.additionalInfo?.lang ?? null; + if (!userLang && !data.isAuthenticated) { + const settings = this.storageService.getItem(SETTINGS_KEY); + userLang = settings?.userLang ?? null; + } + this.notifyUserLang(userLang); + }), skip(1), ).subscribe((data) => { this.authService.gotoDefaultPlace(data.isAuthenticated); @@ -111,4 +122,8 @@ export class AppComponent implements OnInit { } } + private notifyUserLang(userLang: string) { + this.store.dispatch(new ActionSettingsChangeLanguage({userLang})); + } + } diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 0cc0ca4567..ea3dfb4143 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -22,7 +22,7 @@ import { Observable, of, ReplaySubject, throwError } from 'rxjs'; import { catchError, map, mergeMap, tap } from 'rxjs/operators'; import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models'; -import { ActivatedRoute, Router, UrlTree } from '@angular/router'; +import { Router, UrlTree } from '@angular/router'; import { defaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from '../http/http-utils'; import { UserService } from '../http/user.service'; import { Store } from '@ngrx/store'; @@ -35,7 +35,6 @@ import { } from './auth.actions'; import { getCurrentAuthState, getCurrentAuthUser } from './auth.selectors'; import { Authority } from '@shared/models/authority.enum'; -import { ActionSettingsChangeLanguage } from '@app/core/settings/settings.actions'; import { AuthPayload, AuthState, SysParams, SysParamsState } from '@core/auth/auth.models'; import { TranslateService } from '@ngx-translate/core'; import { AuthUser } from '@shared/models/user.model'; @@ -59,7 +58,6 @@ export class AuthService { private userService: UserService, private timeService: TimeService, private router: Router, - private route: ActivatedRoute, private zone: NgZone, private utils: UtilsService, private translate: TranslateService, @@ -419,14 +417,7 @@ export class AuthService { this.loadSystemParams().subscribe( (sysParams) => { authPayload = {...authPayload, ...sysParams}; - let userLang; - if (authPayload.userDetails.additionalInfo && authPayload.userDetails.additionalInfo.lang) { - userLang = authPayload.userDetails.additionalInfo.lang; - } else { - userLang = null; - } loadUserSubject.next(authPayload); - this.notifyUserLang(userLang); loadUserSubject.complete(); }, (err) => { @@ -607,10 +598,6 @@ export class AuthService { this.store.dispatch(new ActionAuthAuthenticated(authPayload)); } - private notifyUserLang(userLang: string) { - this.store.dispatch(new ActionSettingsChangeLanguage({userLang})); - } - private updateAndValidateToken(token, prefix, notify) { let valid = false; const tokenData = this.jwtHelper.decodeToken(token); diff --git a/ui-ngx/src/app/core/core.module.ts b/ui-ngx/src/app/core/core.module.ts index 2f713e405f..a72eb1ada7 100644 --- a/ui-ngx/src/app/core/core.module.ts +++ b/ui-ngx/src/app/core/core.module.ts @@ -31,7 +31,6 @@ import { TranslateModule, TranslateParser } from '@ngx-translate/core'; -import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { TbMissingTranslationHandler } from './translate/missing-translate-handler'; import { MatButtonModule } from '@angular/material/button'; import { MAT_DIALOG_DEFAULT_OPTIONS, MatDialogConfig, MatDialogModule } from '@angular/material/dialog'; @@ -41,9 +40,10 @@ import { TranslateDefaultCompiler } from '@core/translate/translate-default-comp import { WINDOW_PROVIDERS } from '@core/services/window.service'; import { HotkeyModule } from 'angular2-hotkeys'; import { TranslateDefaultParser } from '@core/translate/translate-default-parser'; +import { TranslateDefaultLoader } from '@core/translate/translate-default-loader'; export function HttpLoaderFactory(http: HttpClient) { - return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json'); + return new TranslateDefaultLoader(http); } @NgModule({ diff --git a/ui-ngx/src/app/core/services/resources.service.ts b/ui-ngx/src/app/core/services/resources.service.ts index bd2402b44c..449e950a65 100644 --- a/ui-ngx/src/app/core/services/resources.service.ts +++ b/ui-ngx/src/app/core/services/resources.service.ts @@ -34,6 +34,7 @@ import { select, Store } from '@ngrx/store'; import { selectIsAuthenticated } from '@core/auth/auth.selectors'; import { AppState } from '@core/core.state'; import { map, tap } from 'rxjs/operators'; +import { RequestConfig } from '@core/http/http-utils'; declare const System; @@ -106,11 +107,11 @@ export class ResourcesService { return this.loadResourceByType(fileType, url); } - public downloadResource(downloadUrl: string): Observable { - return this.http.get(downloadUrl, { + public downloadResource(downloadUrl: string, config?: RequestConfig): Observable { + return this.http.get(downloadUrl, {...config, ...{ responseType: 'arraybuffer', observe: 'response' - }).pipe( + }}).pipe( map((response) => { const headers = response.headers; const filename = headers.get('x-filename'); diff --git a/ui-ngx/src/app/core/settings/settings.effects.ts b/ui-ngx/src/app/core/settings/settings.effects.ts index 56615bf62c..1564814a74 100644 --- a/ui-ngx/src/app/core/settings/settings.effects.ts +++ b/ui-ngx/src/app/core/settings/settings.effects.ts @@ -28,7 +28,6 @@ import { AppState } from '@app/core/core.state'; import { LocalStorageService } from '@app/core/local-storage/local-storage.service'; import { TitleService } from '@app/core/services/title.service'; import { updateUserLang } from '@app/core/settings/settings.utils'; -import { AuthService } from '@core/auth/auth.service'; import { UtilsService } from '@core/services/utils.service'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { ActionAuthUpdateLastPublicDashboardId } from '../auth/auth.actions'; @@ -40,7 +39,6 @@ export class SettingsEffects { constructor( private actions$: Actions, private store: Store, - private authService: AuthService, private utils: UtilsService, private router: Router, private localStorageService: LocalStorageService, @@ -49,26 +47,19 @@ export class SettingsEffects { ) { } - - persistSettings = createEffect(() => this.actions$.pipe( + setTranslateServiceLanguage = createEffect(() => this.actions$.pipe( ofType( SettingsActionTypes.CHANGE_LANGUAGE, ), withLatestFrom(this.store.pipe(select(selectSettingsState))), - tap(([action, settings]) => - this.localStorageService.setItem(SETTINGS_KEY, settings) - ) - ), {dispatch: false}); - - - setTranslateServiceLanguage = createEffect(() => this.store.pipe( - select(selectSettingsState), - map(settings => settings.userLang), - distinctUntilChanged(), - tap(userLang => updateUserLang(this.translate, userLang)) + map(settings => settings[1]), + distinctUntilChanged((a, b) => a?.userLang === b?.userLang), + tap(setting => { + this.localStorageService.setItem(SETTINGS_KEY, setting); + updateUserLang(this.translate, setting.userLang); + }) ), {dispatch: false}); - setTitle = createEffect(() => merge( this.actions$.pipe(ofType(SettingsActionTypes.CHANGE_LANGUAGE)), this.router.events.pipe(filter(event => event instanceof ActivationEnd)) @@ -81,7 +72,6 @@ export class SettingsEffects { }) ), {dispatch: false}); - setPublicId = createEffect(() => merge( this.router.events.pipe(filter(event => event instanceof ActivationEnd)) ).pipe( diff --git a/ui-ngx/src/app/core/settings/settings.utils.ts b/ui-ngx/src/app/core/settings/settings.utils.ts index dfe8b37519..e816bbffb0 100644 --- a/ui-ngx/src/app/core/settings/settings.utils.ts +++ b/ui-ngx/src/app/core/settings/settings.utils.ts @@ -18,7 +18,7 @@ import { environment as env } from '@env/environment'; import { TranslateService } from '@ngx-translate/core'; import * as _moment from 'moment'; -export function updateUserLang(translate: TranslateService, userLang: string) { +export function updateUserLang(translate: TranslateService, userLang: string, translations = env.supportedLangs) { let targetLang = userLang; if (!env.production) { console.log(`User lang: ${targetLang}`); @@ -29,7 +29,7 @@ export function updateUserLang(translate: TranslateService, userLang: string) { console.log(`Fallback to browser lang: ${targetLang}`); } } - const detectedSupportedLang = detectSupportedLang(targetLang); + const detectedSupportedLang = detectSupportedLang(targetLang, translations); if (!env.production) { console.log(`Detected supported lang: ${detectedSupportedLang}`); } @@ -37,10 +37,10 @@ export function updateUserLang(translate: TranslateService, userLang: string) { _moment.locale([detectedSupportedLang]); } -function detectSupportedLang(targetLang: string): string { +function detectSupportedLang(targetLang: string, translations: string[]): string { const langTag = (targetLang || '').split('-').join('_'); if (langTag.length) { - if (env.supportedLangs.indexOf(langTag) > -1) { + if (translations.indexOf(langTag) > -1) { return langTag; } else { const parts = langTag.split('_'); @@ -50,7 +50,7 @@ function detectSupportedLang(targetLang: string): string { } else { lang = langTag; } - const foundLangs = env.supportedLangs.filter( + const foundLangs = translations.filter( (supportedLang: string) => { const supportedLangParts = supportedLang.split('_'); return supportedLangParts[0] === lang; diff --git a/ui-ngx/src/app/core/translate/translate-default-loader.ts b/ui-ngx/src/app/core/translate/translate-default-loader.ts new file mode 100644 index 0000000000..7f26170c9c --- /dev/null +++ b/ui-ngx/src/app/core/translate/translate-default-loader.ts @@ -0,0 +1,30 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { TranslateLoader } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; + +export class TranslateDefaultLoader implements TranslateLoader { + + constructor(private http: HttpClient) { + + } + + getTranslation(lang: string): Observable { + return this.http.get(`/assets/locale/locale.constant-${lang}.json`); + } +} diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 999d05b3a8..dc9ed5004b 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -338,6 +338,8 @@ export const isEmpty = (a: any): boolean => _.isEmpty(a); export const unset = (object: any, path: string | symbol): boolean => _.unset(object, path); +export const setByPath = (object: T, path: string | number | symbol, value: any): T => _.set(object, path, value); + export const isEqualIgnoreUndefined = (a: any, b: any): boolean => { if (a === b) { return true; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts index 79ec4b03c7..132ca3d487 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts @@ -114,6 +114,9 @@ import { ComparisonKeyRowComponent } from '@home/components/widget/config/basic/ import { ComparisonKeysTableComponent } from '@home/components/widget/config/basic/chart/comparison-keys-table.component'; +import { + StatusWidgetBasicConfigComponent +} from '@home/components/widget/config/basic/indicator/status-widget-basic-config.component'; @NgModule({ declarations: [ @@ -151,7 +154,8 @@ import { ToggleButtonBasicConfigComponent, TimeSeriesChartBasicConfigComponent, ComparisonKeyRowComponent, - ComparisonKeysTableComponent + ComparisonKeysTableComponent, + StatusWidgetBasicConfigComponent ], imports: [ CommonModule, @@ -191,7 +195,8 @@ import { PowerButtonBasicConfigComponent, SliderBasicConfigComponent, ToggleButtonBasicConfigComponent, - TimeSeriesChartBasicConfigComponent + TimeSeriesChartBasicConfigComponent, + StatusWidgetBasicConfigComponent ] }) export class BasicWidgetConfigModule { @@ -225,5 +230,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type + + +
+
widgets.status-widget.behavior
+
+
widgets.rpc-state.initial-state
+ +
+
+
widgets.rpc-state.disabled-state
+ +
+
+
+
widget-config.appearance
+ + + {{ statusWidgetLayoutTranslationMap.get(layout) | translate }} + + +
+
+
+
widget-config.card-style
+ + {{ 'widgets.status-widget.on' | translate }} + {{ 'widgets.status-widget.off' | translate }} + +
+ + + + +
+
+
widget-config.card-appearance
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts new file mode 100644 index 0000000000..d2110026f6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/status-widget-basic-config.component.ts @@ -0,0 +1,119 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { TargetDevice, WidgetConfig, } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { isUndefined } from '@core/utils'; +import { ValueType } from '@shared/models/constants'; +import { + statusWidgetDefaultSettings, + statusWidgetLayoutImages, + statusWidgetLayouts, + statusWidgetLayoutTranslations, + StatusWidgetSettings +} from '@home/components/widget/lib/indicator/status-widget.models'; + +@Component({ + selector: 'tb-status-widget-basic-config', + templateUrl: './status-widget-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class StatusWidgetBasicConfigComponent extends BasicWidgetConfigComponent { + + get targetDevice(): TargetDevice { + return this.statusWidgetConfigForm.get('targetDevice').value; + } + + statusWidgetLayouts = statusWidgetLayouts; + + statusWidgetLayoutTranslationMap = statusWidgetLayoutTranslations; + statusWidgetLayoutImageMap = statusWidgetLayoutImages; + + valueType = ValueType; + + statusWidgetConfigForm: UntypedFormGroup; + + cardStyleMode = 'on'; + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.statusWidgetConfigForm; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: StatusWidgetSettings = {...statusWidgetDefaultSettings, ...(configData.config.settings || {})}; + this.statusWidgetConfigForm = this.fb.group({ + targetDevice: [configData.config.targetDevice, []], + + initialState: [settings.initialState, []], + disabledState: [settings.disabledState, []], + + layout: [settings.layout, []], + + onState: [settings.onState, []], + offState: [settings.offState, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.targetDevice = config.targetDevice; + + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + + this.widgetConfig.config.settings.initialState = config.initialState; + this.widgetConfig.config.settings.disabledState = config.disabledState; + + this.widgetConfig.config.settings.layout = config.layout; + + this.widgetConfig.config.settings.onState = config.onState; + this.widgetConfig.config.settings.offState = config.offState; + + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + + this.widgetConfig.config.actions = config.actions; + return this.widgetConfig; + } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.enableFullscreen = buttons.includes('fullscreen'); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html new file mode 100644 index 0000000000..391ade6228 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.html @@ -0,0 +1,33 @@ + +
+
+
+ +
+
+
+ {{ icon }} +
+
+
{{ label$ | async }}
+
{{ status$ | async }}
+
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.scss new file mode 100644 index 0000000000..8dcefddd2a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.scss @@ -0,0 +1,99 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-status-widget-panel { + width: 100%; + height: 100%; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + > div:not(.tb-status-widget-overlay), > tb-icon { + z-index: 1; + } + .tb-status-widget-overlay { + position: absolute; + inset: 12px; + } + > div.tb-status-widget-title-panel { + position: absolute; + top: 12px; + left: 12px; + right: 12px; + z-index: 2; + } + .tb-status-widget-content { + width: 100%; + height: 100%; + padding: 16px; + position: relative; + display: flex; + flex-direction: column; + .tb-status-widget-icon-container { + display: flex; + width: 100%; + flex-direction: column; + } + .tb-status-widget-labels-container { + display: flex; + width: 100%; + flex-direction: column; + .tb-status-widget-label { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + } + .tb-status-widget-status { + text-transform: uppercase; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + &.default { + place-content: flex-start space-between; + align-items: flex-start; + } + &.center { + place-content: center flex-start; + align-items: center; + .tb-status-widget-icon-container { + flex: 1; + place-content: center; + align-items: center; + } + .tb-status-widget-labels-container { + flex-direction: column-reverse; + place-content: center flex-start; + align-items: center; + } + } + &.icon { + place-content: center; + align-items: center; + .tb-status-widget-icon-container { + flex: 1; + place-content: center; + align-items: center; + } + .tb-status-widget-labels-container { + display: none; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts new file mode 100644 index 0000000000..5b59b0210f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.component.ts @@ -0,0 +1,242 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + OnDestroy, + OnInit, + Renderer2, ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { BasicActionWidgetComponent } from '@home/components/widget/lib/action/action-widget.models'; +import { + statusWidgetDefaultSettings, + StatusWidgetLayout, + StatusWidgetSettings, StatusWidgetStateSettings +} from '@home/components/widget/lib/indicator/status-widget.models'; +import { Observable } from 'rxjs'; +import { + backgroundStyle, + ComponentStyle, + iconStyle, + overlayStyle, + textStyle +} from '@shared/models/widget-settings.models'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; +import { UtilsService } from '@core/services/utils.service'; +import { ValueType } from '@shared/models/constants'; + +const initialStatusWidgetSize = 147; + +@Component({ + selector: 'tb-status-widget', + templateUrl: './status-widget.component.html', + styleUrls: ['../action/action-widget.scss', './status-widget.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class StatusWidgetComponent extends + BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { + + @ViewChild('statusWidgetPanel', {static: false}) + statusWidgetPanel: ElementRef; + + @ViewChild('statusWidgetContent', {static: false}) + statusWidgetContent: ElementRef; + + settings: StatusWidgetSettings; + + backgroundStyle$: Observable; + overlayStyle: ComponentStyle = {}; + + overlayInset = '12px'; + borderRadius = ''; + + layout: StatusWidgetLayout; + + showLabel = true; + label$: Observable; + labelStyle: ComponentStyle = {}; + + showStatus = true; + status$: Observable; + statusStyle: ComponentStyle = {}; + + icon = ''; + iconStyle: ComponentStyle = {}; + + private panelResize$: ResizeObserver; + + private onLabel$: Observable; + private onStatus$: Observable; + private onBackground$: Observable; + private onBackgroundDisabled$: Observable; + + private offLabel$: Observable; + private offStatus$: Observable; + private offBackground$: Observable; + private offBackgroundDisabled$: Observable; + + private state = false; + private disabled = false; + private disabledState = false; + + constructor(protected imagePipe: ImagePipe, + protected sanitizer: DomSanitizer, + private renderer: Renderer2, + private utils: UtilsService, + protected cd: ChangeDetectorRef, + private elementRef: ElementRef) { + super(cd); + } + + ngOnInit(): void { + super.ngOnInit(); + this.settings = {...statusWidgetDefaultSettings, ...this.ctx.settings}; + this.layout = this.settings.layout; + + this.onLabel$ = this.ctx.registerLabelPattern(this.settings.onState.label, this.onLabel$); + this.onStatus$ = this.ctx.registerLabelPattern(this.settings.onState.status, this.onStatus$); + this.onBackground$ = backgroundStyle(this.settings.onState.background, this.imagePipe, this.sanitizer); + this.onBackgroundDisabled$ = backgroundStyle(this.settings.onState.backgroundDisabled, this.imagePipe, this.sanitizer); + + this.offLabel$ = this.ctx.registerLabelPattern(this.settings.offState.label, this.offLabel$); + this.offStatus$ = this.ctx.registerLabelPattern(this.settings.offState.status, this.offStatus$); + this.offBackground$ = backgroundStyle(this.settings.offState.background, this.imagePipe, this.sanitizer); + this.offBackgroundDisabled$ = backgroundStyle(this.settings.offState.backgroundDisabled, this.imagePipe, this.sanitizer); + + const getInitialStateSettings = + {...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.initial-state')}; + this.createValueGetter(getInitialStateSettings, ValueType.BOOLEAN, { + next: (value) => this.onState(value) + }); + + const disabledStateSettings = + {...this.settings.disabledState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.disabled-state')}; + this.createValueGetter(disabledStateSettings, ValueType.BOOLEAN, { + next: (value) => this.onDisabled(value) + }); + + this.loading$.subscribe((loading) => { + this.updateDisabledState(loading || this.disabled); + }); + + this.updateStyle(this.state, this.disabled); + } + + ngAfterViewInit(): void { + this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'overflow', 'visible'); + this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'position', 'absolute'); + this.panelResize$ = new ResizeObserver(() => { + this.onResize(); + }); + this.panelResize$.observe(this.statusWidgetPanel.nativeElement); + if (this.showLabel) { + this.panelResize$.observe(this.statusWidgetPanel.nativeElement); + } + this.onResize(); + super.ngAfterViewInit(); + } + + ngOnDestroy() { + if (this.panelResize$) { + this.panelResize$.disconnect(); + } + super.ngOnDestroy(); + } + + public onInit() { + super.onInit(); + this.borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius: this.borderRadius}}; + this.cd.detectChanges(); + } + + private onState(value: boolean): void { + const newState = !!value; + if (this.state !== newState) { + this.state = newState; + this.updateStyle(this.state, this.disabled || this.disabledState); + } + } + + private onDisabled(value: boolean): void { + const newDisabled = !!value; + if (this.disabled !== newDisabled) { + this.disabled = newDisabled; + this.updateDisabledState(this.disabled); + } + } + + private updateDisabledState(disabled: boolean) { + this.disabledState = disabled; + this.updateStyle(this.state, this.disabledState); + } + + private onResize() { + const panelWidth = this.statusWidgetPanel.nativeElement.getBoundingClientRect().width; + const panelHeight = this.statusWidgetPanel.nativeElement.getBoundingClientRect().height; + const targetSize = Math.min(panelWidth, panelHeight); + const scale = targetSize / initialStatusWidgetSize; + const width = initialStatusWidgetSize; + const height = initialStatusWidgetSize; + this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'width', width + 'px'); + this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'height', height + 'px'); + this.renderer.setStyle(this.statusWidgetContent.nativeElement, 'transform', `scale(${scale})`); + this.overlayInset = (Math.floor(12 * scale * 100) / 100) + 'px'; + this.cd.markForCheck(); + } + + private updateStyle(state: boolean, disabled: boolean) { + let stateSettings: StatusWidgetStateSettings; + if (state) { + this.label$ = this.onLabel$; + this.status$ = this.onStatus$; + this.backgroundStyle$ = disabled ? this.onBackgroundDisabled$ : this.onBackground$; + stateSettings = this.settings.onState; + } else { + this.label$ = this.offLabel$; + this.status$ = this.offStatus$; + this.backgroundStyle$ = disabled ? this.offBackgroundDisabled$ : this.offBackground$; + stateSettings = this.settings.offState; + } + this.showLabel = stateSettings.showLabel && this.layout !== StatusWidgetLayout.icon; + this.showStatus = stateSettings.showStatus && this.layout !== StatusWidgetLayout.icon; + this.icon = stateSettings.icon; + + const primaryColor = disabled ? stateSettings.primaryColorDisabled : stateSettings.primaryColor; + const secondaryColor = disabled ? stateSettings.secondaryColorDisabled : stateSettings.secondaryColor; + + this.labelStyle = textStyle(stateSettings.labelFont); + this.labelStyle.color = primaryColor; + + this.statusStyle = textStyle(stateSettings.statusFont); + this.statusStyle.color = secondaryColor; + + this.iconStyle = iconStyle(stateSettings.iconSize, stateSettings.iconSizeUnit); + this.iconStyle.color = primaryColor; + + this.overlayStyle = overlayStyle(disabled ? stateSettings.backgroundDisabled.overlay : stateSettings.background.overlay); + if (this.borderRadius) { + this.overlayStyle = {...this.overlayStyle, ...{borderRadius: this.borderRadius}}; + } + this.cd.detectChanges(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.models.ts new file mode 100644 index 0000000000..2ef999c6f2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/status-widget.models.ts @@ -0,0 +1,204 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { DataToValueType, GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models'; +import { BackgroundSettings, BackgroundType, cssUnit, Font } from '@shared/models/widget-settings.models'; + +export enum StatusWidgetLayout { + default = 'default', + center = 'center', + icon = 'icon' +} + +export const statusWidgetLayouts = Object.keys(StatusWidgetLayout) as StatusWidgetLayout[]; + +export const statusWidgetLayoutTranslations = new Map( + [ + [StatusWidgetLayout.default, 'widgets.status-widget.layout-default'], + [StatusWidgetLayout.center, 'widgets.status-widget.layout-center'], + [StatusWidgetLayout.icon, 'widgets.status-widget.layout-icon'] + ] +); + +export const statusWidgetLayoutImages = new Map( + [ + [StatusWidgetLayout.default, 'assets/widget/status-widget/default-layout.svg'], + [StatusWidgetLayout.center, 'assets/widget/status-widget/center-layout.svg'], + [StatusWidgetLayout.icon, 'assets/widget/status-widget/icon-layout.svg'] + ] +); + +export interface StatusWidgetStateSettings { + showLabel: boolean; + label: string; + labelFont: Font; + showStatus: boolean; + status: string; + statusFont: Font; + icon: string; + iconSize: number; + iconSizeUnit: cssUnit; + primaryColor: string; + secondaryColor: string; + background: BackgroundSettings; + primaryColorDisabled: string; + secondaryColorDisabled: string; + backgroundDisabled: BackgroundSettings; +} + +export interface StatusWidgetSettings { + initialState: GetValueSettings; + disabledState: GetValueSettings; + layout: StatusWidgetLayout; + onState: StatusWidgetStateSettings; + offState: StatusWidgetStateSettings; +} + +export const statusWidgetDefaultSettings: StatusWidgetSettings = { + initialState: { + action: GetValueAction.EXECUTE_RPC, + defaultValue: false, + executeRpc: { + method: 'getState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return boolean value */\nreturn data;' + } + }, + disabledState: { + action: GetValueAction.DO_NOTHING, + defaultValue: false, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return boolean value */\nreturn data;' + } + }, + layout: StatusWidgetLayout.default, + onState: { + showLabel: true, + label: 'Window left corner', + labelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '16px' + }, + showStatus: true, + status: 'Opened', + statusFont: { + family: 'Roboto', + size: 10, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '20px' + }, + icon: 'mdi:curtains', + iconSize: 32, + iconSizeUnit: 'px', + primaryColor: '#fff', + secondaryColor: 'rgba(255, 255, 255, 0.80)', + background: { + type: BackgroundType.color, + color: '#3F52DD', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + }, + primaryColorDisabled: 'rgba(0, 0, 0, 0.38)', + secondaryColorDisabled: 'rgba(0, 0, 0, 0.38)', + backgroundDisabled: { + type: BackgroundType.color, + color: '#CACACA', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + } + }, + offState: { + showLabel: true, + label: 'Window left corner', + labelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '16px' + }, + showStatus: true, + status: 'Closed', + statusFont: { + family: 'Roboto', + size: 10, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '20px' + }, + icon: 'mdi:curtains-closed', + iconSize: 32, + iconSizeUnit: 'px', + primaryColor: 'rgba(0, 0, 0, 0.87)', + secondaryColor: 'rgba(0, 0, 0, 0.54)', + background: { + type: BackgroundType.color, + color: '#FFF', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + }, + primaryColorDisabled: 'rgba(0, 0, 0, 0.38)', + secondaryColorDisabled: 'rgba(0, 0, 0, 0.38)', + backgroundDisabled: { + type: BackgroundType.color, + color: '#CACACA', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + } + } +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html new file mode 100644 index 0000000000..40f26c6e9c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.html @@ -0,0 +1,111 @@ + + +
+ + {{ 'widgets.status-widget.label' | translate }} + +
+ + + + + +
+
+
+ + {{ 'widgets.status-widget.status' | translate }} + +
+ + + + + +
+
+
+
+ {{ 'widgets.status-widget.icon' | translate }} +
+
+ + + + + + +
+
+
+
{{ 'widgets.status-widget.color-palette' | translate }}
+
+
+
widgets.status-widget.primary
+ + +
+ +
+
widgets.status-widget.secondary
+ + +
+ +
+
widgets.status-widget.background
+ + +
+
+
+
+
{{ 'widgets.status-widget.disabled-color-palette' | translate }}
+
+
+
widgets.status-widget.primary
+ + +
+ +
+
widgets.status-widget.secondary
+ + +
+ +
+
widgets.status-widget.background
+ + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts new file mode 100644 index 0000000000..8425264d89 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component.ts @@ -0,0 +1,158 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { merge } from 'rxjs'; +import { + StatusWidgetLayout, + StatusWidgetStateSettings +} from '@home/components/widget/lib/indicator/status-widget.models'; + +@Component({ + selector: 'tb-status-widget-state-settings', + templateUrl: './status-widget-state-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => StatusWidgetStateSettingsComponent), + multi: true + } + ] +}) +export class StatusWidgetStateSettingsComponent implements OnInit, OnChanges, ControlValueAccessor { + + StatusWidgetLayout = StatusWidgetLayout; + + @Input() + disabled: boolean; + + @Input() + layout: StatusWidgetLayout; + + private modelValue: StatusWidgetStateSettings; + + private propagateChange = null; + + public stateSettingsFormGroup: UntypedFormGroup; + + constructor(private fb: UntypedFormBuilder) { + } + + ngOnInit(): void { + this.stateSettingsFormGroup = this.fb.group({ + showLabel: [null, []], + label: [null, []], + labelFont: [null, []], + showStatus: [null, []], + status: [null, []], + statusFont: [null, []], + icon: [null, []], + iconSize: [null, []], + iconSizeUnit: [null, []], + primaryColor: [null, []], + secondaryColor: [null, []], + background: [null, []], + primaryColorDisabled: [null, []], + secondaryColorDisabled: [null, []], + backgroundDisabled: [null, []] + }); + this.stateSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + merge(this.stateSettingsFormGroup.get('showLabel').valueChanges, + this.stateSettingsFormGroup.get('showStatus').valueChanges) + .subscribe(() => { + this.updateValidators(); + }); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['layout'].includes(propName)) { + this.updateValidators(); + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.stateSettingsFormGroup.disable({emitEvent: false}); + } else { + this.stateSettingsFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: StatusWidgetStateSettings): void { + this.modelValue = value; + this.stateSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(); + } + + private updateValidators() { + if (this.layout === StatusWidgetLayout.icon) { + this.stateSettingsFormGroup.get('showLabel').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('label').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('labelFont').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('showStatus').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('status').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('statusFont').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('secondaryColor').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('secondaryColorDisabled').disable({emitEvent: false}); + } else { + this.stateSettingsFormGroup.get('showLabel').enable({emitEvent: false}); + this.stateSettingsFormGroup.get('showStatus').enable({emitEvent: false}); + this.stateSettingsFormGroup.get('secondaryColor').enable({emitEvent: false}); + this.stateSettingsFormGroup.get('secondaryColorDisabled').enable({emitEvent: false}); + const showLabel: boolean = this.stateSettingsFormGroup.get('showLabel').value; + const showStatus: boolean = this.stateSettingsFormGroup.get('showStatus').value; + if (showLabel) { + this.stateSettingsFormGroup.get('label').enable({emitEvent: false}); + this.stateSettingsFormGroup.get('labelFont').enable({emitEvent: false}); + } else { + this.stateSettingsFormGroup.get('label').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('labelFont').disable({emitEvent: false}); + } + if (showStatus) { + this.stateSettingsFormGroup.get('status').enable({emitEvent: false}); + this.stateSettingsFormGroup.get('statusFont').enable({emitEvent: false}); + } else { + this.stateSettingsFormGroup.get('status').disable({emitEvent: false}); + this.stateSettingsFormGroup.get('statusFont').disable({emitEvent: false}); + } + } + } + + private updateModel() { + this.modelValue = this.stateSettingsFormGroup.getRawValue(); + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index 3742708e1a..ced092b8cc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -145,6 +145,9 @@ import { import { TimeSeriesChartGridSettingsComponent } from '@home/components/widget/lib/settings/common/chart/time-series-chart-grid-settings.component'; +import { + StatusWidgetStateSettingsComponent +} from '@home/components/widget/lib/settings/common/indicator/status-widget-state-settings.component'; @NgModule({ declarations: [ @@ -198,6 +201,7 @@ import { TimeSeriesChartStatesPanelComponent, TimeSeriesChartStateRowComponent, TimeSeriesChartGridSettingsComponent, + StatusWidgetStateSettingsComponent, DataKeyInputComponent, EntityAliasInputComponent ], @@ -257,6 +261,7 @@ import { TimeSeriesChartStatesPanelComponent, TimeSeriesChartStateRowComponent, TimeSeriesChartGridSettingsComponent, + StatusWidgetStateSettingsComponent, DataKeyInputComponent, EntityAliasInputComponent ], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.html new file mode 100644 index 0000000000..180340881d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.html @@ -0,0 +1,82 @@ + + +
+
widgets.status-widget.behavior
+
+
widgets.rpc-state.initial-state
+ +
+
+
widgets.rpc-state.disabled-state
+ +
+
+
+
widget-config.appearance
+ + + {{ statusWidgetLayoutTranslationMap.get(layout) | translate }} + + +
+
+
+
widget-config.card-style
+ + {{ 'widgets.status-widget.on' | translate }} + {{ 'widgets.status-widget.off' | translate }} + +
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.ts new file mode 100644 index 0000000000..03da985a0e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/status-widget-settings.component.ts @@ -0,0 +1,79 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Injector } from '@angular/core'; +import { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + statusWidgetDefaultSettings, + statusWidgetLayoutImages, + statusWidgetLayouts, + statusWidgetLayoutTranslations +} from '@home/components/widget/lib/indicator/status-widget.models'; +import { ValueType } from '@shared/models/constants'; + +@Component({ + selector: 'tb-status-widget-settings', + templateUrl: './status-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'], +}) +export class StatusWidgetSettingsComponent extends WidgetSettingsComponent { + + get targetDevice(): TargetDevice { + return this.widgetConfig?.config?.targetDevice; + } + + get widgetType(): widgetType { + return this.widgetConfig?.widgetType; + } + + statusWidgetLayouts = statusWidgetLayouts; + + statusWidgetLayoutTranslationMap = statusWidgetLayoutTranslations; + statusWidgetLayoutImageMap = statusWidgetLayoutImages; + + valueType = ValueType; + + statusWidgetSettingsForm: UntypedFormGroup; + + cardStyleMode = 'on'; + + constructor(protected store: Store, + private $injector: Injector, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.statusWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return {...statusWidgetDefaultSettings}; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.statusWidgetSettingsForm = this.fb.group({ + initialState: [settings.initialState, []], + disabledState: [settings.disabledState, []], + layout: [settings.layout, []], + onState: [settings.onState, []], + offState: [settings.offState, []] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 5b83f859e1..a5f9084968 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -342,6 +342,9 @@ import { import { TimeSeriesChartWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component'; +import { + StatusWidgetSettingsComponent +} from '@home/components/widget/lib/settings/indicator/status-widget-settings.component'; @NgModule({ declarations: [ @@ -464,7 +467,8 @@ import { TimeSeriesChartKeySettingsComponent, TimeSeriesChartLineSettingsComponent, TimeSeriesChartBarSettingsComponent, - TimeSeriesChartWidgetSettingsComponent + TimeSeriesChartWidgetSettingsComponent, + StatusWidgetSettingsComponent ], imports: [ CommonModule, @@ -592,7 +596,8 @@ import { TimeSeriesChartKeySettingsComponent, TimeSeriesChartLineSettingsComponent, TimeSeriesChartBarSettingsComponent, - TimeSeriesChartWidgetSettingsComponent + TimeSeriesChartWidgetSettingsComponent, + StatusWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -685,5 +690,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type email - {{ 'user.invalid-email-format' | translate }} + {{ 'login.invalid-email-format' | translate }} diff --git a/ui-ngx/src/app/shared/components/json-content.component.html b/ui-ngx/src/app/shared/components/json-content.component.html index 33b1936213..1a09d539c8 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.html +++ b/ui-ngx/src/app/shared/components/json-content.component.html @@ -18,7 +18,7 @@
-
+