Browse Source

merge with master

pull/11861/head
YevhenBondarenko 2 years ago
parent
commit
40e9496992
  1. 1
      README.md
  2. 10
      application/src/main/data/json/demo/dashboards/firmware.json
  3. 10
      application/src/main/data/json/demo/dashboards/software.json
  4. 4
      application/src/main/data/json/demo/dashboards/thermostats.json
  5. 22
      application/src/main/data/json/edge/instructions/install/centos/instructions.md
  6. 2
      application/src/main/data/json/edge/instructions/install/docker/instructions.md
  7. 2
      application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md
  8. 2
      application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md
  9. 2
      application/src/main/data/json/system/widget_types/asset_admin_table.json
  10. 4
      application/src/main/data/json/system/widget_types/basic_gpio_control.json
  11. 4
      application/src/main/data/json/system/widget_types/basic_gpio_panel.json
  12. 2
      application/src/main/data/json/system/widget_types/device_admin_table.json
  13. 2
      application/src/main/data/json/system/widget_types/device_claiming_widget.json
  14. 10
      application/src/main/data/json/system/widget_types/gateway_configuration.json
  15. 10
      application/src/main/data/json/system/widget_types/gateway_configuration__single_device_.json
  16. 10
      application/src/main/data/json/system/widget_types/gateway_connectors.json
  17. 10
      application/src/main/data/json/system/widget_types/gateway_custom_statistics.json
  18. 10
      application/src/main/data/json/system/widget_types/gateway_general_chart_statistics.json
  19. 10
      application/src/main/data/json/system/widget_types/gateway_general_configuration.json
  20. 10
      application/src/main/data/json/system/widget_types/gateway_logs.json
  21. 4
      application/src/main/data/json/system/widget_types/raspberry_pi_gpio_control.json
  22. 4
      application/src/main/data/json/system/widget_types/raspberry_pi_gpio_panel.json
  23. 2
      application/src/main/data/json/system/widget_types/rpc_button.json
  24. 12
      application/src/main/data/json/system/widget_types/service_rpc.json
  25. 2
      application/src/main/data/json/system/widget_types/update_boolean_timeseries.json
  26. 2
      application/src/main/data/json/system/widget_types/update_device_attribute.json
  27. 2
      application/src/main/data/json/system/widget_types/update_double_timeseries.json
  28. 2
      application/src/main/data/json/system/widget_types/update_integer_timeseries.json
  29. 2
      application/src/main/data/json/system/widget_types/update_location_timeseries.json
  30. 2
      application/src/main/data/json/system/widget_types/update_server_boolean_attribute.json
  31. 2
      application/src/main/data/json/system/widget_types/update_server_date_attribute.json
  32. 2
      application/src/main/data/json/system/widget_types/update_server_double_attribute.json
  33. 2
      application/src/main/data/json/system/widget_types/update_server_image_attribute.json
  34. 2
      application/src/main/data/json/system/widget_types/update_server_integer_attribute.json
  35. 2
      application/src/main/data/json/system/widget_types/update_server_location_attribute.json
  36. 2
      application/src/main/data/json/system/widget_types/update_server_string_attribute.json
  37. 2
      application/src/main/data/json/system/widget_types/update_shared_boolean_attribute.json
  38. 2
      application/src/main/data/json/system/widget_types/update_shared_date_attribute.json
  39. 2
      application/src/main/data/json/system/widget_types/update_shared_double_attribute.json
  40. 2
      application/src/main/data/json/system/widget_types/update_shared_image_attribute.json
  41. 2
      application/src/main/data/json/system/widget_types/update_shared_integer_attribute.json
  42. 2
      application/src/main/data/json/system/widget_types/update_shared_location_attribute.json
  43. 2
      application/src/main/data/json/system/widget_types/update_shared_string_attribute.json
  44. 2
      application/src/main/data/json/system/widget_types/update_string_timeseries.json
  45. 14
      application/src/main/data/resources/dashboards/gateways_dashboard.json
  46. 1
      application/src/main/data/resources/js_modules/gateway-management-extension.js
  47. 11
      application/src/main/data/upgrade/3.8.1/schema_update.sql
  48. 10
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  49. 8
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  50. 19
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  51. 70
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  52. 8
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  53. 3
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  54. 4
      application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
  55. 98
      application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncService.java
  56. 2
      application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java
  57. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/DefaultWidgetsBundleService.java
  58. 1
      application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/TbWidgetsBundleService.java
  59. 2
      application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java
  60. 137
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  61. 30
      application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java
  62. 4
      application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseSchemaService.java
  63. 23
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  64. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/EmailTwoFaProvider.java
  65. 2
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  66. 39
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  67. 64
      application/src/main/java/org/thingsboard/server/service/subscription/TbEntityLocalSubsInfo.java
  68. 172
      application/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java
  69. 42
      application/src/main/java/org/thingsboard/server/service/sync/GitSyncService.java
  70. 11
      application/src/main/resources/thingsboard.yml
  71. 82
      application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java
  72. 4
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  73. 2
      application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
  74. 16
      application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java
  75. 74
      application/src/test/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncServiceTest.java
  76. 5
      application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java
  77. 186
      application/src/test/java/org/thingsboard/server/service/subscription/TbEntityLocalSubsInfoTest.java
  78. 9
      application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java
  79. 138
      application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
  80. 4
      application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/LwM2mAttributesTest.java
  81. 11
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java
  82. 2
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java
  83. 46
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java
  84. 73
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java
  85. 732
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java
  86. 71
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java
  87. 86
      application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java
  88. 100
      application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java
  89. 73
      application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota9LwM2MIntegrationTest.java
  90. 9
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java
  91. 96
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java
  92. 74
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java
  93. 2
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java
  94. 2
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java
  95. 31
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java
  96. 201
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java
  97. 4
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java
  98. 44
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java
  99. 98
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java
  100. 78
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java

1
README.md

@ -1,5 +1,4 @@
# ThingsBoard # ThingsBoard
[![Join the chat at https://gitter.im/thingsboard/chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/thingsboard/chat?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![ThingsBoard Builds Server Status](https://img.shields.io/teamcity/build/e/ThingsBoard_Build?label=TB%20builds%20server&server=https%3A%2F%2Fbuilds.thingsboard.io&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAALzAAAC8wHS6QoqAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAB9FJREFUeJzVm3+MXUUVx7+zWwqEtnRLWisQ2lKVUisIQmsqYCohpUhpEGsFKSJJTS0qGiGIISJ/8CNGYzSaEKBQEZUiP7RgVbCVdpE0xYKBWgI2rFLZJZQWtFKobPfjH3Pfdu7s3Pvmzntv3/JNNr3bOXPO+Z6ZO3PumVmjFgEYJWmWpDmSZks6VtIESV3Zv29LWmGMubdVPgw7gEOBJcAaYC/18fd2+zyqngAwXdL7M9keSduMMXgyH5R0laRPSRpbwf62CrLDB8AAS4HnAqP2EvA1YBTwPuBnwP46I70H+DPwALAS+B5wBTCu3VyHIJvG98dMX+B/BW1vAvcAnwdmAp3t5hWFbORXR5AvwmPARcCYdnNJAnCBR+gd7HQ9HZgLfAt4PUB8AzCv3f43DGCTQ6o/RAo43gtCL2Da4W9TAUwEBhxiPymRvcabAR8eTl+biQ7neYokdyTXlvR7xPt9etM8GmZ0FDxL+WD42FdBdkTDJd0jyU1wzi7pd473e0+qA8AM4AbgkrK1BDgOWAc8ChyTaq+eM5ud93ofcHpAZiY2sanhZaDDaTfAZ7HJUmlWCJzm6bqLQM6QBanXkfthcxgPNbTEW9z2AT8AzgTmANdikxwXX/d0XOi0bQEmFNj6GPAfhuKnXkB98kNsNjsITwacKkI3MNrrf4UnswXoiiRfwyqgo4D8L2hVZglMw456DDYCRwR0jCH/KuWCgE2oysjX8KsA+V+2jHzm3CrP4PMBx/4JfAU4qETP+EAQ/gKcA/w7gnwNbl5yD7bG0DLyM7DZXw3d2f9PA+YD5wIzK+gLBSEFA/XIA2cAVwLvbSQAt3mGP5Gs7IDO8dg1ZYDGcAfOwujZuIwDn+ObUx09hHx+v7Eh5nndCyIIDgBbgd0lMiv9IABfIF+LeDnVyU97xj5XR/6bwI5sZEaXyH2UuHd+WSbfRXktYjAIAfL9wGdSA/Cgo+gtSio12IKJa3hNKAgZ+TciyL+AlwECKzI/ioLgTvsa+YtTyXeSz8ZW15E3wN88p3JBwCZNMeShIKkBTsRmmSG4a0o/sDSJfGboBE/5pRF9pgI9oSBUJP8mXpLk2bm6pO9Aw+QzI8s8xVFbXRaEf3h911cgD7Cyjg0/L/GxnoLdoUoA3O1vDxUyLWyO4AehCpYX6D2L/LpUhtsaCkIWxRoeT+g/DVsqT8EWYDowC5jh6FxUUc+tJJblOmSPqWp4JUFHl6TDUoxLOlnSdknPSnK3sA2S9lfQs0zS7SkzwQ/A61U6A6dKWufpSMVg5mmMeUPSXyv2v0zSN6oa7ZAdwRqiA5CRf0TS+KpGAxiQ1OFN4z8l6PErVXUxSvmp1hvTqUnk35adPWskPWSM6fPaq84ASXqscg/gi9gcvJuC6o0nfwrhw5EYvIpNn88HStcN4M6KulfTys/lzKlO0lb8P2Lrf6VbLDAF+DLweEX998aSx372bwP6gPlVA3BEAvm9FJwVYtPqjwDXA08n6AZbOYoeeeAWp++mSlPGGLMLeFjSuRW6Iektx4GDJc2TdJ6khZKOruKDh/skXWSM6a/Q5yjn+dDKFrE1vw0VR2m2039x4kj7uJ+SslyJ/+7rtaly4mCM+a+kBaq2TbnVpfWy216jmCzpkIR+7kK/MymHNsbslX0NYoMweMpsjNklaWuKXQ9zJf2eOocvAbzHee5N/ojIgvBVxY3madh3v4b1iWZ/o3zw5kpaS+SFDGCq8jPguUQ/CmsCZfi403dhwjv/AHAQMAl41mvbGBMEhq4/c1PJTwmQr1f7u97pfzj5EnwUead/KAg/ivD7Zkf+HSBpFwiRfwibI3SXkOj29PgEivAggdU+C8JWR+6+CN9dm1tSyHcBLwbIj87ax1Kcxe0DJmVyY4CdEeR/TXnVeRLwc+C3wHF1fP+Qp/uGlABc6Cl5mPziVi8IzwDfAZ6KIN9LyhQt9v1GT/+sFCXTOVBBXuOTd+TGkp+eqWjKSTBwMPAvR+9TjSibjK35l93mWIxdZFKOxPzFseEgAJd7Olt6v+AC8jdIqwRhLbZM758HRH3tYa/vnoqtKZ4JHIk99tvh6HqNVl3RLSB/JfBEBPnBwxXsJ2uf176qxO7hwE3ALq/PfuyVXhdXt4r8+QHyK7K2cXWCMLiTOPqODwTh2IDdD2CP12LwCnUKMankO8kfiAySd2SKgjCEfEEQ+nznsZc7eyLJA9zddPKZIx0c2NcHgMsL5MZhr83XULiTeCSXAEcG2m4PjPCXsEWWBdhbZ/4h6knN4u07Mxv4MbCojtxo7DW6RTRwopMFxt0xeoCJAblLvCDdlWpzRAG42CO2sET2UUfuVbetsYPF9mKq8zwg6Q8lsm7bRJxt8N0cAPdar5FUupYU9X03B2C782wknVUi+0nneacxZk9rXBpGABO8RXA72demJ7fcWyvubIe/TQN2y11MuJ6wA5v3z8HeMbjba+8n5StwJCDb9lYUEI/Fde3mEQ1svnBKRvp32K/LEPYQd1z3XQJfsG3/Sw/gKElLZev8tb8rnizpBEmF1SDZ06ZbJN0saa+kayQtV77qi6QnJF1njFnXdOebAcIXssvQB3yfcGrcCZwEnAfMC8mMKGArNUVT28VubF4/nyZflx8Jr8BVkr4tm83tzn5ek/S8pM2SnpT0gv8H283C/wGTFfhGtexQwQAAAABJRU5ErkJggg==&labelColor=305680)](https://builds.thingsboard.io/viewType.html?buildTypeId=ThingsBoard_Build&guest=1) [![ThingsBoard Builds Server Status](https://img.shields.io/teamcity/build/e/ThingsBoard_Build?label=TB%20builds%20server&server=https%3A%2F%2Fbuilds.thingsboard.io&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAALzAAAC8wHS6QoqAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAB9FJREFUeJzVm3+MXUUVx7+zWwqEtnRLWisQ2lKVUisIQmsqYCohpUhpEGsFKSJJTS0qGiGIISJ/8CNGYzSaEKBQEZUiP7RgVbCVdpE0xYKBWgI2rFLZJZQWtFKobPfjH3Pfdu7s3Pvmzntv3/JNNr3bOXPO+Z6ZO3PumVmjFgEYJWmWpDmSZks6VtIESV3Zv29LWmGMubdVPgw7gEOBJcAaYC/18fd2+zyqngAwXdL7M9keSduMMXgyH5R0laRPSRpbwf62CrLDB8AAS4HnAqP2EvA1YBTwPuBnwP46I70H+DPwALAS+B5wBTCu3VyHIJvG98dMX+B/BW1vAvcAnwdmAp3t5hWFbORXR5AvwmPARcCYdnNJAnCBR+gd7HQ9HZgLfAt4PUB8AzCv3f43DGCTQ6o/RAo43gtCL2Da4W9TAUwEBhxiPymRvcabAR8eTl+biQ7neYokdyTXlvR7xPt9etM8GmZ0FDxL+WD42FdBdkTDJd0jyU1wzi7pd473e0+qA8AM4AbgkrK1BDgOWAc8ChyTaq+eM5ud93ofcHpAZiY2sanhZaDDaTfAZ7HJUmlWCJzm6bqLQM6QBanXkfthcxgPNbTEW9z2AT8AzgTmANdikxwXX/d0XOi0bQEmFNj6GPAfhuKnXkB98kNsNjsITwacKkI3MNrrf4UnswXoiiRfwyqgo4D8L2hVZglMw456DDYCRwR0jCH/KuWCgE2oysjX8KsA+V+2jHzm3CrP4PMBx/4JfAU4qETP+EAQ/gKcA/w7gnwNbl5yD7bG0DLyM7DZXw3d2f9PA+YD5wIzK+gLBSEFA/XIA2cAVwLvbSQAt3mGP5Gs7IDO8dg1ZYDGcAfOwujZuIwDn+ObUx09hHx+v7Eh5nndCyIIDgBbgd0lMiv9IABfIF+LeDnVyU97xj5XR/6bwI5sZEaXyH2UuHd+WSbfRXktYjAIAfL9wGdSA/Cgo+gtSio12IKJa3hNKAgZ+TciyL+AlwECKzI/ioLgTvsa+YtTyXeSz8ZW15E3wN88p3JBwCZNMeShIKkBTsRmmSG4a0o/sDSJfGboBE/5pRF9pgI9oSBUJP8mXpLk2bm6pO9Aw+QzI8s8xVFbXRaEf3h911cgD7Cyjg0/L/GxnoLdoUoA3O1vDxUyLWyO4AehCpYX6D2L/LpUhtsaCkIWxRoeT+g/DVsqT8EWYDowC5jh6FxUUc+tJJblOmSPqWp4JUFHl6TDUoxLOlnSdknPSnK3sA2S9lfQs0zS7SkzwQ/A61U6A6dKWufpSMVg5mmMeUPSXyv2v0zSN6oa7ZAdwRqiA5CRf0TS+KpGAxiQ1OFN4z8l6PErVXUxSvmp1hvTqUnk35adPWskPWSM6fPaq84ASXqscg/gi9gcvJuC6o0nfwrhw5EYvIpNn88HStcN4M6KulfTys/lzKlO0lb8P2Lrf6VbLDAF+DLweEX998aSx372bwP6gPlVA3BEAvm9FJwVYtPqjwDXA08n6AZbOYoeeeAWp++mSlPGGLMLeFjSuRW6Iektx4GDJc2TdJ6khZKOruKDh/skXWSM6a/Q5yjn+dDKFrE1vw0VR2m2039x4kj7uJ+SslyJ/+7rtaly4mCM+a+kBaq2TbnVpfWy216jmCzpkIR+7kK/MymHNsbslX0NYoMweMpsjNklaWuKXQ9zJf2eOocvAbzHee5N/ojIgvBVxY3madh3v4b1iWZ/o3zw5kpaS+SFDGCq8jPguUQ/CmsCZfi403dhwjv/AHAQMAl41mvbGBMEhq4/c1PJTwmQr1f7u97pfzj5EnwUead/KAg/ivD7Zkf+HSBpFwiRfwibI3SXkOj29PgEivAggdU+C8JWR+6+CN9dm1tSyHcBLwbIj87ax1Kcxe0DJmVyY4CdEeR/TXnVeRLwc+C3wHF1fP+Qp/uGlABc6Cl5mPziVi8IzwDfAZ6KIN9LyhQt9v1GT/+sFCXTOVBBXuOTd+TGkp+eqWjKSTBwMPAvR+9TjSibjK35l93mWIxdZFKOxPzFseEgAJd7Olt6v+AC8jdIqwRhLbZM758HRH3tYa/vnoqtKZ4JHIk99tvh6HqNVl3RLSB/JfBEBPnBwxXsJ2uf176qxO7hwE3ALq/PfuyVXhdXt4r8+QHyK7K2cXWCMLiTOPqODwTh2IDdD2CP12LwCnUKMankO8kfiAySd2SKgjCEfEEQ+nznsZc7eyLJA9zddPKZIx0c2NcHgMsL5MZhr83XULiTeCSXAEcG2m4PjPCXsEWWBdhbZ/4h6knN4u07Mxv4MbCojtxo7DW6RTRwopMFxt0xeoCJAblLvCDdlWpzRAG42CO2sET2UUfuVbetsYPF9mKq8zwg6Q8lsm7bRJxt8N0cAPdar5FUupYU9X03B2C782wknVUi+0nneacxZk9rXBpGABO8RXA72demJ7fcWyvubIe/TQN2y11MuJ6wA5v3z8HeMbjba+8n5StwJCDb9lYUEI/Fde3mEQ1svnBKRvp32K/LEPYQd1z3XQJfsG3/Sw/gKElLZev8tb8rnizpBEmF1SDZ06ZbJN0saa+kayQtV77qi6QnJF1njFnXdOebAcIXssvQB3yfcGrcCZwEnAfMC8mMKGArNUVT28VubF4/nyZflx8Jr8BVkr4tm83tzn5ek/S8pM2SnpT0gv8H283C/wGTFfhGtexQwQAAAABJRU5ErkJggg==&labelColor=305680)](https://builds.thingsboard.io/viewType.html?buildTypeId=ThingsBoard_Build&guest=1)
ThingsBoard is an open-source IoT platform for data collection, processing, visualization, and device management. ThingsBoard is an open-source IoT platform for data collection, processing, visualization, and device management.

10
application/src/main/data/json/demo/dashboards/firmware.json

@ -279,7 +279,7 @@
"name": "Edit firmware", "name": "Edit firmware",
"icon": "edit", "icon": "edit",
"type": "customPretty", "type": "customPretty",
"customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>Edit firmware {{entityName}}</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content fxLayout=\"column\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"firmwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>", "customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar class=\"flex flex-row\" color=\"primary\">\n <h2>Edit firmware {{entityName}}</h2>\n <span class=\"flex-1\"></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content class=\"flex flex-col\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"firmwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions class=\"flex flex-row items-center justify-end\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>",
"customCss": "form {\n min-width: 300px !important;\n}", "customCss": "form {\n min-width: 300px !important;\n}",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n firmwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n firmwareId: vm.entity.firmwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.firmwareId = formValues.firmwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n firmwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n firmwareId: vm.entity.firmwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.firmwareId = formValues.firmwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}",
"customResources": [], "customResources": [],
@ -1142,7 +1142,7 @@
"name": "Edit firmware", "name": "Edit firmware",
"icon": "edit", "icon": "edit",
"type": "customPretty", "type": "customPretty",
"customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>Edit firmware {{entityName}}</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content fxLayout=\"column\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"firmwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>", "customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar class=\"flex flex-row\" color=\"primary\">\n <h2>Edit firmware {{entityName}}</h2>\n <span class=\"flex-1\"></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content class=\"flex flex-col\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"firmwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions class=\"flex flex-row items-center justify-end\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>",
"customCss": "form {\n min-width: 300px !important;\n}", "customCss": "form {\n min-width: 300px !important;\n}",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n firmwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n firmwareId: vm.entity.firmwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.firmwareId = formValues.firmwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n firmwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n firmwareId: vm.entity.firmwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.firmwareId = formValues.firmwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}",
"customResources": [], "customResources": [],
@ -1448,7 +1448,7 @@
"name": "Edit firmware", "name": "Edit firmware",
"icon": "edit", "icon": "edit",
"type": "customPretty", "type": "customPretty",
"customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>Edit firmware {{entityName}}</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content fxLayout=\"column\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"firmwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>", "customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar class=\"flex flex-row\" color=\"primary\">\n <h2>Edit firmware {{entityName}}</h2>\n <span class=\"flex-1\"></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content class=\"flex flex-col\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"firmwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions class=\"flex flex-row items-center justify-end\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>",
"customCss": "form {\n min-width: 300px !important;\n}", "customCss": "form {\n min-width: 300px !important;\n}",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n firmwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n firmwareId: vm.entity.firmwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.firmwareId = formValues.firmwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n firmwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n firmwareId: vm.entity.firmwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.firmwareId = formValues.firmwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}",
"customResources": [], "customResources": [],
@ -1754,7 +1754,7 @@
"name": "Edit firmware", "name": "Edit firmware",
"icon": "edit", "icon": "edit",
"type": "customPretty", "type": "customPretty",
"customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>Edit firmware {{entityName}}</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content fxLayout=\"column\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"firmwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>", "customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar class=\"flex flex-row\" color=\"primary\">\n <h2>Edit firmware {{entityName}}</h2>\n <span class=\"flex-1\"></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content class=\"flex flex-col\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"firmwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions class=\"flex flex-row items-center justify-end\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>",
"customCss": "form {\n min-width: 300px !important;\n}", "customCss": "form {\n min-width: 300px !important;\n}",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n firmwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n firmwareId: vm.entity.firmwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.firmwareId = formValues.firmwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n firmwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n firmwareId: vm.entity.firmwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.firmwareId = formValues.firmwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}",
"customResources": [], "customResources": [],
@ -2060,7 +2060,7 @@
"name": "Edit firmware", "name": "Edit firmware",
"icon": "edit", "icon": "edit",
"type": "customPretty", "type": "customPretty",
"customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>Edit firmware {{entityName}}</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content fxLayout=\"column\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"firmwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>", "customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar class=\"flex flex-row\" color=\"primary\">\n <h2>Edit firmware {{entityName}}</h2>\n <span class=\"flex-1\"></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content class=\"flex flex-col\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"firmwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions class=\"flex flex-row items-center justify-end\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>",
"customCss": "form {\n min-width: 300px !important;\n}", "customCss": "form {\n min-width: 300px !important;\n}",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n firmwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n firmwareId: vm.entity.firmwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.firmwareId = formValues.firmwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n firmwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n firmwareId: vm.entity.firmwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.firmwareId = formValues.firmwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}",
"customResources": [], "customResources": [],

10
application/src/main/data/json/demo/dashboards/software.json

@ -279,7 +279,7 @@
"name": "Edit software", "name": "Edit software",
"icon": "edit", "icon": "edit",
"type": "customPretty", "type": "customPretty",
"customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>Edit software {{entityName}}</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content fxLayout=\"column\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n type=\"SOFTWARE\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"softwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>", "customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar class=\"flex flex-row\" color=\"primary\">\n <h2>Edit software {{entityName}}</h2>\n <span class=\"flex-1\"></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content class=\"flex flex-col\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n type=\"SOFTWARE\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"softwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions class=\"flex flex-row items-center justify-end\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>",
"customCss": "form {\n min-width: 300px !important;\n}", "customCss": "form {\n min-width: 300px !important;\n}",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n softwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n softwareId: vm.entity.softwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.softwareId = formValues.softwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n softwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n softwareId: vm.entity.softwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.softwareId = formValues.softwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}",
"customResources": [], "customResources": [],
@ -1142,7 +1142,7 @@
"name": "Edit software", "name": "Edit software",
"icon": "edit", "icon": "edit",
"type": "customPretty", "type": "customPretty",
"customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>Edit software {{entityName}}</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content fxLayout=\"column\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n type=\"SOFTWARE\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"softwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>", "customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar class=\"flex flex-row\" color=\"primary\">\n <h2>Edit software {{entityName}}</h2>\n <span class=\"flex-1\"></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content class=\"flex flex-col\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n type=\"SOFTWARE\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"softwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions class=\"flex flex-row items-center justify-end\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>",
"customCss": "form {\n min-width: 300px !important;\n}", "customCss": "form {\n min-width: 300px !important;\n}",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n softwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n softwareId: vm.entity.softwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.softwareId = formValues.softwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n softwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n softwareId: vm.entity.softwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.softwareId = formValues.softwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}",
"customResources": [], "customResources": [],
@ -1448,7 +1448,7 @@
"name": "Edit software", "name": "Edit software",
"icon": "edit", "icon": "edit",
"type": "customPretty", "type": "customPretty",
"customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>Edit software {{entityName}}</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content fxLayout=\"column\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n type=\"SOFTWARE\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"softwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>", "customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar class=\"flex flex-row\" color=\"primary\">\n <h2>Edit software {{entityName}}</h2>\n <span class=\"flex-1\"></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content class=\"flex flex-col\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n type=\"SOFTWARE\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"softwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions class=\"flex flex-row items-center justify-end\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>",
"customCss": "form {\n min-width: 300px !important;\n}", "customCss": "form {\n min-width: 300px !important;\n}",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n softwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n softwareId: vm.entity.softwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.softwareId = formValues.softwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n softwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n softwareId: vm.entity.softwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.softwareId = formValues.softwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}",
"customResources": [], "customResources": [],
@ -1754,7 +1754,7 @@
"name": "Edit software", "name": "Edit software",
"icon": "edit", "icon": "edit",
"type": "customPretty", "type": "customPretty",
"customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>Edit software {{entityName}}</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content fxLayout=\"column\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n type=\"SOFTWARE\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"softwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>", "customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar class=\"flex flex-row\" color=\"primary\">\n <h2>Edit software {{entityName}}</h2>\n <span class=\"flex-1\"></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content class=\"flex flex-col\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n type=\"SOFTWARE\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"softwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions class=\"flex flex-row items-center justify-end\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>",
"customCss": "form {\n min-width: 300px !important;\n}", "customCss": "form {\n min-width: 300px !important;\n}",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n softwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n softwareId: vm.entity.softwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.softwareId = formValues.softwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n softwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n softwareId: vm.entity.softwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.softwareId = formValues.softwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}",
"customResources": [], "customResources": [],
@ -2060,7 +2060,7 @@
"name": "Edit software", "name": "Edit software",
"icon": "edit", "icon": "edit",
"type": "customPretty", "type": "customPretty",
"customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>Edit software {{entityName}}</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content fxLayout=\"column\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n type=\"SOFTWARE\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"softwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>", "customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar class=\"flex flex-row\" color=\"primary\">\n <h2>Edit software {{entityName}}</h2>\n <span class=\"flex-1\"></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div *ngIf=\"entity.deviceProfileId\" mat-dialog-content class=\"flex flex-col\">\n <tb-ota-package-autocomplete\n [useFullEntityId]=\"true\"\n type=\"SOFTWARE\"\n [deviceProfileId]=\"entity.deviceProfileId.id\"\n formControlName=\"softwareId\">\n </tb-ota-package-autocomplete>\n </div>\n <div mat-dialog-actions class=\"flex flex-row items-center justify-end\">\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n </div>\n</form>",
"customCss": "form {\n min-width: 300px !important;\n}", "customCss": "form {\n min-width: 300px !important;\n}",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n softwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n softwareId: vm.entity.softwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.softwareId = formValues.softwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n softwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n softwareId: vm.entity.softwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.softwareId = formValues.softwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}",
"customResources": [], "customResources": [],

4
application/src/main/data/json/demo/dashboards/thermostats.json

@ -160,7 +160,7 @@
"name": "Add", "name": "Add",
"icon": "add", "icon": "add",
"type": "customPretty", "type": "customPretty",
"customHtml": "<form #addEntityForm=\"ngForm\" [formGroup]=\"addEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"add-entity-form\">\n <mat-toolbar color=\"primary\">\n <h2>Add thermostat</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div mat-dialog-content fxLayout=\"column\">\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Thermostat name</mat-label>\n <input matInput formControlName=\"entityName\" required>\n <mat-error *ngIf=\"addEntityFormGroup.get('entityName').hasError('required')\">\n Thermostat name is required.\n </mat-error>\n </mat-form-field>\n <div formGroupName=\"attributes\" fxLayout=\"column\">\n <mat-slide-toggle formControlName=\"temperatureAlarmFlag\">\n High temperature alarm\n </mat-slide-toggle>\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>High temperature threshold, °C</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"addEntityFormGroup.get('attributes').get('temperatureAlarmFlag').value\"\n formControlName=\"temperatureAlarmThreshold\">\n <mat-error *ngIf=\"addEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').hasError('required')\">\n High temperature threshold is required.\n </mat-error>\n </mat-form-field>\n \n <mat-slide-toggle formControlName=\"humidityAlarmFlag\">\n Low humidity alarm\n </mat-slide-toggle>\n \n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Low humidity threshold, %</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"addEntityFormGroup.get('attributes').get('humidityAlarmFlag').value\"\n formControlName=\"humidityAlarmThreshold\">\n <mat-error *ngIf=\"addEntityFormGroup.get('attributes').get('humidityAlarmThreshold').hasError('required')\">\n Low humidity threshold is required.\n </mat-error>\n </mat-form-field>\n </div>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty\">\n Create\n </button>\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n </div>\n</form>", "customHtml": "<form #addEntityForm=\"ngForm\" [formGroup]=\"addEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"add-entity-form\">\n <mat-toolbar color=\"primary\">\n <h2>Add thermostat</h2>\n <span class=\"flex-1\"></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div mat-dialog-content class=\"flex flex-col\">\n <mat-form-field class=\"mat-block flex-1\">\n <mat-label>Thermostat name</mat-label>\n <input matInput formControlName=\"entityName\" required>\n <mat-error *ngIf=\"addEntityFormGroup.get('entityName').hasError('required')\">\n Thermostat name is required.\n </mat-error>\n </mat-form-field>\n <div formGroupName=\"attributes\" class=\"flex flex-col\">\n <mat-slide-toggle formControlName=\"temperatureAlarmFlag\">\n High temperature alarm\n </mat-slide-toggle>\n <mat-form-field class=\"mat-block flex-1\">\n <mat-label>High temperature threshold, °C</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"addEntityFormGroup.get('attributes').get('temperatureAlarmFlag').value\"\n formControlName=\"temperatureAlarmThreshold\">\n <mat-error *ngIf=\"addEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').hasError('required')\">\n High temperature threshold is required.\n </mat-error>\n </mat-form-field>\n\n <mat-slide-toggle formControlName=\"humidityAlarmFlag\">\n Low humidity alarm\n </mat-slide-toggle>\n\n <mat-form-field class=\"mat-block flex-1\">\n <mat-label>Low humidity threshold, %</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"addEntityFormGroup.get('attributes').get('humidityAlarmFlag').value\"\n formControlName=\"humidityAlarmThreshold\">\n <mat-error *ngIf=\"addEntityFormGroup.get('attributes').get('humidityAlarmThreshold').hasError('required')\">\n Low humidity threshold is required.\n </mat-error>\n </mat-form-field>\n </div>\n </div>\n <div mat-dialog-actions class=\"flex flex-row items-center justify-end\">\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty\">\n Create\n </button>\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n </div>\n</form>",
"customCss": ".add-entity-form{\n width: 300px;\n}\n", "customCss": ".add-entity-form{\n width: 300px;\n}\n",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenAddEntityDialog();\n\nfunction openAddEntityDialog() {\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\n}\n\nfunction AddEntityDialogController(instance) {\n let vm = instance;\n \n vm.addEntityFormGroup = vm.fb.group({\n entityName: ['', [vm.validators.required]],\n attributes: vm.fb.group({\n temperatureAlarmFlag: [false],\n temperatureAlarmThreshold: [{value: null, disabled: true}],\n humidityAlarmFlag: [false],\n humidityAlarmThreshold: [{value: null, disabled: true}]\n })\n });\n \n vm.addEntityFormGroup.get('attributes').get('temperatureAlarmFlag').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.addEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').enable();\n } else {\n vm.addEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').disable();\n }\n });\n \n vm.addEntityFormGroup.get('attributes').get('humidityAlarmFlag').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.addEntityFormGroup.get('attributes').get('humidityAlarmThreshold').enable();\n } else {\n vm.addEntityFormGroup.get('attributes').get('humidityAlarmThreshold').disable();\n }\n });\n\n vm.save = function() {\n vm.addEntityFormGroup.markAsPristine();\n saveEntityObservable().subscribe(\n function (entity) {\n saveAttributes(entity.id).subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n function saveEntityObservable() {\n const formValues = vm.addEntityFormGroup.value;\n let entity = {\n name: formValues.entityName,\n type: \"thermostat\"\n };\n return deviceService.saveDevice(entity);\n }\n \n function saveAttributes(entityId) {\n let attributes = vm.addEntityFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n if(attributes[key] !== null) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenAddEntityDialog();\n\nfunction openAddEntityDialog() {\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\n}\n\nfunction AddEntityDialogController(instance) {\n let vm = instance;\n \n vm.addEntityFormGroup = vm.fb.group({\n entityName: ['', [vm.validators.required]],\n attributes: vm.fb.group({\n temperatureAlarmFlag: [false],\n temperatureAlarmThreshold: [{value: null, disabled: true}],\n humidityAlarmFlag: [false],\n humidityAlarmThreshold: [{value: null, disabled: true}]\n })\n });\n \n vm.addEntityFormGroup.get('attributes').get('temperatureAlarmFlag').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.addEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').enable();\n } else {\n vm.addEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').disable();\n }\n });\n \n vm.addEntityFormGroup.get('attributes').get('humidityAlarmFlag').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.addEntityFormGroup.get('attributes').get('humidityAlarmThreshold').enable();\n } else {\n vm.addEntityFormGroup.get('attributes').get('humidityAlarmThreshold').disable();\n }\n });\n\n vm.save = function() {\n vm.addEntityFormGroup.markAsPristine();\n saveEntityObservable().subscribe(\n function (entity) {\n saveAttributes(entity.id).subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n function saveEntityObservable() {\n const formValues = vm.addEntityFormGroup.value;\n let entity = {\n name: formValues.entityName,\n type: \"thermostat\"\n };\n return deviceService.saveDevice(entity);\n }\n \n function saveAttributes(entityId) {\n let attributes = vm.addEntityFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n if(attributes[key] !== null) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}",
"customResources": [], "customResources": [],
@ -180,7 +180,7 @@
"name": "Edit", "name": "Edit",
"icon": "edit", "icon": "edit",
"type": "customPretty", "type": "customPretty",
"customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar color=\"primary\">\n <h2>Edit thermostat {{entityName}}</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div mat-dialog-content fxLayout=\"column\">\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Thermostat name</mat-label>\n <input matInput formControlName=\"entityName\" readonly>\n </mat-form-field>\n <div formGroupName=\"attributes\" fxLayout=\"column\">\n <mat-slide-toggle formControlName=\"temperatureAlarmFlag\">\n High temperature alarm\n </mat-slide-toggle>\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>High temperature threshold, °C</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"editEntityFormGroup.get('attributes').get('temperatureAlarmFlag').value\"\n formControlName=\"temperatureAlarmThreshold\">\n <mat-error *ngIf=\"editEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').hasError('required')\">\n High temperature threshold is required.\n </mat-error>\n </mat-form-field>\n\n <mat-slide-toggle formControlName=\"humidityAlarmFlag\">\n Low humidity alarm\n </mat-slide-toggle>\n\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Low humidity threshold, %</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"editEntityFormGroup.get('attributes').get('humidityAlarmFlag').value\"\n formControlName=\"humidityAlarmThreshold\">\n <mat-error *ngIf=\"editEntityFormGroup.get('attributes').get('humidityAlarmThreshold').hasError('required')\">\n Low humidity threshold is required.\n </mat-error>\n </mat-form-field>\n </div>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n </div>\n</form>", "customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar color=\"primary\">\n <h2>Edit thermostat {{entityName}}</h2>\n <span class=\"flex-1\"></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div mat-dialog-content class=\"flex flex-col\">\n <mat-form-field class=\"mat-block flex-1\">\n <mat-label>Thermostat name</mat-label>\n <input matInput formControlName=\"entityName\" readonly>\n </mat-form-field>\n <div formGroupName=\"attributes\" class=\"flex flex-col\">\n <mat-slide-toggle formControlName=\"temperatureAlarmFlag\">\n High temperature alarm\n </mat-slide-toggle>\n <mat-form-field class=\"mat-block flex-1\">\n <mat-label>High temperature threshold, °C</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"editEntityFormGroup.get('attributes').get('temperatureAlarmFlag').value\"\n formControlName=\"temperatureAlarmThreshold\">\n <mat-error *ngIf=\"editEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').hasError('required')\">\n High temperature threshold is required.\n </mat-error>\n </mat-form-field>\n\n <mat-slide-toggle formControlName=\"humidityAlarmFlag\">\n Low humidity alarm\n </mat-slide-toggle>\n\n <mat-form-field class=\"mat-block flex-1\">\n <mat-label>Low humidity threshold, %</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"editEntityFormGroup.get('attributes').get('humidityAlarmFlag').value\"\n formControlName=\"humidityAlarmThreshold\">\n <mat-error *ngIf=\"editEntityFormGroup.get('attributes').get('humidityAlarmThreshold').hasError('required')\">\n Low humidity threshold is required.\n </mat-error>\n </mat-form-field>\n </div>\n </div>\n <div mat-dialog-actions class=\"flex flex-row items-center justify-end\">\n <button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n </div>\n</form>",
"customCss": ".edit-entity-form{\n width: 300px;\n}", "customCss": ".edit-entity-form{\n width: 300px;\n}",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n \n vm.entityId = entityId;\n vm.entityName = entityName;\n vm.attributes = {};\n \n vm.editEntityFormGroup = vm.fb.group({\n entityName: [''],\n attributes: vm.fb.group({\n temperatureAlarmFlag: [false],\n temperatureAlarmThreshold: [{value: null, disabled: true}],\n humidityAlarmFlag: [false],\n humidityAlarmThreshold: [{value: null, disabled: true}]\n })\n });\n \n vm.editEntityFormGroup.get('attributes').get('temperatureAlarmFlag').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.editEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').enable();\n } else {\n vm.editEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').disable();\n }\n });\n \n vm.editEntityFormGroup.get('attributes').get('humidityAlarmFlag').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.editEntityFormGroup.get('attributes').get('humidityAlarmThreshold').enable();\n } else {\n vm.editEntityFormGroup.get('attributes').get('humidityAlarmThreshold').disable();\n }\n });\n \n \n getEntityInfo();\n \n \n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveAttributes(entityId).subscribe(\n function () {\n vm.dialogRef.close(null);\n }\n );\n };\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n function getEntityAttributes(attributes) {\n for (var i = 0; i < attributes.length; i++) {\n vm.attributes[attributes[i].key] = attributes[i].value;\n }\n }\n \n function getEntityInfo() {\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE').subscribe(\n function (attributes) {\n getEntityAttributes(attributes);\n vm.editEntityFormGroup.patchValue({\n entityName: vm.entityName,\n attributes: vm.attributes\n });\n // if(vm.attributes.temperatureAlarmFlag) {\n // vm.editEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').enable();\n // }\n // if(vm.attributes.humidityAlarmFlag) {\n // vm.editEntityFormGroup.get('attributes').get('humidityAlarmThreshold').enable();\n // }\n }\n );\n }\n \n function saveAttributes(entityId) {\n let attributes = vm.editEntityFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n if (attributes[key] !== vm.attributes[key]) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n \n vm.entityId = entityId;\n vm.entityName = entityName;\n vm.attributes = {};\n \n vm.editEntityFormGroup = vm.fb.group({\n entityName: [''],\n attributes: vm.fb.group({\n temperatureAlarmFlag: [false],\n temperatureAlarmThreshold: [{value: null, disabled: true}],\n humidityAlarmFlag: [false],\n humidityAlarmThreshold: [{value: null, disabled: true}]\n })\n });\n \n vm.editEntityFormGroup.get('attributes').get('temperatureAlarmFlag').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.editEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').enable();\n } else {\n vm.editEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').disable();\n }\n });\n \n vm.editEntityFormGroup.get('attributes').get('humidityAlarmFlag').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.editEntityFormGroup.get('attributes').get('humidityAlarmThreshold').enable();\n } else {\n vm.editEntityFormGroup.get('attributes').get('humidityAlarmThreshold').disable();\n }\n });\n \n \n getEntityInfo();\n \n \n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveAttributes(entityId).subscribe(\n function () {\n vm.dialogRef.close(null);\n }\n );\n };\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n function getEntityAttributes(attributes) {\n for (var i = 0; i < attributes.length; i++) {\n vm.attributes[attributes[i].key] = attributes[i].value;\n }\n }\n \n function getEntityInfo() {\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE').subscribe(\n function (attributes) {\n getEntityAttributes(attributes);\n vm.editEntityFormGroup.patchValue({\n entityName: vm.entityName,\n attributes: vm.attributes\n });\n // if(vm.attributes.temperatureAlarmFlag) {\n // vm.editEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').enable();\n // }\n // if(vm.attributes.humidityAlarmFlag) {\n // vm.editEntityFormGroup.get('attributes').get('humidityAlarmThreshold').enable();\n // }\n }\n );\n }\n \n function saveAttributes(entityId) {\n let attributes = vm.editEntityFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n if (attributes[key] !== vm.attributes[key]) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}",
"customResources": [], "customResources": [],

22
application/src/main/data/json/edge/instructions/install/centos/instructions.md

@ -56,13 +56,13 @@ sudo yum update
sudo yum -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm sudo yum -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm
# Install packages # Install packages
sudo yum -y install epel-release yum-utils sudo yum -y install epel-release yum-utils
sudo yum-config-manager --enable pgdg15 sudo yum-config-manager --enable pgdg16
sudo yum install postgresql15-server postgresql15 sudo yum install postgresql16-server postgresql16 postgresql16-contrib
# Initialize your PostgreSQL DB # Initialize your PostgreSQL DB
sudo /usr/pgsql-15/bin/postgresql-15-setup initdb sudo /usr/pgsql-16/bin/postgresql-16-setup initdb
sudo systemctl start postgresql-15 sudo systemctl start postgresql-16
# Optional: Configure PostgreSQL to start on boot # Optional: Configure PostgreSQL to start on boot
sudo systemctl enable --now postgresql-15 sudo systemctl enable --now postgresql-16
{:copy-code} {:copy-code}
``` ```
@ -74,12 +74,12 @@ sudo systemctl enable --now postgresql-15
sudo yum -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm sudo yum -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm
# Install packages # Install packages
sudo dnf -qy module disable postgresql sudo dnf -qy module disable postgresql
sudo dnf -y install postgresql15 postgresql15-server sudo dnf -y install postgresql16 postgresql16-server postgresql16-contrib
# Initialize your PostgreSQL DB # Initialize your PostgreSQL DB
sudo /usr/pgsql-15/bin/postgresql-15-setup initdb sudo /usr/pgsql-16/bin/postgresql-16-setup initdb
sudo systemctl start postgresql-15 sudo systemctl start postgresql-16
# Optional: Configure PostgreSQL to start on boot # Optional: Configure PostgreSQL to start on boot
sudo systemctl enable --now postgresql-15 sudo systemctl enable --now postgresql-16
{:copy-code} {:copy-code}
``` ```
@ -101,7 +101,7 @@ After configuring the password, edit the pg_hba.conf to use MD5 authentication w
Edit pg_hba.conf file: Edit pg_hba.conf file:
```bash ```bash
sudo nano /var/lib/pgsql/15/data/pg_hba.conf sudo nano /var/lib/pgsql/16/data/pg_hba.conf
{:copy-code} {:copy-code}
``` ```
@ -121,7 +121,7 @@ host all all 127.0.0.1/32 md5
Finally, you should restart the PostgreSQL service to initialize the new configuration: Finally, you should restart the PostgreSQL service to initialize the new configuration:
```bash ```bash
sudo systemctl restart postgresql-15.service sudo systemctl restart postgresql-16.service
{:copy-code} {:copy-code}
``` ```

2
application/src/main/data/json/edge/instructions/install/docker/instructions.md

@ -38,7 +38,7 @@ services:
${EXTRA_HOSTS} ${EXTRA_HOSTS}
postgres: postgres:
restart: always restart: always
image: "postgres:15" image: "postgres:16"
ports: ports:
- "5432" - "5432"
environment: environment:

2
application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md

@ -49,7 +49,7 @@ echo "deb http://apt.postgresql.org/pub/repos/apt/ ${RELEASE}"-pgdg main | sudo
# install and launch the postgresql service: # install and launch the postgresql service:
sudo apt update sudo apt update
sudo apt -y install postgresql-15 sudo apt -y install postgresql-16
sudo service postgresql start sudo service postgresql start
{:copy-code} {:copy-code}
``` ```

2
application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md

@ -21,7 +21,7 @@ services:
entrypoint: upgrade-tb-edge.sh entrypoint: upgrade-tb-edge.sh
postgres: postgres:
restart: always restart: always
image: "postgres:15" image: "postgres:16"
ports: ports:
- "5432" - "5432"
environment: environment:

2
application/src/main/data/json/system/widget_types/asset_admin_table.json

File diff suppressed because one or more lines are too long

4
application/src/main/data/json/system/widget_types/basic_gpio_control.json

File diff suppressed because one or more lines are too long

4
application/src/main/data/json/system/widget_types/basic_gpio_panel.json

@ -9,8 +9,8 @@
"sizeX": 5, "sizeX": 5,
"sizeY": 2, "sizeY": 2,
"resources": [], "resources": [],
"templateHtml": "<div class=\"gpio-panel\" style=\"height: 100%;\">\n <section fxLayout=\"row\" *ngFor=\"let row of rows\">\n <section fxFlex fxLayout=\"row\" *ngFor=\"let cell of row; let $index = index\">\n <section fxLayout=\"row\" fxFlex *ngIf=\"cell\" fxLayoutAlign=\"{{$index===0 ? 'end center' : 'start center'}}\">\n <span class=\"gpio-left-label\" [fxShow]=\"$index===0\">{{ cell.label }}</span>\n <section fxLayout=\"row\" class=\"led-panel\" [ngClass]=\"$index===0 ? 'col-0' : 'col-1'\"\n [ngStyle]=\"{backgroundColor: ledPanelBackgroundColor}\">\n <span class=\"pin\" [fxShow]=\"$index===0\">{{cell.pin}}</span>\n <span class=\"led-container\">\n <tb-led-light [size]=\"prefferedRowHeight\"\n [colorOn]=\"cell.colorOn\"\n [colorOff]=\"cell.colorOff\"\n [offOpacity]=\"'0.9'\"\n [enabled]=\"cell.enabled\">\n </tb-led-light>\n </span>\n <span class=\"pin\" [fxShow]=\"$index===1\">{{cell.pin}}</span>\n </section>\n <span class=\"gpio-right-label\" [fxShow]=\"$index===1\">{{ cell.label }}</span>\n </section>\n <section fxLayout=\"row\" fxFlex *ngIf=\"!cell\">\n <span fxFlex [fxShow]=\"$index===0\"></span>\n <span class=\"led-panel\"\n [ngStyle]=\"{height: prefferedRowHeight+'px', backgroundColor: ledPanelBackgroundColor}\"></span>\n <span fxFlex [fxShow]=\"$index===1\"></span>\n </section>\n </section>\n </section> \n</div>", "templateHtml": "<div class=\"gpio-panel\" style=\"height: 100%;\">\n <section class=\"flex flex-row\" *ngFor=\"let row of rows\">\n <section class=\"flex flex-1 flex-row\" *ngFor=\"let cell of row; let $index = index\">\n <section class=\"flex flex-1 flex-row\" [class.justify-end]=\"$index===0\" [class.justify-start]=\"$index!==0\" *ngIf=\"cell\">\n <span class=\"gpio-left-label\" [class.!hidden]=\"$index!==0\">{{ cell.label }}</span>\n <section class=\"led-panel flex flex-row\" [class.col-0]=\"$index===0\" [class.col-1]=\"$index!==0\"\n [style.background-color]=\"ledPanelBackgroundColor\">\n <span class=\"pin\" [class.!hidden]=\"$index!==0\">{{cell.pin}}</span>\n <span class=\"led-container\">\n <tb-led-light [size]=\"prefferedRowHeight\"\n [colorOn]=\"cell.colorOn\"\n [colorOff]=\"cell.colorOff\"\n [offOpacity]=\"'0.9'\"\n [enabled]=\"cell.enabled\">\n </tb-led-light>\n </span>\n <span class=\"pin\" [class.!hidden]=\"$index!==1\">{{cell.pin}}</span>\n </section>\n <span class=\"gpio-right-label\" [class.!hidden]=\"$index!==1\">{{ cell.label }}</span>\n </section>\n <section class=\"flex flex-1 flex-row\" *ngIf=\"!cell\">\n <span class=\"flex-1\" [class.!hidden]=\"$index!==0\"></span>\n <span class=\"led-panel\"\n [style.height.px]=\"prefferedRowHeight\"\n [style.background-color]=\"ledPanelBackgroundColor\"></span>\n <span class=\"flex-1\" [class.!hidden]=\"$index!==1\"></span>\n </section>\n </section>\n </section>\n</div>",
"templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n\n.gpio-panel tb-led-light > div {\n margin: auto;\n}\n\n.led-panel {\n margin: 0;\n width: 66px;\n min-width: 66px;\n}\n\n.led-container {\n width: 48px;\n min-width: 48px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.led-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.led-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section.flex-1 {\n min-width: 0px;\n}\n\n\n.gpio-panel tb-led-light > div {\n margin: auto;\n}\n\n.led-panel {\n margin: 0;\n width: 66px;\n min-width: 66px;\n}\n\n.led-container {\n width: 48px;\n min-width: 48px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.led-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.led-panel.col-1 .pin {\n margin-right: auto;\n\n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}",
"controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-panel-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n var i, gpio;\n \n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n \n scope.gpioList = [];\n scope.gpioByPin = {};\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false,\n colorOn: tinycolor(gpio.color).lighten(20).toHexString(),\n colorOff: tinycolor(gpio.color).darken().toHexString()\n }\n );\n scope.gpioByPin[gpio.pin] = scope.gpioList[scope.gpioList.length-1];\n }\n\n scope.ledPanelBackgroundColor = settings.ledPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var changed = false;\n for (var d = 0; d < self.ctx.data.length; d++) {\n var cellData = self.ctx.data[d];\n var dataKey = cellData.dataKey;\n var gpio = self.ctx.$scope.gpioByPin[dataKey.label];\n if (gpio) {\n var enabled = false;\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n enabled = (tvPair[1] === true || tvPair[1] === 'true');\n }\n if (gpio.enabled != enabled) {\n changed = true;\n gpio.enabled = enabled;\n }\n }\n }\n if (changed) {\n self.ctx.detectChanges();\n } \n}\n\nself.onResize = function() {\n var rowCount = self.ctx.$scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n self.ctx.$scope.prefferedRowHeight = prefferedRowHeight;\n \n var ratio = prefferedRowHeight/32;\n \n var css = '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n \n cssParser.createStyleElement(namespace, css); \n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-panel-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n var i, gpio;\n \n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n \n scope.gpioList = [];\n scope.gpioByPin = {};\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false,\n colorOn: tinycolor(gpio.color).lighten(20).toHexString(),\n colorOff: tinycolor(gpio.color).darken().toHexString()\n }\n );\n scope.gpioByPin[gpio.pin] = scope.gpioList[scope.gpioList.length-1];\n }\n\n scope.ledPanelBackgroundColor = settings.ledPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var changed = false;\n for (var d = 0; d < self.ctx.data.length; d++) {\n var cellData = self.ctx.data[d];\n var dataKey = cellData.dataKey;\n var gpio = self.ctx.$scope.gpioByPin[dataKey.label];\n if (gpio) {\n var enabled = false;\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n enabled = (tvPair[1] === true || tvPair[1] === 'true');\n }\n if (gpio.enabled != enabled) {\n changed = true;\n gpio.enabled = enabled;\n }\n }\n }\n if (changed) {\n self.ctx.detectChanges();\n } \n}\n\nself.onResize = function() {\n var rowCount = self.ctx.$scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n self.ctx.$scope.prefferedRowHeight = prefferedRowHeight;\n \n var ratio = prefferedRowHeight/32;\n \n var css = '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n \n cssParser.createStyleElement(namespace, css); \n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",

2
application/src/main/data/json/system/widget_types/device_admin_table.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/device_claiming_widget.json

@ -9,7 +9,7 @@
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 4.5, "sizeY": 4.5,
"resources": [], "resources": [],
"templateHtml": "<form *ngIf=\"claimDeviceFormGroup\" #claimDeviceForm=\"ngForm\" [formGroup]=\"claimDeviceFormGroup\"\n tb-toast toastTarget=\"{{ toastTargetId }}\"\n class=\"claim-form\" (ngSubmit)=\"claim(claimDeviceForm)\">\n <fieldset [disabled]=\"(isLoading$ | async) || loading\">\n <mat-form-field class=\"mat-block\">\n <mat-label *ngIf=\"showLabel\">{{deviceLabel}}</mat-label>\n <input matInput formControlName=\"deviceName\" required>\n <mat-error *ngIf=\"claimDeviceFormGroup.get('deviceName').hasError('required')\">\n {{requiredErrorDevice}}\n </mat-error>\n </mat-form-field>\n <mat-form-field *ngIf=\"secretKeyField\" class=\"mat-block\">\n <mat-label *ngIf=\"showLabel\">{{secretKeyLabel}}</mat-label>\n <input matInput formControlName=\"deviceSecret\" required>\n <mat-error *ngIf=\"claimDeviceFormGroup.get('deviceSecret').hasError('required')\">\n {{requiredErrorSecretKey}}\n </mat-error>\n </mat-form-field>\n </fieldset>\n <div class=\"mat-block\" fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\" [disabled]=\"(isLoading$ | async) || claimDeviceForm.invalid || !claimDeviceForm.dirty\">\n {{labelClaimButon}}\n </button>\n </div>\n</form>\n", "templateHtml": "<form *ngIf=\"claimDeviceFormGroup\" #claimDeviceForm=\"ngForm\" [formGroup]=\"claimDeviceFormGroup\"\n tb-toast toastTarget=\"{{ toastTargetId }}\"\n class=\"claim-form\" (ngSubmit)=\"claim(claimDeviceForm)\">\n <fieldset [disabled]=\"(isLoading$ | async) || loading\">\n <mat-form-field class=\"mat-block\">\n <mat-label *ngIf=\"showLabel\">{{deviceLabel}}</mat-label>\n <input matInput formControlName=\"deviceName\" required>\n <mat-error *ngIf=\"claimDeviceFormGroup.get('deviceName').hasError('required')\">\n {{requiredErrorDevice}}\n </mat-error>\n </mat-form-field>\n <mat-form-field *ngIf=\"secretKeyField\" class=\"mat-block\">\n <mat-label *ngIf=\"showLabel\">{{secretKeyLabel}}</mat-label>\n <input matInput formControlName=\"deviceSecret\" required>\n <mat-error *ngIf=\"claimDeviceFormGroup.get('deviceSecret').hasError('required')\">\n {{requiredErrorSecretKey}}\n </mat-error>\n </mat-form-field>\n </fieldset>\n <div class=\"mat-block flex flex-row items-center justify-end\">\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\" [disabled]=\"(isLoading$ | async) || claimDeviceForm.invalid || !claimDeviceForm.dirty\">\n {{labelClaimButon}}\n </button>\n </div>\n</form>\n",
"templateCss": ".claim-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n", "templateCss": ".claim-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n",
"controllerScript": "let $scope;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n}\n\nfunction init() {\n $scope = self.ctx.$scope;\n let $injector = $scope.$injector;\n let utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n let $translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n let deviceService = $scope.$injector.get(self.ctx.servicesMap.get('deviceService'));\n let settings = self.ctx.settings || {};\n \n $scope.toastTargetId = 'device-claiming-widget' + utils.guid();\n $scope.secretKeyField = settings.deviceSecret;\n $scope.showLabel = settings.showLabel;\n\n let titleTemplate = \"\";\n let successfulClaim = utils.customTranslation(settings.successfulClaimDevice, settings.successfulClaimDevice) || $translate.instant('widgets.input-widgets.claim-successful');\n let failedClaimDevice = utils.customTranslation(settings.failedClaimDevice, settings.failedClaimDevice) || $translate.instant('widgets.input-widgets.claim-failed');\n let deviceNotFound = utils.customTranslation(settings.deviceNotFound, settings.deviceNotFound) || $translate.instant('widgets.input-widgets.claim-not-found');\n \n if (settings.widgetTitle && settings.widgetTitle.length) {\n titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n titleTemplate = self.ctx.widgetConfig.title;\n }\n self.ctx.widgetTitle = titleTemplate;\n \n $scope.deviceLabel = utils.customTranslation(settings.deviceLabel, settings.deviceLabel) || $translate.instant('widgets.input-widgets.device-name');\n $scope.requiredErrorDevice= utils.customTranslation(settings.requiredErrorDevice, settings.requiredErrorDevice) || $translate.instant('widgets.input-widgets.device-name-required');\n \n $scope.secretKeyLabel = utils.customTranslation(settings.secretKeyLabel, settings.secretKeyLabel) || $translate.instant('widgets.input-widgets.secret-key');\n $scope.requiredErrorSecretKey= utils.customTranslation(settings.requiredErrorSecretKey, settings.requiredErrorSecretKey) || $translate.instant('widgets.input-widgets.secret-key-required');\n \n $scope.labelClaimButon = utils.customTranslation(settings.labelClaimButon, settings.labelClaimButon) || $translate.instant('widgets.input-widgets.claim-device');\n \n $scope.claimDeviceFormGroup = $scope.fb.group(\n {deviceName: ['', [$scope.validators.required]]}\n );\n if ($scope.secretKeyField) {\n $scope.claimDeviceFormGroup.addControl('deviceSecret', $scope.fb.control('', [$scope.validators.required]));\n }\n \n $scope.claim = function(claimDeviceForm) {\n $scope.loading = true;\n\n let deviceName = $scope.claimDeviceFormGroup.get('deviceName').value;\n let claimRequest = {};\n if ($scope.secretKeyField) {\n claimRequest.secretKey = $scope.claimDeviceFormGroup.get('deviceSecret').value;\n }\n deviceService.claimDevice(deviceName, claimRequest, { ignoreErrors: true }).subscribe(\n function (data) {\n successClaim(claimDeviceForm);\n self.ctx.detectChanges();\n },\n function (error) {\n $scope.loading = false;\n if(error.status == 404) {\n $scope.showErrorToast(deviceNotFound, 'bottom', 'left', $scope.toastTargetId);\n } else {\n let errorMessage = failedClaimDevice;\n if (error.status !== 400) {\n if (error.error && error.error.message) {\n errorMessage = error.error.message;\n }\n }\n $scope.showErrorToast(errorMessage, 'bottom', 'left', $scope.toastTargetId);\n } \n self.ctx.detectChanges();\n }\n );\n }\n\n function successClaim(claimDeviceForm) {\n let deviceObj = {\n deviceName: ''\n };\n if ($scope.secretKeyField) {\n deviceObj.deviceSecret = '';\n } \n claimDeviceForm.resetForm(); \n $scope.claimDeviceFormGroup.reset(deviceObj);\n $scope.loading = false;\n $scope.showSuccessToast(successfulClaim, 2000, 'bottom', 'left', $scope.toastTargetId);\n self.ctx.updateAliases();\n }\n \n}\n", "controllerScript": "let $scope;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges(true);\n });\n}\n\nfunction init() {\n $scope = self.ctx.$scope;\n let $injector = $scope.$injector;\n let utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n let $translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n let deviceService = $scope.$injector.get(self.ctx.servicesMap.get('deviceService'));\n let settings = self.ctx.settings || {};\n \n $scope.toastTargetId = 'device-claiming-widget' + utils.guid();\n $scope.secretKeyField = settings.deviceSecret;\n $scope.showLabel = settings.showLabel;\n\n let titleTemplate = \"\";\n let successfulClaim = utils.customTranslation(settings.successfulClaimDevice, settings.successfulClaimDevice) || $translate.instant('widgets.input-widgets.claim-successful');\n let failedClaimDevice = utils.customTranslation(settings.failedClaimDevice, settings.failedClaimDevice) || $translate.instant('widgets.input-widgets.claim-failed');\n let deviceNotFound = utils.customTranslation(settings.deviceNotFound, settings.deviceNotFound) || $translate.instant('widgets.input-widgets.claim-not-found');\n \n if (settings.widgetTitle && settings.widgetTitle.length) {\n titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n titleTemplate = self.ctx.widgetConfig.title;\n }\n self.ctx.widgetTitle = titleTemplate;\n \n $scope.deviceLabel = utils.customTranslation(settings.deviceLabel, settings.deviceLabel) || $translate.instant('widgets.input-widgets.device-name');\n $scope.requiredErrorDevice= utils.customTranslation(settings.requiredErrorDevice, settings.requiredErrorDevice) || $translate.instant('widgets.input-widgets.device-name-required');\n \n $scope.secretKeyLabel = utils.customTranslation(settings.secretKeyLabel, settings.secretKeyLabel) || $translate.instant('widgets.input-widgets.secret-key');\n $scope.requiredErrorSecretKey= utils.customTranslation(settings.requiredErrorSecretKey, settings.requiredErrorSecretKey) || $translate.instant('widgets.input-widgets.secret-key-required');\n \n $scope.labelClaimButon = utils.customTranslation(settings.labelClaimButon, settings.labelClaimButon) || $translate.instant('widgets.input-widgets.claim-device');\n \n $scope.claimDeviceFormGroup = $scope.fb.group(\n {deviceName: ['', [$scope.validators.required]]}\n );\n if ($scope.secretKeyField) {\n $scope.claimDeviceFormGroup.addControl('deviceSecret', $scope.fb.control('', [$scope.validators.required]));\n }\n \n $scope.claim = function(claimDeviceForm) {\n $scope.loading = true;\n\n let deviceName = $scope.claimDeviceFormGroup.get('deviceName').value;\n let claimRequest = {};\n if ($scope.secretKeyField) {\n claimRequest.secretKey = $scope.claimDeviceFormGroup.get('deviceSecret').value;\n }\n deviceService.claimDevice(deviceName, claimRequest, { ignoreErrors: true }).subscribe(\n function (data) {\n successClaim(claimDeviceForm);\n self.ctx.detectChanges();\n },\n function (error) {\n $scope.loading = false;\n if(error.status == 404) {\n $scope.showErrorToast(deviceNotFound, 'bottom', 'left', $scope.toastTargetId);\n } else {\n let errorMessage = failedClaimDevice;\n if (error.status !== 400) {\n if (error.error && error.error.message) {\n errorMessage = error.error.message;\n }\n }\n $scope.showErrorToast(errorMessage, 'bottom', 'left', $scope.toastTargetId);\n } \n self.ctx.detectChanges();\n }\n );\n }\n\n function successClaim(claimDeviceForm) {\n let deviceObj = {\n deviceName: ''\n };\n if ($scope.secretKeyField) {\n deviceObj.deviceSecret = '';\n } \n claimDeviceForm.resetForm(); \n $scope.claimDeviceFormGroup.reset(deviceObj);\n $scope.loading = false;\n $scope.showSuccessToast(successfulClaim, 2000, 'bottom', 'left', $scope.toastTargetId);\n self.ctx.updateAliases();\n }\n \n}\n",
"settingsSchema": "", "settingsSchema": "",

10
application/src/main/data/json/system/widget_types/gateway_configuration.json

@ -8,7 +8,15 @@
"type": "static", "type": "static",
"sizeX": 8, "sizeX": 8,
"sizeY": 6.5, "sizeY": 6.5,
"resources": [], "resources": [
{
"url": {
"entityType": "TB_RESOURCE",
"id": "${RESOURCE:gateway-management-extension.js}"
},
"isModule": true
}
],
"templateHtml": "<tb-gateway-form\n [ctx]=\"ctx\">\n</tb-gateway-form>\n", "templateHtml": "<tb-gateway-form\n [ctx]=\"ctx\">\n</tb-gateway-form>\n",
"templateCss": "", "templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n", "controllerScript": "self.onInit = function() {\n}\n",

10
application/src/main/data/json/system/widget_types/gateway_configuration__single_device_.json

@ -8,7 +8,15 @@
"type": "latest", "type": "latest",
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 9, "sizeY": 9,
"resources": [], "resources": [
{
"url": {
"entityType": "TB_RESOURCE",
"id": "${RESOURCE:gateway-management-extension.js}"
},
"isModule": true
}
],
"templateHtml": "<tb-gateway-form\n [ctx]=\"ctx\"\n [isStateForm]=\"true\">\n</tb-gateway-form>", "templateHtml": "<tb-gateway-form\n [ctx]=\"ctx\"\n [isStateForm]=\"true\">\n</tb-gateway-form>",
"templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}",
"controllerScript": "self.onInit = function() {\n}\n\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\t\t\t\n dataKeysOptional: true,\n singleEntity: true\n };\n}\n\n", "controllerScript": "self.onInit = function() {\n}\n\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\t\t\t\n dataKeysOptional: true,\n singleEntity: true\n };\n}\n\n",

10
application/src/main/data/json/system/widget_types/gateway_connectors.json

@ -8,7 +8,15 @@
"type": "latest", "type": "latest",
"sizeX": 11, "sizeX": 11,
"sizeY": 8, "sizeY": 8,
"resources": [], "resources": [
{
"url": {
"entityType": "TB_RESOURCE",
"id": "${RESOURCE:gateway-management-extension.js}"
},
"isModule": true
}
],
"templateHtml": "<tb-gateway-connector [device]=\"entityId\" *ngIf=\"entityId\" [ctx]=\"ctx\"></tb-gateway-connector>", "templateHtml": "<tb-gateway-connector [device]=\"entityId\" *ngIf=\"entityId\" [ctx]=\"ctx\"></tb-gateway-connector>",
"templateCss": "", "templateCss": "",
"controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n }\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.gatewayConnectors?.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}", "controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n }\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.gatewayConnectors?.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}",

10
application/src/main/data/json/system/widget_types/gateway_custom_statistics.json

@ -8,7 +8,15 @@
"type": "timeseries", "type": "timeseries",
"sizeX": 8, "sizeX": 8,
"sizeY": 5, "sizeY": 5,
"resources": [], "resources": [
{
"url": {
"entityType": "TB_RESOURCE",
"id": "${RESOURCE:gateway-management-extension.js}"
},
"isModule": true
}
],
"templateHtml": "<tb-gateway-statistics [ctx]=ctx></tb-gateway-statistics>", "templateHtml": "<tb-gateway-statistics [ctx]=ctx></tb-gateway-statistics>",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
"controllerScript": "self.onInit = function() { \n};\n\nself.onDataUpdated = function() {\n};\n\nself.onLatestDataUpdated = function() {\n};\n\nself.onResize = function() {\n};\n\nself.onEditModeChanged = function() {\n};\n\nself.onDestroy = function() {\n};\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: false,\n dataKeysOptional: true\n };\n}\n", "controllerScript": "self.onInit = function() { \n};\n\nself.onDataUpdated = function() {\n};\n\nself.onLatestDataUpdated = function() {\n};\n\nself.onResize = function() {\n};\n\nself.onEditModeChanged = function() {\n};\n\nself.onDestroy = function() {\n};\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: false,\n dataKeysOptional: true\n };\n}\n",

10
application/src/main/data/json/system/widget_types/gateway_general_chart_statistics.json

@ -8,7 +8,15 @@
"type": "timeseries", "type": "timeseries",
"sizeX": 8, "sizeX": 8,
"sizeY": 5, "sizeY": 5,
"resources": [], "resources": [
{
"url": {
"entityType": "TB_RESOURCE",
"id": "${RESOURCE:gateway-management-extension.js}"
},
"isModule": true
}
],
"templateHtml": "<tb-gateway-statistics [ctx]=ctx [general]='true'></tb-gateway-statistics>", "templateHtml": "<tb-gateway-statistics [ctx]=ctx [general]='true'></tb-gateway-statistics>",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
"controllerScript": "self.onInit = function() { \n};\n\nself.onDataUpdated = function() {\n};\n\nself.onLatestDataUpdated = function() {\n};\n\nself.onResize = function() {\n};\n\nself.onEditModeChanged = function() {\n};\n\nself.onDestroy = function() {\n};\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: false\n };\n}\n", "controllerScript": "self.onInit = function() { \n};\n\nself.onDataUpdated = function() {\n};\n\nself.onLatestDataUpdated = function() {\n};\n\nself.onResize = function() {\n};\n\nself.onEditModeChanged = function() {\n};\n\nself.onDestroy = function() {\n};\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: false\n };\n}\n",

10
application/src/main/data/json/system/widget_types/gateway_general_configuration.json

@ -8,7 +8,15 @@
"type": "latest", "type": "latest",
"sizeX": 11, "sizeX": 11,
"sizeY": 8, "sizeY": 8,
"resources": [], "resources": [
{
"url": {
"entityType": "TB_RESOURCE",
"id": "${RESOURCE:gateway-management-extension.js}"
},
"isModule": true
}
],
"templateHtml": "<tb-gateway-configuration [device]=\"entityId\" *ngIf=\"entityId\"></tb-gateway-configuration>", "templateHtml": "<tb-gateway-configuration [device]=\"entityId\" *ngIf=\"entityId\"></tb-gateway-configuration>",
"templateCss": "", "templateCss": "",
"controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n }\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}", "controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n }\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}",

10
application/src/main/data/json/system/widget_types/gateway_logs.json

@ -8,7 +8,15 @@
"type": "timeseries", "type": "timeseries",
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 3, "sizeY": 3,
"resources": [], "resources": [
{
"url": {
"entityType": "TB_RESOURCE",
"id": "${RESOURCE:gateway-management-extension.js}"
},
"isModule": true
}
],
"templateHtml": "<tb-gateway-logs [ctx]=\"ctx\">\n \n</tb-gateway-logs>", "templateHtml": "<tb-gateway-logs [ctx]=\"ctx\">\n \n</tb-gateway-logs>",
"templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}", "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}",
"controllerScript": "self.onInit = function() {\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}", "controllerScript": "self.onInit = function() {\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}",

4
application/src/main/data/json/system/widget_types/raspberry_pi_gpio_control.json

File diff suppressed because one or more lines are too long

4
application/src/main/data/json/system/widget_types/raspberry_pi_gpio_panel.json

@ -9,8 +9,8 @@
"sizeX": 7, "sizeX": 7,
"sizeY": 10.5, "sizeY": 10.5,
"resources": [], "resources": [],
"templateHtml": "<div class=\"gpio-panel\" style=\"height: 100%;\">\n <section fxLayout=\"row\" *ngFor=\"let row of rows\">\n <section fxFlex fxLayout=\"row\" *ngFor=\"let cell of row; let $index = index\">\n <section fxLayout=\"row\" fxFlex *ngIf=\"cell\" fxLayoutAlign=\"{{$index===0 ? 'end center' : 'start center'}}\">\n <span class=\"gpio-left-label\" [fxShow]=\"$index===0\">{{ cell.label }}</span>\n <section fxLayout=\"row\" class=\"led-panel\" [ngClass]=\"$index===0 ? 'col-0' : 'col-1'\"\n [ngStyle]=\"{backgroundColor: ledPanelBackgroundColor}\">\n <span class=\"pin\" [fxShow]=\"$index===0\">{{cell.pin}}</span>\n <span class=\"led-container\">\n <tb-led-light [size]=\"prefferedRowHeight\"\n [colorOn]=\"cell.colorOn\"\n [colorOff]=\"cell.colorOff\"\n [offOpacity]=\"'0.9'\"\n [enabled]=\"cell.enabled\">\n </tb-led-light>\n </span>\n <span class=\"pin\" [fxShow]=\"$index===1\">{{cell.pin}}</span>\n </section>\n <span class=\"gpio-right-label\" [fxShow]=\"$index===1\">{{ cell.label }}</span>\n </section>\n <section fxLayout=\"row\" fxFlex *ngIf=\"!cell\">\n <span fxFlex [fxShow]=\"$index===0\"></span>\n <span class=\"led-panel\"\n [ngStyle]=\"{height: prefferedRowHeight+'px', backgroundColor: ledPanelBackgroundColor}\"></span>\n <span fxFlex [fxShow]=\"$index===1\"></span>\n </section>\n </section>\n </section> \n</div>", "templateHtml": "<div class=\"gpio-panel\" style=\"height: 100%;\">\n <section class=\"flex flex-row\" *ngFor=\"let row of rows\">\n <section class=\"flex flex-1 flex-row\" *ngFor=\"let cell of row; let $index = index\">\n <section class=\"flex flex-1 flex-row\" [class.justify-end]=\"$index===0\" [class.justify-start]=\"$index!==0\" *ngIf=\"cell\">\n <span class=\"gpio-left-label\" [class.!hidden]=\"$index!==0\">{{ cell.label }}</span>\n <section class=\"led-panel flex flex-row\" [class.col-0]=\"$index===0\" [class.col-1]=\"$index!==0\"\n [style.background-color]=\"ledPanelBackgroundColor\">\n <span class=\"pin\" [class.!hidden]=\"$index!==0\">{{cell.pin}}</span>\n <span class=\"led-container\">\n <tb-led-light [size]=\"prefferedRowHeight\"\n [colorOn]=\"cell.colorOn\"\n [colorOff]=\"cell.colorOff\"\n [offOpacity]=\"'0.9'\"\n [enabled]=\"cell.enabled\">\n </tb-led-light>\n </span>\n <span class=\"pin\" [class.!hidden]=\"$index!==1\">{{cell.pin}}</span>\n </section>\n <span class=\"gpio-right-label\" [class.!hidden]=\"$index!==1\">{{ cell.label }}</span>\n </section>\n <section class=\"flex flex-1 flex-row\" *ngIf=\"!cell\">\n <span class=\"flex-1\" [class.!hidden]=\"$index!==0\"></span>\n <span class=\"led-panel\"\n [style.height.px]=\"prefferedRowHeight\"\n [style.background-color]=\"ledPanelBackgroundColor\"></span>\n <span class=\"flex-1\" [class.!hidden]=\"$index!==1\"></span>\n </section>\n </section>\n </section>\n</div>",
"templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n.gpio-panel tb-led-light > div {\n margin: auto;\n}\n\n.led-panel {\n margin: 0;\n width: 66px;\n min-width: 66px;\n}\n\n.led-container {\n width: 48px;\n min-width: 48px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.led-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.led-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section.flex-1 {\n min-width: 0px;\n}\n\n\n.gpio-panel tb-led-light > div {\n margin: auto;\n}\n\n.led-panel {\n margin: 0;\n width: 66px;\n min-width: 66px;\n}\n\n.led-container {\n width: 48px;\n min-width: 48px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.led-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.led-panel.col-1 .pin {\n margin-right: auto;\n\n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}",
"controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-panel-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n var i, gpio;\n \n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n \n scope.gpioList = [];\n scope.gpioByPin = {};\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false,\n colorOn: tinycolor(gpio.color).lighten(20).toHexString(),\n colorOff: tinycolor(gpio.color).darken().toHexString()\n }\n );\n scope.gpioByPin[gpio.pin] = scope.gpioList[scope.gpioList.length-1];\n }\n\n scope.ledPanelBackgroundColor = settings.ledPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var changed = false;\n for (var d = 0; d < self.ctx.data.length; d++) {\n var cellData = self.ctx.data[d];\n var dataKey = cellData.dataKey;\n var gpio = self.ctx.$scope.gpioByPin[dataKey.label];\n if (gpio) {\n var enabled = false;\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n enabled = (tvPair[1] === true || tvPair[1] === 'true');\n }\n if (gpio.enabled != enabled) {\n changed = true;\n gpio.enabled = enabled;\n }\n }\n }\n if (changed) {\n self.ctx.detectChanges();\n } \n}\n\nself.onResize = function() {\n var rowCount = self.ctx.$scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n self.ctx.$scope.prefferedRowHeight = prefferedRowHeight;\n \n var ratio = prefferedRowHeight/32;\n \n var css = '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n \n cssParser.createStyleElement(namespace, css); \n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n", "controllerScript": "var namespace;\nvar cssParser = new cssjs();\n\nself.onInit = function() {\n var utils = self.ctx.$injector.get(self.ctx.servicesMap.get('utils'));\n namespace = 'gpio-panel-' + utils.guid();\n cssParser.testMode = false;\n cssParser.cssPreviewNamespace = namespace;\n self.ctx.$container.addClass(namespace);\n self.ctx.ngZone.run(function() {\n init(); \n });\n}\n\nfunction init() {\n var i, gpio;\n \n var scope = self.ctx.$scope;\n var settings = self.ctx.settings;\n \n scope.gpioList = [];\n scope.gpioByPin = {};\n for (var g = 0; g < settings.gpioList.length; g++) {\n gpio = settings.gpioList[g];\n scope.gpioList.push(\n {\n row: gpio.row,\n col: gpio.col,\n pin: gpio.pin,\n label: gpio.label,\n enabled: false,\n colorOn: tinycolor(gpio.color).lighten(20).toHexString(),\n colorOff: tinycolor(gpio.color).darken().toHexString()\n }\n );\n scope.gpioByPin[gpio.pin] = scope.gpioList[scope.gpioList.length-1];\n }\n\n scope.ledPanelBackgroundColor = settings.ledPanelBackgroundColor || tinycolor('green').lighten(2).toRgbString();\n\n scope.gpioCells = {};\n var rowCount = 0;\n for (i = 0; i < scope.gpioList.length; i++) {\n gpio = scope.gpioList[i];\n scope.gpioCells[gpio.row+'_'+gpio.col] = gpio;\n rowCount = Math.max(rowCount, gpio.row+1);\n }\n \n scope.prefferedRowHeight = 32;\n scope.rows = [];\n for (i = 0; i < rowCount; i++) {\n var row = [];\n for (var c =0; c<2;c++) {\n if (scope.gpioCells[i+'_'+c]) {\n row[c] = scope.gpioCells[i+'_'+c];\n } else {\n row[c] = null;\n }\n }\n scope.rows.push(row);\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var changed = false;\n for (var d = 0; d < self.ctx.data.length; d++) {\n var cellData = self.ctx.data[d];\n var dataKey = cellData.dataKey;\n var gpio = self.ctx.$scope.gpioByPin[dataKey.label];\n if (gpio) {\n var enabled = false;\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n enabled = (tvPair[1] === true || tvPair[1] === 'true');\n }\n if (gpio.enabled != enabled) {\n changed = true;\n gpio.enabled = enabled;\n }\n }\n }\n if (changed) {\n self.ctx.detectChanges();\n } \n}\n\nself.onResize = function() {\n var rowCount = self.ctx.$scope.rows.length;\n var prefferedRowHeight = (self.ctx.height - 35)/rowCount;\n prefferedRowHeight = Math.min(32, prefferedRowHeight);\n prefferedRowHeight = Math.max(12, prefferedRowHeight);\n self.ctx.$scope.prefferedRowHeight = prefferedRowHeight;\n \n var ratio = prefferedRowHeight/32;\n \n var css = '.gpio-left-label, .gpio-right-label {\\n' +\n ' font-size: ' + 16*ratio+'px;\\n'+\n '}\\n';\n var pinsFontSize = Math.max(9, 12*ratio);\n css += '.pin {\\n' +\n ' font-size: ' + pinsFontSize+'px;\\n'+\n '}\\n';\n \n cssParser.createStyleElement(namespace, css); \n \n self.ctx.detectChanges();\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",

2
application/src/main/data/json/system/widget_types/rpc_button.json

@ -9,7 +9,7 @@
"sizeX": 4, "sizeX": 4,
"sizeY": 2, "sizeY": 2,
"resources": [], "resources": [],
"templateHtml": "<div class=\"tb-rpc-button\" fxLayout=\"column\">\n <div fxFlex=\"20\" class=\"title-container\" fxLayout=\"row\"\n fxLayoutAlign=\"center center\" [fxShow]=\"showTitle\">\n <span class=\"button-title\">{{title}}</span>\n </div>\n <div fxFlex=\"{{showTitle ? 80 : 100}}\" [ngStyle]=\"{paddingTop: showTitle ? '5px': '10px'}\"\n class=\"button-container\" fxLayout=\"column\" fxLayoutAlign=\"center center\">\n <div>\n <button mat-button (click)=\"sendCommand()\"\n [class.mat-mdc-raised-button]=\"styleButton?.isRaised\"\n [color]=\"styleButton?.isPrimary ? 'primary' : ''\"\n [ngStyle]=\"customStyle\">\n {{buttonLable}}\n </button>\n </div>\n </div>\n <div class=\"error-container\" [ngStyle]=\"{'background': error?.length ? 'rgba(255,255,255,0.25)' : 'none'}\"\n fxLayout=\"row\" fxLayoutAlign=\"center center\">\n <span class=\"button-error\">{{ error }}</span>\n </div>\n</div>", "templateHtml": "<div class=\"tb-rpc-button flex flex-col\">\n <div class=\"title-container flex max-w-20% flex-full flex-row items-center justify-center\"\n [class.!hidden]=\"!showTitle\">\n <span class=\"button-title\">{{title}}</span>\n </div>\n <div class=\"button-container flex flex-full flex-col items-center justify-center\"\n [class]=\"{\n 'max-w-80%': showTitle,\n 'max-w-100%': !showTitle\n }\"\n [style.padding-top]=\"showTitle ? '5px': '10px'\">\n <div>\n <button mat-button (click)=\"sendCommand()\"\n [class.mat-mdc-raised-button]=\"styleButton?.isRaised\"\n [color]=\"styleButton?.isPrimary ? 'primary' : ''\"\n [style]=\"customStyle\">\n {{buttonLable}}\n </button>\n </div>\n </div>\n <div class=\"error-container flex flex-row items-center justify-center\" [style.background]=\"error?.length ? 'rgba(255,255,255,0.25)' : 'none'\">\n <span class=\"button-error\">{{ error }}</span>\n </div>\n</div>",
"templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-mdc-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-mdc-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}",
"controllerScript": "var requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n \n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n let rpcEnabled = self.ctx.defaultSubscription.rpcEnabled;\n\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton.bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n if (!rpcEnabled) {\n self.ctx.$scope.error =\n 'Target device is not set!';\n }\n\n self.ctx.$scope.sendCommand = function() {\n var rpcMethod = self.ctx.settings.methodName;\n var rpcParams = self.ctx.settings.methodParams;\n if (rpcParams.length) {\n try {\n rpcParams = JSON.parse(rpcParams);\n } catch (e) {}\n }\n var timeout = self.ctx.settings.requestTimeout;\n var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ?\n true : false;\n\n var commandPromise;\n if (oneWayElseTwoWay) {\n commandPromise = self.ctx.controlApi.sendOneWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n }\n commandPromise.subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n );\n };\n}\n\nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}\n", "controllerScript": "var requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n \n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n let rpcEnabled = self.ctx.defaultSubscription.rpcEnabled;\n\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton.bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n if (!rpcEnabled) {\n self.ctx.$scope.error =\n 'Target device is not set!';\n }\n\n self.ctx.$scope.sendCommand = function() {\n var rpcMethod = self.ctx.settings.methodName;\n var rpcParams = self.ctx.settings.methodParams;\n if (rpcParams.length) {\n try {\n rpcParams = JSON.parse(rpcParams);\n } catch (e) {}\n }\n var timeout = self.ctx.settings.requestTimeout;\n var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ?\n true : false;\n\n var commandPromise;\n if (oneWayElseTwoWay) {\n commandPromise = self.ctx.controlApi.sendOneWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n }\n commandPromise.subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n );\n };\n}\n\nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}\n",
"settingsSchema": "", "settingsSchema": "",

12
application/src/main/data/json/system/widget_types/service_rpc.json

@ -8,9 +8,17 @@
"type": "rpc", "type": "rpc",
"sizeX": 8.5, "sizeX": 8.5,
"sizeY": 5.5, "sizeY": 5.5,
"resources": [], "resources": [
{
"url": {
"entityType": "TB_RESOURCE",
"id": "${RESOURCE:gateway-management-extension.js}"
},
"isModule": true
}
],
"templateHtml": "<tb-gateway-service-rpc [ctx]=\"ctx\"></tb-gateway-service-rpc>", "templateHtml": "<tb-gateway-service-rpc [ctx]=\"ctx\"></tb-gateway-service-rpc>",
"templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section[fxflex] {\n min-width: 0px;\n}\n\n\n.switch-panel {\n margin: 0;\n height: 32px;\n width: 66px;\n min-width: 66px;\n}\n\n.switch-panel mat-slide-toggle {\n margin: 0;\n width: 36px;\n min-width: 36px;\n}\n\n.switch-panel.col-0 mat-slide-toggle {\n margin-left: 8px;\n margin-right: 4px;\n}\n\n.switch-panel.col-1 mat-slide-toggle {\n margin-left: 4px;\n margin-right: 8px;\n}\n\n.gpio-row {\n height: 32px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.switch-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.switch-panel.col-1 .pin {\n margin-right: auto;\n \n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}", "templateCss": ".error {\n font-size: 14px !important;\n color: maroon;/*rgb(250,250,250);*/\n background-color: transparent;\n padding: 6px;\n}\n\n.error span {\n margin: auto;\n}\n\n.gpio-panel {\n padding-top: 10px;\n white-space: nowrap;\n}\n\n.gpio-panel section.flex-1 {\n min-width: 0px;\n}\n\n\n.switch-panel {\n margin: 0;\n height: 32px;\n width: 66px;\n min-width: 66px;\n}\n\n.switch-panel mat-slide-toggle {\n margin: 0;\n width: 36px;\n min-width: 36px;\n}\n\n.switch-panel.col-0 mat-slide-toggle {\n margin-left: 8px;\n margin-right: 4px;\n}\n\n.switch-panel.col-1 mat-slide-toggle {\n margin-left: 4px;\n margin-right: 8px;\n}\n\n.gpio-row {\n height: 32px;\n}\n\n.pin {\n margin-top: auto;\n margin-bottom: auto;\n color: white;\n font-size: 12px;\n width: 16px;\n min-width: 16px;\n}\n\n.switch-panel.col-0 .pin {\n margin-left: auto;\n padding-left: 2px;\n text-align: right;\n}\n\n.switch-panel.col-1 .pin {\n margin-right: auto;\n\n text-align: left;\n}\n\n.gpio-left-label {\n margin-right: 8px;\n}\n\n.gpio-right-label {\n margin-left: 8px;\n}",
"controllerScript": "\nself.onInit = function() {\n};", "controllerScript": "\nself.onInit = function() {\n};",
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",

2
application/src/main/data/json/system/widget_types/update_boolean_timeseries.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/update_device_attribute.json

@ -9,7 +9,7 @@
"sizeX": 4, "sizeX": 4,
"sizeY": 2, "sizeY": 2,
"resources": [], "resources": [],
"templateHtml": "<div class=\"tb-rpc-button\" fxLayout=\"column\">\n <div fxFlex=\"20\" class=\"title-container\" fxLayout=\"row\"\n fxLayoutAlign=\"center center\" [fxShow]=\"showTitle\">\n <span class=\"button-title\">{{title}}</span>\n </div>\n <div fxFlex=\"{{showTitle ? 80 : 100}}\" [ngStyle]=\"{paddingTop: showTitle ? '5px': '10px'}\"\n class=\"button-container\" fxLayout=\"column\" fxLayoutAlign=\"center center\">\n <div>\n <button mat-button (click)=\"sendUpdate()\"\n [class.mat-mdc-raised-button]=\"styleButton?.isRaised\"\n [color]=\"styleButton?.isPrimary ? 'primary' : ''\"\n [ngStyle]=\"customStyle\">\n {{buttonLable}}\n </button>\n </div>\n </div>\n <div class=\"error-container\" [ngStyle]=\"{'background': error?.length ? 'rgba(255,255,255,0.25)' : 'none'}\"\n fxLayout=\"row\" fxLayoutAlign=\"center center\">\n <span class=\"button-error\">{{ error }}</span>\n </div>\n</div>", "templateHtml": "<div class=\"tb-rpc-button flex flex-col\">\n <div class=\"title-container flex max-w-20% flex-full flex-row items-center justify-center\"\n [class.!hidden]=\"!showTitle\">\n <span class=\"button-title\">{{title}}</span>\n </div>\n <div class=\"button-container flex flex-full flex-col items-center justify-center\"\n [class]=\"{\n 'max-w-80%': showTitle,\n 'max-w-100%': !showTitle\n }\"\n [style.padding-top]=\"showTitle ? '5px': '10px'\">\n <div>\n <button mat-button (click)=\"sendCommand()\"\n [class.mat-mdc-raised-button]=\"styleButton?.isRaised\"\n [color]=\"styleButton?.isPrimary ? 'primary' : ''\"\n [style]=\"customStyle\">\n {{buttonLable}}\n </button>\n </div>\n </div>\n <div class=\"error-container flex flex-row items-center justify-center\" [style.background]=\"error?.length ? 'rgba(255,255,255,0.25)' : 'none'\">\n <span class=\"button-error\">{{ error }}</span>\n </div>\n</div>",
"templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-mdc-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-mdc-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}",
"controllerScript": "self.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n let attributeService = self.ctx.$scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n let entityId = {\n entityType: \"DEVICE\",\n id: self.ctx.defaultSubscription.targetDeviceId\n };\n attributeService.saveEntityAttributes(entityId,\n entityAttributeType, attributes).subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n\n );\n };\n}\n", "controllerScript": "self.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n let attributeService = self.ctx.$scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n let entityId = {\n entityType: \"DEVICE\",\n id: self.ctx.defaultSubscription.targetDeviceId\n };\n attributeService.saveEntityAttributes(entityId,\n entityAttributeType, attributes).subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n\n );\n };\n}\n",
"settingsSchema": "", "settingsSchema": "",

2
application/src/main/data/json/system/widget_types/update_double_timeseries.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/update_integer_timeseries.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/update_location_timeseries.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/update_server_boolean_attribute.json

@ -9,7 +9,7 @@
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 3, "sizeY": 3,
"resources": [], "resources": [],
"templateHtml": "<div tb-toast toastTarget=\"{{ toastTargetId }}\" style=\"width: 100%; height: 100%;\">\r\n <form *ngIf=\"attributeUpdateFormGroup\"\r\n class=\"attribute-update-form\"\r\n [formGroup]=\"attributeUpdateFormGroup\"\r\n (ngSubmit)=\"updateAttribute()\">\r\n <div style=\"padding: 0 8px; margin: auto 0;\">\r\n <div class=\"attribute-update-form__grid\" [fxShow]=\"entityDetected && isValidParameter && dataKeyDetected\">\r\n <div class=\"grid__element\">\r\n <mat-checkbox formControlName=\"checkboxValue\"\r\n (change)=\"changed()\"\r\n aria-label=\"{{'widgets.input-widgets.switch-timeseries-value' | translate}}\">\r\n {{currentValue}}\r\n </mat-checkbox>\r\n </div>\r\n </div>\r\n\r\n <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" [fxHide]=\"entityDetected\">\r\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\r\n </div>\r\n <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\"\r\n [fxShow]=\"entityDetected && !dataKeyDetected\">\r\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\r\n </div>\r\n <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\"\r\n [fxShow]=\"entityDetected && !isValidParameter\">\r\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\r\n </div>\r\n </div>\r\n </form>\r\n</div>", "templateHtml": "<div tb-toast toastTarget=\"{{ toastTargetId }}\" style=\"width: 100%; height: 100%;\">\r\n <form *ngIf=\"attributeUpdateFormGroup\"\r\n class=\"attribute-update-form\"\r\n [formGroup]=\"attributeUpdateFormGroup\"\r\n (ngSubmit)=\"updateAttribute()\">\r\n <div style=\"padding: 0 8px; margin: auto 0;\">\r\n <div class=\"attribute-update-form__grid\" [class.!hidden]=\"!entityDetected || !isValidParameter || !dataKeyDetected\">\r\n <div class=\"grid__element\">\r\n <mat-checkbox formControlName=\"checkboxValue\"\r\n (change)=\"changed()\"\r\n aria-label=\"{{'widgets.input-widgets.switch-timeseries-value' | translate}}\">\r\n {{currentValue}}\r\n </mat-checkbox>\r\n </div>\r\n </div>\r\n\r\n <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" [class.!hidden]=\"entityDetected\">\r\n {{ 'widgets.input-widgets.no-entity-selected' | translate }}\r\n </div>\r\n <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\"\r\n [class.!hidden]=\"!entityDetected || dataKeyDetected\">\r\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\r\n </div>\r\n <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\"\r\n [class.!hidden]=\"!entityDetected || isValidParameter\">\r\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\r\n </div>\r\n </div>\r\n </form>\r\n</div>",
"templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}",
"controllerScript": "let settings;\nlet attributeService;\nlet utils;\nlet translate;\nlet $scope;\nlet map;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init();\n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n settings.trueValue = utils.defaultValue(utils.customTranslation(settings.trueValue, settings.trueValue), true);\n settings.falseValue = utils.defaultValue(utils.customTranslation(settings.falseValue, settings.falseValue), false);\n\n map = {\n true: settings.trueValue,\n false: settings.falseValue\n };\n \n $scope.checkboxValue = false;\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({checkboxValue: [$scope.checkboxValue]});\n\n $scope.changed = function() {\n $scope.checkboxValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n };\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function() {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.checkboxValue\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = self.ctx.data[0].data[0][1] === 'true';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.attributeUpdateFormGroup.get('checkboxValue').patchValue($scope.checkboxValue);\n self.ctx.detectChanges();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {}", "controllerScript": "let settings;\nlet attributeService;\nlet utils;\nlet translate;\nlet $scope;\nlet map;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init();\n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n\n settings.trueValue = utils.defaultValue(utils.customTranslation(settings.trueValue, settings.trueValue), true);\n settings.falseValue = utils.defaultValue(utils.customTranslation(settings.falseValue, settings.falseValue), false);\n\n map = {\n true: settings.trueValue,\n false: settings.falseValue\n };\n \n $scope.checkboxValue = false;\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({checkboxValue: [$scope.checkboxValue]});\n\n $scope.changed = function() {\n $scope.checkboxValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n };\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n\n $scope.entityDetected = true;\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function() {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SERVER_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.checkboxValue\n }\n ]\n ).subscribe(\n function success() {\n $scope.originalValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = self.ctx.data[0].data[0][1] === 'true';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.attributeUpdateFormGroup.get('checkboxValue').patchValue($scope.checkboxValue);\n self.ctx.detectChanges();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {}",
"settingsSchema": "", "settingsSchema": "",

2
application/src/main/data/json/system/widget_types/update_server_date_attribute.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/update_server_double_attribute.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/update_server_image_attribute.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/update_server_integer_attribute.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/update_server_location_attribute.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/update_server_string_attribute.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/update_shared_boolean_attribute.json

@ -9,7 +9,7 @@
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 3, "sizeY": 3,
"resources": [], "resources": [],
"templateHtml": "<div tb-toast toastTarget=\"{{ toastTargetId }}\" style=\"width: 100%; height: 100%;\">\r\n <form *ngIf=\"attributeUpdateFormGroup\"\r\n class=\"attribute-update-form\"\r\n [formGroup]=\"attributeUpdateFormGroup\"\r\n (ngSubmit)=\"updateAttribute()\">\r\n <div style=\"padding: 0 8px; margin: auto 0;\">\r\n <div class=\"attribute-update-form__grid\" [fxShow]=\"entityDetected && isValidParameter && dataKeyDetected\">\r\n <div class=\"grid__element\">\r\n <mat-checkbox formControlName=\"checkboxValue\"\r\n (change)=\"changed()\"\r\n aria-label=\"{{'widgets.input-widgets.switch-timeseries-value' | translate}}\">\r\n {{currentValue}}\r\n </mat-checkbox>\r\n </div>\r\n </div>\r\n\r\n <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" [fxHide]=\"entityDetected\" [innerHtml]=\"message\"></div>\r\n <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\"\r\n [fxShow]=\"entityDetected && !dataKeyDetected\">\r\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\r\n </div>\r\n <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\"\r\n [fxShow]=\"entityDetected && !isValidParameter\">\r\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\r\n </div>\r\n </div>\r\n </form>\r\n</div>", "templateHtml": "<div tb-toast toastTarget=\"{{ toastTargetId }}\" style=\"width: 100%; height: 100%;\">\r\n <form *ngIf=\"attributeUpdateFormGroup\"\r\n class=\"attribute-update-form\"\r\n [formGroup]=\"attributeUpdateFormGroup\"\r\n (ngSubmit)=\"updateAttribute()\">\r\n <div style=\"padding: 0 8px; margin: auto 0;\">\r\n <div class=\"attribute-update-form__grid\" [class.!hidden]=\"!entityDetected || !isValidParameter || !dataKeyDetected\">\r\n <div class=\"grid__element\">\r\n <mat-checkbox formControlName=\"checkboxValue\"\r\n (change)=\"changed()\"\r\n aria-label=\"{{'widgets.input-widgets.switch-timeseries-value' | translate}}\">\r\n {{currentValue}}\r\n </mat-checkbox>\r\n </div>\r\n </div>\r\n\r\n <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" [class.!hidden]=\"entityDetected\" [innerHtml]=\"message\"></div>\r\n <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\"\r\n [class.!hidden]=\"!entityDetected || dataKeyDetected\">\r\n {{ 'widgets.input-widgets.no-attribute-selected' | translate }}\r\n </div>\r\n <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\"\r\n [class.!hidden]=\"!entityDetected || isValidParameter\">\r\n {{ 'widgets.input-widgets.timeseries-not-allowed' | translate }}\r\n </div>\r\n </div>\r\n </form>\r\n</div>",
"templateCss": ".attribute-update-form {\r\n overflow: hidden;\r\n height: 100%;\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n\r\n.attribute-update-form__grid {\r\n display: flex;\r\n}\r\n.grid__element:first-child {\r\n flex: 1;\r\n}\r\n\r\n.grid__element {\r\n display: flex;\r\n}\r\n\r\n.attribute-update-form .mat-button.mat-icon-button {\r\n width: 32px;\r\n min-width: 32px;\r\n height: 32px;\r\n min-height: 32px;\r\n padding: 0 !important;\r\n margin: 0 !important;\r\n line-height: 20px;\r\n}\r\n\r\n.attribute-update-form .mat-icon-button mat-icon {\r\n width: 20px;\r\n min-width: 20px;\r\n height: 20px;\r\n min-height: 20px;\r\n font-size: 20px;\r\n}\r\n\r\n.tb-toast {\r\n font-size: 14px!important;\r\n}", "templateCss": ".attribute-update-form {\r\n overflow: hidden;\r\n height: 100%;\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n\r\n.attribute-update-form__grid {\r\n display: flex;\r\n}\r\n.grid__element:first-child {\r\n flex: 1;\r\n}\r\n\r\n.grid__element {\r\n display: flex;\r\n}\r\n\r\n.attribute-update-form .mat-button.mat-icon-button {\r\n width: 32px;\r\n min-width: 32px;\r\n height: 32px;\r\n min-height: 32px;\r\n padding: 0 !important;\r\n margin: 0 !important;\r\n line-height: 20px;\r\n}\r\n\r\n.attribute-update-form .mat-icon-button mat-icon {\r\n width: 20px;\r\n min-width: 20px;\r\n height: 20px;\r\n min-height: 20px;\r\n font-size: 20px;\r\n}\r\n\r\n.tb-toast {\r\n font-size: 14px!important;\r\n}",
"controllerScript": "let settings;\nlet attributeService;\nlet utils;\nlet translate;\nlet $scope;\nlet map;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init();\n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n\n settings.trueValue = utils.defaultValue(utils.customTranslation(settings.trueValue, settings.trueValue), true);\n settings.falseValue = utils.defaultValue(utils.customTranslation(settings.falseValue, settings.falseValue), false);\n\n map = {\n true: settings.trueValue,\n false: settings.falseValue\n };\n \n $scope.checkboxValue = false;\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({checkboxValue: [$scope.checkboxValue]});\n\n $scope.changed = function() {\n $scope.checkboxValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n };\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function() {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.checkboxValue || false\n }\n ]\n ).subscribe(\n function success() {\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = self.ctx.data[0].data[0][1] === 'true';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.attributeUpdateFormGroup.get('checkboxValue').patchValue($scope.checkboxValue);\n self.ctx.detectChanges();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {}", "controllerScript": "let settings;\nlet attributeService;\nlet utils;\nlet translate;\nlet $scope;\nlet map;\n\nself.onInit = function() {\n self.ctx.ngZone.run(function() {\n init();\n self.ctx.detectChanges(true);\n });\n};\n\n\nfunction init() {\n $scope = self.ctx.$scope;\n attributeService = $scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n utils = $scope.$injector.get(self.ctx.servicesMap.get('utils'));\n translate = $scope.$injector.get(self.ctx.servicesMap.get('translate'));\n $scope.toastTargetId = 'input-widget' + utils.guid();\n settings = utils.deepClone(self.ctx.settings) || {};\n settings.showResultMessage = utils.defaultValue(settings.showResultMessage, true);\n\n $scope.isValidParameter = true;\n $scope.dataKeyDetected = false;\n $scope.message = translate.instant('widgets.input-widgets.no-entity-selected');\n\n settings.trueValue = utils.defaultValue(utils.customTranslation(settings.trueValue, settings.trueValue), true);\n settings.falseValue = utils.defaultValue(utils.customTranslation(settings.falseValue, settings.falseValue), false);\n\n map = {\n true: settings.trueValue,\n false: settings.falseValue\n };\n \n $scope.checkboxValue = false;\n $scope.currentValue = map[$scope.checkboxValue];\n\n $scope.attributeUpdateFormGroup = $scope.fb.group({checkboxValue: [$scope.checkboxValue]});\n\n $scope.changed = function() {\n $scope.checkboxValue = $scope.attributeUpdateFormGroup.get('checkboxValue').value;\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.updateAttribute();\n };\n\n if (self.ctx.datasources && self.ctx.datasources.length) {\n var datasource = self.ctx.datasources[0];\n if (datasource.type === 'entity') {\n if (datasource.entityType === 'DEVICE') {\n if (datasource.entityType && datasource.entityId) {\n $scope.entityName = datasource.entityName;\n if (settings.widgetTitle && settings.widgetTitle.length) {\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n $scope.titleTemplate = self.ctx.widgetConfig.title;\n }\n \n $scope.entityDetected = true;\n }\n } else {\n $scope.message = translate.instant('widgets.input-widgets.not-allowed-entity');\n }\n }\n if (datasource.dataKeys.length) {\n if (datasource.dataKeys[0].type !== \"attribute\") {\n $scope.isValidParameter = false;\n } else {\n $scope.currentKey = datasource.dataKeys[0].name;\n $scope.dataKeyType = datasource.dataKeys[0].type;\n $scope.dataKeyDetected = true;\n }\n }\n }\n\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n $scope.updateAttribute = function() {\n if ($scope.entityDetected) {\n var datasource = self.ctx.datasources[0];\n attributeService.saveEntityAttributes(\n datasource.entity.id,\n 'SHARED_SCOPE',\n [\n {\n key: $scope.currentKey,\n value: $scope.checkboxValue || false\n }\n ]\n ).subscribe(\n function success() {\n if (settings.showResultMessage) {\n $scope.showSuccessToast(translate.instant('widgets.input-widgets.update-successful'), 1000, 'bottom', 'left', $scope.toastTargetId);\n }\n },\n function fail() {\n if (settings.showResultMessage) {\n $scope.showErrorToast(translate.instant('widgets.input-widgets.update-failed'), 'bottom', 'left', $scope.toastTargetId);\n }\n }\n );\n }\n };\n}\n\nself.onDataUpdated = function() {\n try {\n if ($scope.dataKeyDetected) {\n $scope.checkboxValue = self.ctx.data[0].data[0][1] === 'true';\n $scope.currentValue = map[$scope.checkboxValue];\n $scope.attributeUpdateFormGroup.get('checkboxValue').patchValue($scope.checkboxValue);\n self.ctx.detectChanges();\n }\n } catch (e) {\n console.log(e);\n }\n}\n\nself.onResize = function() {}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {}",
"settingsSchema": "", "settingsSchema": "",

2
application/src/main/data/json/system/widget_types/update_shared_date_attribute.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/update_shared_double_attribute.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/update_shared_image_attribute.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/update_shared_integer_attribute.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/update_shared_location_attribute.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/update_shared_string_attribute.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/update_string_timeseries.json

File diff suppressed because one or more lines are too long

14
application/src/main/data/json/tenant/dashboards/gateways.json → application/src/main/data/resources/dashboards/gateways_dashboard.json

@ -1892,7 +1892,7 @@
"useMarkdownTextFunction": false, "useMarkdownTextFunction": false,
"markdownTextPattern": "<div class=\"action-buttons-container\">\r\n <button mat-raised-button color=\"primary\"\r\n (click)=\"ctx.actionsApi.handleWidgetAction($event, ctx.actionsApi.getActionDescriptors('elementClick')[0], ctx.datasources[0].entity.id)\"\r\n >{{ 'gateway.launch-command' | translate }}\r\n </button>\r\n</div>", "markdownTextPattern": "<div class=\"action-buttons-container\">\r\n <button mat-raised-button color=\"primary\"\r\n (click)=\"ctx.actionsApi.handleWidgetAction($event, ctx.actionsApi.getActionDescriptors('elementClick')[0], ctx.datasources[0].entity.id)\"\r\n >{{ 'gateway.launch-command' | translate }}\r\n </button>\r\n</div>",
"applyDefaultMarkdownStyle": false, "applyDefaultMarkdownStyle": false,
"markdownCss": ".action-buttons-container {\r\n display: flex;\r\n flex-wrap: wrap;\r\n flex-direction: row;\r\n height: 100%;\r\n width: 100%;\r\n align-content: center;\r\n}\r\n\r\nbutton {\r\n flex-grow: 1;\r\n margin: 10px;\r\n min-width: 150px;\r\n height: auto;\r\n}" "markdownCss": ".action-buttons-container {\r\n display: flex;\r\n flex-wrap: wrap;\r\n flex-direction: row;\r\n height: 100%;\r\n width: 100%;\r\n align-content: center;\r\n}\r\n\r\nbutton {\r\n flex-grow: 1;\r\n margin: 10px;\r\n min-width: 150px;\r\n height: auto;\r\n line-height: 36px;\n}"
}, },
"title": "Service command", "title": "Service command",
"showTitleIcon": false, "showTitleIcon": false,
@ -1919,7 +1919,15 @@
"customHtml": "<div class=\"container\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>{{ 'gateway.launch-command' | translate }}</h2>\n <span fxFlex></span>\n <div [tb-help]=\"'gatewayInstall'\"></div>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <tb-gateway-command [deviceId]=\"entityId\"></tb-gateway-command>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button color=\"primary\"\n type=\"button\"\n (click)=\"cancel()\" cdkFocusInitial>\n {{ 'action.close' | translate }}\n </button>\n </div>\n</div>\n", "customHtml": "<div class=\"container\">\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2>{{ 'gateway.launch-command' | translate }}</h2>\n <span fxFlex></span>\n <div [tb-help]=\"'gatewayInstall'\"></div>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <tb-gateway-command [deviceId]=\"entityId\"></tb-gateway-command>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button color=\"primary\"\n type=\"button\"\n (click)=\"cancel()\" cdkFocusInitial>\n {{ 'action.close' | translate }}\n </button>\n </div>\n</div>\n",
"customCss": ".container {\n display: grid;\n grid-template-rows: min-content minmax(auto, 1fr) min-content;\n height: 100%;\n max-height: 100vh;\n width: 600px;\n max-width: 100%;\n}", "customCss": ".container {\n display: grid;\n grid-template-rows: min-content minmax(auto, 1fr) min-content;\n height: 100%;\n max-height: 100vh;\n width: 600px;\n max-width: 100%;\n}",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\n\nopenCommands();\n\nfunction openCommands() {\n customDialog.customDialog(htmlTemplate, CommandsDialogController, {panelClass: \"test\"}).subscribe();\n}\n\nfunction CommandsDialogController(instance) {\n let vm = instance;\n \n vm.entityId = entityId.id;\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n}\n", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\n\nopenCommands();\n\nfunction openCommands() {\n customDialog.customDialog(htmlTemplate, CommandsDialogController, {panelClass: \"test\"}).subscribe();\n}\n\nfunction CommandsDialogController(instance) {\n let vm = instance;\n \n vm.entityId = entityId.id;\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n}\n",
"customResources": [], "customResources": [
{
"url": {
"entityType": "TB_RESOURCE",
"id": "${RESOURCE:gateway-management-extension.js}"
},
"isModule": true
}
],
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
"id": "337c767b-3217-d3d3-b955-7b0bd0858a1d" "id": "337c767b-3217-d3d3-b955-7b0bd0858a1d"
@ -2000,7 +2008,7 @@
"useMarkdownTextFunction": true, "useMarkdownTextFunction": true,
"markdownTextFunction": "let buttonsHtml = \"\" \nctx.actionsApi.getActionDescriptors('elementClick').forEach((btn, index)=>{\n let disabled =false;\n if (index == 2) {\n disabled = data[0] && data[0].RemoteLoggingLevel ? data[0].RemoteLoggingLevel == \"NONE\" : true;\n } else if (index == 4) {\n const conf = data[0].general_configuration? JSON.parse(data[0].general_configuration): {};\n disabled = !conf.remoteShell;\n }\n buttonsHtml += `<button mat-raised-button disabled=${disabled} color=\"primary\"(click)=\"ctx.actionsApi.handleWidgetAction($event, ctx.actionsApi.getActionDescriptors('elementClick')[${index}], ctx.datasources[0].entity.id)\" fxFlex fxflex.md=\"50\">${btn.displayName}</button>`\n});\n\nreturn `<div class=\"action-buttons-container\" fxLayout=\"columnd\" fxLayout.md=\"row wrap\" fxLayoutGap=\"5px\" >${buttonsHtml}</div>`;", "markdownTextFunction": "let buttonsHtml = \"\" \nctx.actionsApi.getActionDescriptors('elementClick').forEach((btn, index)=>{\n let disabled =false;\n if (index == 2) {\n disabled = data[0] && data[0].RemoteLoggingLevel ? data[0].RemoteLoggingLevel == \"NONE\" : true;\n } else if (index == 4) {\n const conf = data[0].general_configuration? JSON.parse(data[0].general_configuration): {};\n disabled = !conf.remoteShell;\n }\n buttonsHtml += `<button mat-raised-button disabled=${disabled} color=\"primary\"(click)=\"ctx.actionsApi.handleWidgetAction($event, ctx.actionsApi.getActionDescriptors('elementClick')[${index}], ctx.datasources[0].entity.id)\" fxFlex fxflex.md=\"50\">${btn.displayName}</button>`\n});\n\nreturn `<div class=\"action-buttons-container\" fxLayout=\"columnd\" fxLayout.md=\"row wrap\" fxLayoutGap=\"5px\" >${buttonsHtml}</div>`;",
"applyDefaultMarkdownStyle": false, "applyDefaultMarkdownStyle": false,
"markdownCss": ".action-buttons-container {\r\n display: flex;\r\n flex-wrap: wrap;\r\n flex-direction: row;\r\n height: 100%;\r\n width: 100%;\r\n align-content: start;\r\n}\r\n\r\nbutton {\r\n flex-grow: 1;\r\n margin: 10px;\r\n min-width: 150px;\r\n height: auto;\r\n}" "markdownCss": ".action-buttons-container {\r\n display: flex;\r\n flex-wrap: wrap;\r\n flex-direction: row;\r\n height: 100%;\r\n width: 100%;\r\n align-content: start;\r\n}\r\n\r\nbutton {\r\n flex-grow: 1;\r\n margin: 10px;\r\n min-width: 150px;\r\n height: auto;\r\n line-height: 36px;\n}"
}, },
"title": "General configuration", "title": "General configuration",
"showTitleIcon": false, "showTitleIcon": false,

1
application/src/main/data/resources/js_modules/gateway-management-extension.js

File diff suppressed because one or more lines are too long

11
application/src/main/data/upgrade/3.8.1/schema_update.sql

@ -14,6 +14,17 @@
-- limitations under the License. -- limitations under the License.
-- --
ALTER TABLE user_credentials ADD COLUMN IF NOT EXISTS last_login_ts BIGINT;
UPDATE user_credentials c SET last_login_ts = (SELECT (additional_info::json ->> 'lastLoginTs')::bigint FROM tb_user u WHERE u.id = c.user_id)
WHERE last_login_ts IS NULL;
ALTER TABLE user_credentials ADD COLUMN IF NOT EXISTS failed_login_attempts INT;
UPDATE user_credentials c SET failed_login_attempts = (SELECT (additional_info::json ->> 'failedLoginAttempts')::int FROM tb_user u WHERE u.id = c.user_id)
WHERE failed_login_attempts IS NULL;
UPDATE tb_user SET additional_info = (additional_info::jsonb - 'lastLoginTs' - 'failedLoginAttempts' - 'userCredentialsEnabled')::text
WHERE additional_info IS NOT NULL AND additional_info != 'null';
-- UPDATE RULE NODE DEBUG MODE TO DEBUG STRATEGY START -- UPDATE RULE NODE DEBUG MODE TO DEBUG STRATEGY START
ALTER TABLE rule_node ALTER TABLE rule_node

10
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -235,7 +235,15 @@ public class AdminController extends BaseController {
} }
} }
String email = getCurrentUser().getEmail(); String email = getCurrentUser().getEmail();
mailService.sendTestMail(adminSettings.getJsonValue(), email); try {
mailService.sendTestMail(adminSettings.getJsonValue(), email);
} catch (ThingsboardException e) {
String error = e.getMessage();
if (e.getCause() != null) {
error += ": " + e.getCause().getMessage(); // showing actual underlying error for testing purposes
}
throw new ThingsboardException(error, e.getErrorCode());
}
} }
} }

8
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -217,7 +217,7 @@ public class AuthController extends BaseController {
try { try {
mailService.sendAccountActivatedEmail(loginUrl, email); mailService.sendAccountActivatedEmail(loginUrl, email);
} catch (Exception e) { } catch (Exception e) {
log.info("Unable to send account activation email [{}]", e.getMessage()); log.warn("Unable to send account activation email [{}]", e.getMessage());
} }
} }
@ -256,7 +256,11 @@ public class AuthController extends BaseController {
String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request); String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request);
String loginUrl = String.format("%s/login", baseUrl); String loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail(); String email = user.getEmail();
mailService.sendPasswordWasResetEmail(loginUrl, email); try {
mailService.sendPasswordWasResetEmail(loginUrl, email);
} catch (Exception e) {
log.warn("Couldn't send password was reset email: {}", e.getMessage());
}
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));

19
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException; import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
@ -402,7 +403,7 @@ public abstract class BaseController {
|| exception instanceof DataValidationException || cause instanceof IncorrectParameterException) { || exception instanceof DataValidationException || cause instanceof IncorrectParameterException) {
return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else if (exception instanceof MessagingException) { } else if (exception instanceof MessagingException) {
return new ThingsboardException("Unable to send mail: " + exception.getMessage(), ThingsboardErrorCode.GENERAL); return new ThingsboardException("Unable to send mail", ThingsboardErrorCode.GENERAL);
} else if (exception instanceof AsyncRequestTimeoutException) { } else if (exception instanceof AsyncRequestTimeoutException) {
return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL); return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL);
} else if (exception instanceof DataAccessException) { } else if (exception instanceof DataAccessException) {
@ -877,14 +878,18 @@ public abstract class BaseController {
} }
protected void checkUserInfo(User user) throws ThingsboardException { protected void checkUserInfo(User user) throws ThingsboardException {
ObjectNode info;
if (user.getAdditionalInfo() instanceof ObjectNode additionalInfo) { if (user.getAdditionalInfo() instanceof ObjectNode additionalInfo) {
checkDashboardInfo(additionalInfo); info = additionalInfo;
checkDashboardInfo(info);
UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId()); } else {
if (userCredentials.isEnabled() && !additionalInfo.has("userCredentialsEnabled")) { info = JacksonUtil.newObjectNode();
additionalInfo.put("userCredentialsEnabled", true); user.setAdditionalInfo(info);
}
} }
UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId());
info.put("userCredentialsEnabled", userCredentials.isEnabled());
info.put("lastLoginTs", userCredentials.getLastLoginTs());
} }
protected void checkDashboardInfo(JsonNode additionalInfo) throws ThingsboardException { protected void checkDashboardInfo(JsonNode additionalInfo) throws ThingsboardException {

70
application/src/main/java/org/thingsboard/server/controller/TbResourceController.java

@ -44,10 +44,12 @@ import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject; import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.util.ThrowingSupplier;
import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.resource.TbResourceService;
@ -106,13 +108,29 @@ public class TbResourceController extends BaseController {
.body(resource); .body(resource);
} }
@ApiOperation(value = "Download resource (downloadResource)",
notes = "Download resource with a given type and key for the given scope" + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/resource/{resourceType}/{scope}/{key}")
public ResponseEntity<ByteArrayResource> downloadResourceIfChanged(@Parameter(description = "Type of the resource", schema = @Schema(allowableValues = {"lwm2m_model", "jks", "pkcs_12", "js_module", "dashboard"}))
@PathVariable("resourceType") String resourceTypeStr,
@Parameter(description = "Scope of the resource", schema = @Schema(allowableValues = {"system", "tenant"}))
@PathVariable String scope,
@Parameter(description = "Key of the resource, e.g. 'extension.js'")
@PathVariable String key,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
ResourceType resourceType = ResourceType.valueOf(resourceTypeStr.toUpperCase());
return downloadResourceIfChanged(() -> checkResourceInfo(scope, resourceType, key, Operation.READ), etag);
}
@ApiOperation(value = "Download LWM2M Resource (downloadLwm2mResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Download LWM2M Resource (downloadLwm2mResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/resource/lwm2m/{resourceId}/download", produces = "application/xml") @GetMapping(value = "/resource/lwm2m/{resourceId}/download", produces = "application/xml")
public ResponseEntity<ByteArrayResource> downloadLwm2mResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION) public ResponseEntity<ByteArrayResource> downloadLwm2mResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId, @PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.LWM2M_MODEL, strResourceId, etag); return downloadResourceIfChanged(strResourceId, etag);
} }
@ApiOperation(value = "Download PKCS_12 Resource (downloadPkcs12ResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Download PKCS_12 Resource (downloadPkcs12ResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@ -121,7 +139,7 @@ public class TbResourceController extends BaseController {
public ResponseEntity<ByteArrayResource> downloadPkcs12ResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION) public ResponseEntity<ByteArrayResource> downloadPkcs12ResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId, @PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.PKCS_12, strResourceId, etag); return downloadResourceIfChanged(strResourceId, etag);
} }
@ApiOperation(value = "Download JKS Resource (downloadJksResourceIfChanged)", @ApiOperation(value = "Download JKS Resource (downloadJksResourceIfChanged)",
@ -131,7 +149,7 @@ public class TbResourceController extends BaseController {
public ResponseEntity<ByteArrayResource> downloadJksResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION) public ResponseEntity<ByteArrayResource> downloadJksResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId, @PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.JKS, strResourceId, etag); return downloadResourceIfChanged(strResourceId, etag);
} }
@ApiOperation(value = "Download JS Resource (downloadJsResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @ApiOperation(value = "Download JS Resource (downloadJsResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@ -140,7 +158,7 @@ public class TbResourceController extends BaseController {
public ResponseEntity<ByteArrayResource> downloadJsResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION) public ResponseEntity<ByteArrayResource> downloadJsResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId, @PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException { @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.JS_MODULE, strResourceId, etag); return downloadResourceIfChanged(strResourceId, etag);
} }
@ApiOperation(value = "Get Resource Info (getResourceInfoById)", @ApiOperation(value = "Get Resource Info (getResourceInfoById)",
@ -211,6 +229,7 @@ public class TbResourceController extends BaseController {
} else { } else {
Collections.addAll(resourceTypes, ResourceType.values()); Collections.addAll(resourceTypes, ResourceType.values());
resourceTypes.remove(ResourceType.IMAGE); resourceTypes.remove(ResourceType.IMAGE);
resourceTypes.remove(ResourceType.DASHBOARD);
} }
filter.resourceTypes(resourceTypes); filter.resourceTypes(resourceTypes);
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
@ -271,7 +290,7 @@ public class TbResourceController extends BaseController {
@RequestParam String sortOrder, @RequestParam String sortOrder,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"id", "name"}, requiredMode = Schema.RequiredMode.REQUIRED)) @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"id", "name"}, requiredMode = Schema.RequiredMode.REQUIRED))
@RequestParam String sortProperty, @RequestParam String sortProperty,
@Parameter(description = "LwM2M Object ids.", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @Parameter(description = "LwM2M Object ids.", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam(required = false) String[] objectIds) throws ThingsboardException { @RequestParam(required = false) String[] objectIds) throws ThingsboardException {
return checkNotNull(tbResourceService.findLwM2mObject(getTenantId(), sortOrder, sortProperty, objectIds)); return checkNotNull(tbResourceService.findLwM2mObject(getTenantId(), sortOrder, sortProperty, objectIds));
} }
@ -288,30 +307,49 @@ public class TbResourceController extends BaseController {
tbResourceService.delete(tbResource, getCurrentUser()); tbResourceService.delete(tbResource, getCurrentUser());
} }
private ResponseEntity<ByteArrayResource> downloadResourceIfChanged(ResourceType resourceType, String strResourceId, String etag) throws ThingsboardException { private ResponseEntity<ByteArrayResource> downloadResourceIfChanged(String strResourceId, String etag) throws ThingsboardException {
checkParameter(RESOURCE_ID, strResourceId); checkParameter(RESOURCE_ID, strResourceId);
TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); TbResourceId resourceId = new TbResourceId(toUUID(strResourceId));
return downloadResourceIfChanged(() -> checkResourceInfoId(resourceId, Operation.READ), etag);
}
private ResponseEntity<ByteArrayResource> downloadResourceIfChanged(ThrowingSupplier<TbResourceInfo> resourceInfoProvider,
String etag) throws ThingsboardException {
TbResourceInfo resourceInfo = resourceInfoProvider.get();
if (etag != null) { if (etag != null) {
TbResourceInfo tbResourceInfo = checkResourceInfoId(resourceId, Operation.READ);
etag = StringUtils.remove(etag, '\"'); // etag is wrapped in double quotes due to HTTP specification etag = StringUtils.remove(etag, '\"'); // etag is wrapped in double quotes due to HTTP specification
if (etag.equals(tbResourceInfo.getEtag())) { if (etag.equals(resourceInfo.getEtag())) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED) return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
.eTag(tbResourceInfo.getEtag()) .eTag(resourceInfo.getEtag())
.build(); .build();
} }
} }
TbResource tbResource = checkResourceId(resourceId, Operation.READ); byte[] data = resourceService.getResourceData(resourceInfo.getTenantId(), resourceInfo.getId());
ByteArrayResource resource = new ByteArrayResource(data);
ByteArrayResource resource = new ByteArrayResource(tbResource.getData());
return ResponseEntity.ok() return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + tbResource.getFileName()) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + resourceInfo.getFileName())
.header("x-filename", tbResource.getFileName()) .header("x-filename", resourceInfo.getFileName())
.contentLength(resource.contentLength()) .contentLength(resource.contentLength())
.header("Content-Type", resourceType.getMediaType()) .header("Content-Type", resourceInfo.getResourceType().getMediaType())
.cacheControl(CacheControl.noCache()) .cacheControl(CacheControl.noCache())
.eTag(tbResource.getEtag()) .eTag(resourceInfo.getEtag())
.body(resource); .body(resource);
} }
private TbResourceInfo checkResourceInfo(String scope, ResourceType resourceType, String key, Operation operation) throws ThingsboardException {
TenantId tenantId;
if (scope.equals("tenant")) {
tenantId = getTenantId();
} else if (scope.equals("system")) {
tenantId = TenantId.SYS_TENANT_ID;
} else {
throw new IllegalArgumentException("Invalid scope");
}
TbResourceInfo resourceInfo = resourceService.findResourceInfoByTenantIdAndKey(tenantId, resourceType, key);
checkEntity(getCurrentUser(), checkNotNull(resourceInfo), operation);
return resourceInfo;
}
} }

8
application/src/main/java/org/thingsboard/server/controller/UserController.java

@ -78,7 +78,6 @@ import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -123,7 +122,6 @@ public class UserController extends BaseController {
private final MailService mailService; private final MailService mailService;
private final JwtTokenFactory tokenFactory; private final JwtTokenFactory tokenFactory;
private final SystemSecurityService systemSecurityService;
private final ApplicationEventPublisher eventPublisher; private final ApplicationEventPublisher eventPublisher;
private final TbUserService tbUserService; private final TbUserService tbUserService;
private final EntityQueryService entityQueryService; private final EntityQueryService entityQueryService;
@ -219,7 +217,11 @@ public class UserController extends BaseController {
accessControlService.checkPermission(securityUser, Resource.USER, Operation.READ, user.getId(), user); accessControlService.checkPermission(securityUser, Resource.USER, Operation.READ, user.getId(), user);
UserActivationLink activationLink = tbUserService.getActivationLink(securityUser.getTenantId(), securityUser.getCustomerId(), user.getId(), request); UserActivationLink activationLink = tbUserService.getActivationLink(securityUser.getTenantId(), securityUser.getCustomerId(), user.getId(), request);
mailService.sendActivationEmail(activationLink.value(), activationLink.ttlMs(), email); try {
mailService.sendActivationEmail(activationLink.value(), activationLink.ttlMs(), email);
} catch (Exception e) {
throw new ThingsboardException("Couldn't send user activation email", ThingsboardErrorCode.GENERAL);
}
} }
@ApiOperation(value = "Get activation link (getActivationLink)", @ApiOperation(value = "Get activation link (getActivationLink)",

3
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -154,6 +154,7 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry); entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Updating system data..."); log.info("Updating system data...");
dataUpdateService.upgradeRuleNodes(); dataUpdateService.upgradeRuleNodes();
installScripts.loadSystemResources();
systemDataLoaderService.loadSystemWidgets(); systemDataLoaderService.loadSystemWidgets();
installScripts.loadSystemLwm2mResources(); installScripts.loadSystemLwm2mResources();
installScripts.loadSystemImages(); installScripts.loadSystemImages();
@ -170,6 +171,7 @@ public class ThingsboardInstallService {
log.info("Installing DataBase schema for entities..."); log.info("Installing DataBase schema for entities...");
entityDatabaseSchemaService.createDatabaseSchema(); entityDatabaseSchemaService.createDatabaseSchema();
entityDatabaseSchemaService.createSchemaVersion();
entityDatabaseSchemaService.createOrUpdateViewsAndFunctions(); entityDatabaseSchemaService.createOrUpdateViewsAndFunctions();
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry); entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
@ -194,6 +196,7 @@ public class ThingsboardInstallService {
systemDataLoaderService.createDefaultTenantProfiles(); systemDataLoaderService.createDefaultTenantProfiles();
systemDataLoaderService.createAdminSettings(); systemDataLoaderService.createAdminSettings();
systemDataLoaderService.createRandomJwtSettings(); systemDataLoaderService.createRandomJwtSettings();
installScripts.loadSystemResources();
systemDataLoaderService.loadSystemWidgets(); systemDataLoaderService.loadSystemWidgets();
systemDataLoaderService.createOAuth2Templates(); systemDataLoaderService.createOAuth2Templates();
systemDataLoaderService.createQueues(); systemDataLoaderService.createQueues();

4
application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java

@ -50,7 +50,6 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.RelationActionEvent; import org.thingsboard.server.dao.eventsourcing.RelationActionEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserServiceImpl;
/** /**
* This event listener does not support async event processing because relay on ThreadLocal * This event listener does not support async event processing because relay on ThreadLocal
@ -226,13 +225,10 @@ public class EdgeEventSourcingListener {
} }
private void cleanUpUserAdditionalInfo(User user) { private void cleanUpUserAdditionalInfo(User user) {
// reset FAILED_LOGIN_ATTEMPTS and LAST_LOGIN_TS - edge is not interested in this information
if (user.getAdditionalInfo() instanceof NullNode) { if (user.getAdditionalInfo() instanceof NullNode) {
user.setAdditionalInfo(null); user.setAdditionalInfo(null);
} }
if (user.getAdditionalInfo() instanceof ObjectNode additionalInfo) { if (user.getAdditionalInfo() instanceof ObjectNode additionalInfo) {
additionalInfo.remove(UserServiceImpl.FAILED_LOGIN_ATTEMPTS);
additionalInfo.remove(UserServiceImpl.LAST_LOGIN_TS);
if (additionalInfo.isEmpty()) { if (additionalInfo.isEmpty()) {
user.setAdditionalInfo(null); user.setAdditionalInfo(null);
} else { } else {

98
application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncService.java

@ -0,0 +1,98 @@
/**
* 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.
*/
package org.thingsboard.server.service.entitiy.dashboard;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.GitSyncService;
import org.thingsboard.server.service.sync.vc.GitRepository.FileType;
import org.thingsboard.server.service.sync.vc.GitRepository.RepoFile;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
@Service
@TbCoreComponent
@RequiredArgsConstructor
@Slf4j
@ConditionalOnProperty(value = "transport.gateway.dashboard.sync.enabled", havingValue = "true")
public class DashboardSyncService {
private final GitSyncService gitSyncService;
private final ResourceService resourceService;
private final WidgetsBundleService widgetsBundleService;
private final PartitionService partitionService;
@Value("${transport.gateway.dashboard.sync.repository_url:}")
private String repoUrl;
@Value("${transport.gateway.dashboard.sync.branch:main}")
private String branch;
@Value("${transport.gateway.dashboard.sync.fetch_frequency:24}")
private int fetchFrequencyHours;
private static final String REPO_KEY = "gateways-dashboard";
private static final String GATEWAYS_DASHBOARD_KEY = "gateways_dashboard.json";
@AfterStartUp(order = AfterStartUp.REGULAR_SERVICE)
public void init() throws Exception {
gitSyncService.registerSync(REPO_KEY, repoUrl, branch, TimeUnit.HOURS.toMillis(fetchFrequencyHours), this::update);
}
private void update() {
if (!partitionService.isMyPartition(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID)) {
return;
}
List<RepoFile> resources = listFiles("resources");
for (RepoFile resourceFile : resources) {
String data = getFileContent(resourceFile.path());
resourceService.createOrUpdateSystemResource(ResourceType.JS_MODULE, resourceFile.name(), data);
}
Stream<String> widgetsBundles = listFiles("widget_bundles").stream()
.map(widgetsBundleFile -> getFileContent(widgetsBundleFile.path()));
Stream<String> widgetTypes = listFiles("widget_types").stream()
.map(widgetTypeFile -> getFileContent(widgetTypeFile.path()));
widgetsBundleService.updateSystemWidgets(widgetsBundles, widgetTypes);
RepoFile dashboardFile = listFiles("dashboards").get(0);
String dashboardJson = getFileContent(dashboardFile.path());
resourceService.createOrUpdateSystemResource(ResourceType.DASHBOARD, GATEWAYS_DASHBOARD_KEY, dashboardJson);
log.info("Gateways dashboard sync completed");
}
private List<RepoFile> listFiles(String path) {
return gitSyncService.listFiles(REPO_KEY, path, 1, FileType.FILE);
}
private String getFileContent(String path) {
return gitSyncService.getFileContent(REPO_KEY, path);
}
}

2
application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java

@ -60,7 +60,7 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse
mailService.sendActivationEmail(activationLink.value(), activationLink.ttlMs(), savedUser.getEmail()); mailService.sendActivationEmail(activationLink.value(), activationLink.ttlMs(), savedUser.getEmail());
} catch (ThingsboardException e) { } catch (ThingsboardException e) {
userService.deleteUser(tenantId, savedUser); userService.deleteUser(tenantId, savedUser);
throw e; throw new ThingsboardException("Couldn't send user activation email", ThingsboardErrorCode.GENERAL);
} }
} }
logEntityActionService.logEntityAction(tenantId, savedUser.getId(), savedUser, customerId, actionType, user); logEntityActionService.logEntityAction(tenantId, savedUser.getId(), savedUser, customerId, actionType, user);

3
application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/DefaultWidgetsBundleService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.entitiy.widgets.bundle; package org.thingsboard.server.service.entitiy.widgets.bundle;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
@ -34,6 +35,7 @@ import java.util.List;
@Service @Service
@TbCoreComponent @TbCoreComponent
@AllArgsConstructor @AllArgsConstructor
@Slf4j
public class DefaultWidgetsBundleService extends AbstractTbEntityService implements TbWidgetsBundleService { public class DefaultWidgetsBundleService extends AbstractTbEntityService implements TbWidgetsBundleService {
private final WidgetsBundleService widgetsBundleService; private final WidgetsBundleService widgetsBundleService;
@ -79,4 +81,5 @@ public class DefaultWidgetsBundleService extends AbstractTbEntityService impleme
widgetTypeService.updateWidgetsBundleWidgetFqns(user.getTenantId(), widgetsBundleId, widgetFqns); widgetTypeService.updateWidgetsBundleWidgetFqns(user.getTenantId(), widgetsBundleId, widgetFqns);
autoCommit(user, widgetsBundleId); autoCommit(user, widgetsBundleId);
} }
} }

1
application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/TbWidgetsBundleService.java

@ -29,4 +29,5 @@ public interface TbWidgetsBundleService extends SimpleTbEntityService<WidgetsBun
void updateWidgetsBundleWidgetFqns(WidgetsBundleId widgetsBundleId, List<String> widgetFqns, User user) throws Exception; void updateWidgetsBundleWidgetFqns(WidgetsBundleId widgetsBundleId, List<String> widgetFqns, User user) throws Exception;
} }

2
application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java

@ -23,4 +23,6 @@ public interface EntityDatabaseSchemaService extends DatabaseSchemaService {
void createCustomerTitleUniqueConstraintIfNotExists(); void createCustomerTitleUniqueConstraintIfNotExists();
void createSchemaVersion();
} }

137
application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -37,13 +36,9 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
@ -57,8 +52,9 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.service.install.update.ImagesUpdater; import org.thingsboard.server.service.install.update.ImagesUpdater;
import java.io.IOException; import java.io.IOException;
import java.nio.file.DirectoryStream; import java.io.UncheckedIOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
@ -95,7 +91,7 @@ public class InstallScripts {
public static final String OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates"; public static final String OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates";
public static final String DASHBOARDS_DIR = "dashboards"; public static final String DASHBOARDS_DIR = "dashboards";
public static final String MODELS_LWM2M_DIR = "lwm2m-registry"; public static final String MODELS_LWM2M_DIR = "lwm2m-registry";
public static final String CREDENTIALS_DIR = "credentials"; public static final String RESOURCES_DIR = "resources";
public static final String JSON_EXT = ".json"; public static final String JSON_EXT = ".json";
public static final String SVG_EXT = ".svg"; public static final String SVG_EXT = ".svg";
@ -173,7 +169,6 @@ public class InstallScripts {
loadRuleChainsFromPath(tenantId, edgeChainsDir); loadRuleChainsFromPath(tenantId, edgeChainsDir);
} }
@SneakyThrows
private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) { private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) {
findRuleChainsFromPath(ruleChainsPath).forEach(path -> { findRuleChainsFromPath(ruleChainsPath).forEach(path -> {
try { try {
@ -185,12 +180,10 @@ public class InstallScripts {
}); });
} }
List<Path> findRuleChainsFromPath(Path ruleChainsPath) throws IOException { List<Path> findRuleChainsFromPath(Path ruleChainsPath) {
List<Path> paths = new ArrayList<>(); try (Stream<Path> files = listDir(ruleChainsPath).filter(path -> path.toString().endsWith(InstallScripts.JSON_EXT))) {
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(ruleChainsPath, path -> path.toString().endsWith(InstallScripts.JSON_EXT))) { return files.toList();
dirStream.forEach(paths::add);
} }
return paths;
} }
public RuleChain createDefaultRuleChain(TenantId tenantId, String ruleChainName) { public RuleChain createDefaultRuleChain(TenantId tenantId, String ruleChainName) {
@ -214,11 +207,11 @@ public class InstallScripts {
return ruleChain; return ruleChain;
} }
public void loadSystemWidgets() throws Exception { public void loadSystemWidgets() {
log.info("Loading system widgets"); log.info("Loading system widgets");
Map<Path, JsonNode> widgetsBundlesMap = new HashMap<>(); Map<Path, JsonNode> widgetsBundlesMap = new HashMap<>();
Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR); Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) { try (Stream<Path> dirStream = listDir(widgetBundlesDir).filter(path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach( dirStream.forEach(
path -> { path -> {
JsonNode widgetsBundleDescriptorJson; JsonNode widgetsBundleDescriptorJson;
@ -250,12 +243,14 @@ public class InstallScripts {
} }
Path widgetTypesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_TYPES_DIR); Path widgetTypesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_TYPES_DIR);
if (Files.exists(widgetTypesDir)) { if (Files.exists(widgetTypesDir)) {
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetTypesDir, path -> path.toString().endsWith(JSON_EXT))) { try (Stream<Path> dirStream = listDir(widgetTypesDir).filter(path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach( dirStream.forEach(
path -> { path -> {
try { try {
JsonNode widgetTypeJson = JacksonUtil.toJsonNode(path.toFile()); String widgetTypeJson = Files.readString(path);
WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class); widgetTypeJson = resourceService.checkSystemResourcesUsage(widgetTypeJson, ResourceType.JS_MODULE);
WidgetTypeDetails widgetTypeDetails = JacksonUtil.fromString(widgetTypeJson, WidgetTypeDetails.class);
widgetTypeService.saveWidgetType(widgetTypeDetails); widgetTypeService.saveWidgetType(widgetTypeDetails);
} catch (Exception e) { } catch (Exception e) {
log.error("Unable to load widget type from json: [{}]", path.toString()); log.error("Unable to load widget type from json: [{}]", path.toString());
@ -303,12 +298,12 @@ public class InstallScripts {
} }
} }
private void loadSystemScadaSymbols() throws Exception { private void loadSystemScadaSymbols() {
log.info("Loading system SCADA symbols"); log.info("Loading system SCADA symbols");
Path scadaSymbolsDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, SCADA_SYMBOLS_DIR); Path scadaSymbolsDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, SCADA_SYMBOLS_DIR);
if (Files.exists(scadaSymbolsDir)) { if (Files.exists(scadaSymbolsDir)) {
WidgetTypeDetails scadaSymbolWidgetTemplate = widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "scada_symbol"); WidgetTypeDetails scadaSymbolWidgetTemplate = widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "scada_symbol");
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(scadaSymbolsDir, path -> path.toString().endsWith(SVG_EXT))) { try (Stream<Path> dirStream = listDir(scadaSymbolsDir).filter(path -> path.toString().endsWith(SVG_EXT))) {
dirStream.forEach( dirStream.forEach(
path -> { path -> {
try { try {
@ -355,7 +350,7 @@ public class InstallScripts {
} }
private WidgetTypeDetails saveScadaSymbolWidget(WidgetTypeDetails template, TbResourceInfo scadaSymbol, private WidgetTypeDetails saveScadaSymbolWidget(WidgetTypeDetails template, TbResourceInfo scadaSymbol,
ImageUtils.ScadaSymbolMetadataInfo metadata) { ImageUtils.ScadaSymbolMetadataInfo metadata) {
String symbolUrl = DataConstants.TB_IMAGE_PREFIX + scadaSymbol.getLink(); String symbolUrl = DataConstants.TB_IMAGE_PREFIX + scadaSymbol.getLink();
WidgetTypeDetails scadaSymbolWidget = new WidgetTypeDetails(); WidgetTypeDetails scadaSymbolWidget = new WidgetTypeDetails();
JsonNode descriptor = JacksonUtil.clone(template.getDescriptor()); JsonNode descriptor = JacksonUtil.clone(template.getDescriptor());
@ -375,34 +370,26 @@ public class InstallScripts {
defaultConfig.put("title", metadata.getTitle()); defaultConfig.put("title", metadata.getTitle());
ObjectNode settings; ObjectNode settings;
if (defaultConfig.has("settings")) { if (defaultConfig.has("settings")) {
settings = (ObjectNode)defaultConfig.get("settings"); settings = (ObjectNode) defaultConfig.get("settings");
} else { } else {
settings = JacksonUtil.newObjectNode(); settings = JacksonUtil.newObjectNode();
defaultConfig.set("settings", settings); defaultConfig.set("settings", settings);
} }
settings.put("scadaSymbolUrl", symbolUrl); settings.put("scadaSymbolUrl", symbolUrl);
((ObjectNode)descriptor).put("defaultConfig", JacksonUtil.toString(defaultConfig)); ((ObjectNode) descriptor).put("defaultConfig", JacksonUtil.toString(defaultConfig));
((ObjectNode)descriptor).put("sizeX", metadata.getWidgetSizeX()); ((ObjectNode) descriptor).put("sizeX", metadata.getWidgetSizeX());
((ObjectNode)descriptor).put("sizeY", metadata.getWidgetSizeY()); ((ObjectNode) descriptor).put("sizeY", metadata.getWidgetSizeY());
String controllerScript = descriptor.get("controllerScript").asText(); String controllerScript = descriptor.get("controllerScript").asText();
controllerScript = controllerScript.replaceAll("previewWidth: '\\d*px'", "previewWidth: '" + (metadata.getWidgetSizeX() * 100) + "px'"); controllerScript = controllerScript.replaceAll("previewWidth: '\\d*px'", "previewWidth: '" + (metadata.getWidgetSizeX() * 100) + "px'");
controllerScript = controllerScript.replaceAll("previewHeight: '\\d*px'", "previewHeight: '" + (metadata.getWidgetSizeY() * 100 + 20) + "px'"); controllerScript = controllerScript.replaceAll("previewHeight: '\\d*px'", "previewHeight: '" + (metadata.getWidgetSizeY() * 100 + 20) + "px'");
((ObjectNode)descriptor).put("controllerScript", controllerScript); ((ObjectNode) descriptor).put("controllerScript", controllerScript);
return widgetTypeService.saveWidgetType(scadaSymbolWidget); return widgetTypeService.saveWidgetType(scadaSymbolWidget);
} }
private void deleteSystemWidgetBundle(String bundleAlias) { private void deleteSystemWidgetBundle(String bundleAlias) {
WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, bundleAlias); WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, bundleAlias);
if (widgetsBundle != null) { if (widgetsBundle != null) {
PageData<WidgetTypeInfo> widgetTypes; widgetTypeService.deleteWidgetTypesByBundleId(TenantId.SYS_TENANT_ID, widgetsBundle.getId());
var pageLink = new PageLink(1024);
do {
widgetTypes = widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(TenantId.SYS_TENANT_ID, widgetsBundle.getId(), false, DeprecatedFilter.ALL, null, pageLink);
for (var widgetType : widgetTypes.getData()) {
widgetTypeService.deleteWidgetType(TenantId.SYS_TENANT_ID, widgetType.getId());
}
pageLink.nextPageLink();
} while (widgetTypes.hasNext());
widgetsBundleService.deleteWidgetsBundle(TenantId.SYS_TENANT_ID, widgetsBundle.getId()); widgetsBundleService.deleteWidgetsBundle(TenantId.SYS_TENANT_ID, widgetsBundle.getId());
} }
} }
@ -415,11 +402,10 @@ public class InstallScripts {
imagesUpdater.updateAssetProfilesImages(); imagesUpdater.updateAssetProfilesImages();
} }
@SneakyThrows
public void loadSystemImages() { public void loadSystemImages() {
log.info("Loading system images..."); log.info("Loading system images...");
Stream<Path> dashboardsFiles = Stream.concat(Files.list(Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR)), Stream<Path> dashboardsFiles = Stream.concat(listDir(Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR)),
Files.list(Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DASHBOARDS_DIR))); listDir(Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DASHBOARDS_DIR)));
try (dashboardsFiles) { try (dashboardsFiles) {
dashboardsFiles.forEach(file -> { dashboardsFiles.forEach(file -> {
try { try {
@ -442,25 +428,22 @@ public class InstallScripts {
loadDashboardsFromDir(tenantId, customerId, dashboardsDir); loadDashboardsFromDir(tenantId, customerId, dashboardsDir);
} }
@SneakyThrows
private void loadDashboardsFromDir(TenantId tenantId, CustomerId customerId, Path dashboardsDir) { private void loadDashboardsFromDir(TenantId tenantId, CustomerId customerId, Path dashboardsDir) {
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dashboardsDir, path -> path.toString().endsWith(JSON_EXT))) { try (Stream<Path> dashboards = listDir(dashboardsDir).filter(path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach( dashboards.forEach(path -> {
path -> { try {
try { JsonNode dashboardJson = JacksonUtil.toJsonNode(path.toFile());
JsonNode dashboardJson = JacksonUtil.toJsonNode(path.toFile()); Dashboard dashboard = JacksonUtil.treeToValue(dashboardJson, Dashboard.class);
Dashboard dashboard = JacksonUtil.treeToValue(dashboardJson, Dashboard.class); dashboard.setTenantId(tenantId);
dashboard.setTenantId(tenantId); Dashboard savedDashboard = dashboardService.saveDashboard(dashboard);
Dashboard savedDashboard = dashboardService.saveDashboard(dashboard); if (customerId != null && !customerId.isNullUid()) {
if (customerId != null && !customerId.isNullUid()) { dashboardService.assignDashboardToCustomer(TenantId.SYS_TENANT_ID, savedDashboard.getId(), customerId);
dashboardService.assignDashboardToCustomer(TenantId.SYS_TENANT_ID, savedDashboard.getId(), customerId);
}
} catch (Exception e) {
log.error("Unable to load dashboard from json: [{}]", path.toString());
throw new RuntimeException("Unable to load dashboard from json", e);
}
} }
); } catch (Exception e) {
log.error("Unable to load dashboard from json: [{}]", path.toString());
throw new RuntimeException("Unable to load dashboard from json", e);
}
});
} }
} }
@ -475,9 +458,9 @@ public class InstallScripts {
} }
} }
public void createOAuth2Templates() throws Exception { public void createOAuth2Templates() {
Path oauth2ConfigTemplatesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, OAUTH2_CONFIG_TEMPLATES_DIR); Path oauth2ConfigTemplatesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, OAUTH2_CONFIG_TEMPLATES_DIR);
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(oauth2ConfigTemplatesDir, path -> path.toString().endsWith(JSON_EXT))) { try (Stream<Path> dirStream = listDir(oauth2ConfigTemplatesDir).filter(path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach( dirStream.forEach(
path -> { path -> {
try { try {
@ -500,7 +483,7 @@ public class InstallScripts {
public void loadSystemLwm2mResources() { public void loadSystemLwm2mResources() {
Path resourceLwm2mPath = Paths.get(getDataDir(), MODELS_LWM2M_DIR); Path resourceLwm2mPath = Paths.get(getDataDir(), MODELS_LWM2M_DIR);
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(resourceLwm2mPath, path -> path.toString().endsWith(InstallScripts.XML_EXT))) { try (Stream<Path> dirStream = listDir(resourceLwm2mPath).filter(path -> path.toString().endsWith(InstallScripts.XML_EXT))) {
dirStream.forEach( dirStream.forEach(
path -> { path -> {
try { try {
@ -523,6 +506,43 @@ public class InstallScripts {
} }
} }
public void loadSystemResources() {
Path resourcesDir = Path.of(getDataDir(), RESOURCES_DIR);
loadSystemResources(resourcesDir.resolve("js_modules"), ResourceType.JS_MODULE);
loadSystemResources(resourcesDir.resolve("dashboards"), ResourceType.DASHBOARD);
}
private void loadSystemResources(Path dir, ResourceType resourceType) {
listDir(dir).forEach(resourceFile -> {
String resourceKey = resourceFile.getFileName().toString();
try {
String data = getContent(resourceFile);
TbResource resource = resourceService.createOrUpdateSystemResource(resourceType, resourceKey, data);
log.info("{} resource {}", (resource.getId() == null ? "Created" : "Updated"), resourceKey);
} catch (Exception e) {
throw new RuntimeException("Unable to load system resource " + resourceFile, e);
}
});
}
private String getContent(Path file) {
try {
return Files.readString(file);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private Stream<Path> listDir(Path dir) {
try {
return Files.list(dir);
} catch (NoSuchFileException e) {
return Stream.empty();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private void doSaveLwm2mResource(TbResource resource) throws ThingsboardException { private void doSaveLwm2mResource(TbResource resource) throws ThingsboardException {
log.trace("Executing saveResource [{}]", resource); log.trace("Executing saveResource [{}]", resource);
if (resource.getData() == null || resource.getData().length == 0) { if (resource.getData() == null || resource.getData().length == 0) {
@ -534,4 +554,5 @@ public class InstallScripts {
resourceService.saveResource(resource); resourceService.saveResource(resource);
} }
} }
} }

30
application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java

@ -16,7 +16,10 @@
package org.thingsboard.server.service.install; package org.thingsboard.server.service.install;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.info.BuildProperties;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Service @Service
@ -29,6 +32,11 @@ public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSer
public static final String SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL = "schema-entities-idx-psql-addon.sql"; public static final String SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL = "schema-entities-idx-psql-addon.sql";
public static final String SCHEMA_VIEWS_AND_FUNCTIONS_SQL = "schema-views-and-functions.sql"; public static final String SCHEMA_VIEWS_AND_FUNCTIONS_SQL = "schema-views-and-functions.sql";
@Autowired
private BuildProperties buildProperties;
@Autowired
private JdbcTemplate jdbcTemplate;
public SqlEntityDatabaseSchemaService() { public SqlEntityDatabaseSchemaService() {
super(SCHEMA_ENTITIES_SQL, SCHEMA_ENTITIES_IDX_SQL); super(SCHEMA_ENTITIES_SQL, SCHEMA_ENTITIES_IDX_SQL);
} }
@ -59,4 +67,26 @@ public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSer
"ALTER TABLE customer ADD CONSTRAINT customer_title_unq_key UNIQUE(tenant_id, title); END IF; END; $$;", "ALTER TABLE customer ADD CONSTRAINT customer_title_unq_key UNIQUE(tenant_id, title); END IF; END; $$;",
"create 'customer_title_unq_key' constraint if it doesn't already exist!"); "create 'customer_title_unq_key' constraint if it doesn't already exist!");
} }
@Override
public void createSchemaVersion() {
try {
Long schemaVersion = jdbcTemplate.queryForList("SELECT schema_version FROM tb_schema_settings", Long.class).stream().findFirst().orElse(null);
if (schemaVersion == null) {
jdbcTemplate.execute("INSERT INTO tb_schema_settings (schema_version) VALUES (" + getSchemaVersion() + ")");
}
} catch (Exception e) {
log.warn("Failed to create schema version [{}]!", buildProperties.getVersion(), e);
}
}
private int getSchemaVersion() {
String[] versionParts = buildProperties.getVersion().replaceAll("[^\\d.]", "").split("\\.");
int major = Integer.parseInt(versionParts[0]);
int minor = Integer.parseInt(versionParts[1]);
int patch = versionParts.length > 2 ? Integer.parseInt(versionParts[2]) : 0;
return major * 1000000 + minor * 1000 + patch;
}
} }

4
application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseSchemaService.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.service.install; package org.thingsboard.server.service.install;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.util.SqlTsDao; import org.thingsboard.server.dao.util.SqlTsDao;
@ -25,9 +24,6 @@ import org.thingsboard.server.dao.util.SqlTsDao;
@Profile("install") @Profile("install")
public class SqlTsDatabaseSchemaService extends SqlAbstractDatabaseSchemaService implements TsDatabaseSchemaService { public class SqlTsDatabaseSchemaService extends SqlAbstractDatabaseSchemaService implements TsDatabaseSchemaService {
@Value("${sql.postgres.ts_key_value_partitioning:MONTHS}")
private String partitionType;
public SqlTsDatabaseSchemaService() { public SqlTsDatabaseSchemaService() {
super("schema-ts-psql.sql", null); super("schema-ts-psql.sql", null);
} }

23
application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java

@ -443,17 +443,16 @@ public class DefaultMailService implements MailService {
} }
} }
private void sendMailWithTimeout(JavaMailSender mailSender, MimeMessage msg, long timeout) { private void sendMailWithTimeout(JavaMailSender mailSender, MimeMessage msg, long timeout) throws ThingsboardException {
var submittedMail = Futures.withTimeout( var submittedMail = Futures.withTimeout(
mailExecutorService.submit(() -> mailSender.send(msg)), mailExecutorService.submit(() -> mailSender.send(msg)),
timeout, TimeUnit.MILLISECONDS, timeoutScheduler); timeout, TimeUnit.MILLISECONDS, timeoutScheduler);
try { try {
submittedMail.get(timeout, TimeUnit.MILLISECONDS); submittedMail.get(timeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) { } catch (TimeoutException e) {
log.debug("Error during mail submission", e);
throw new RuntimeException("Timeout!"); throw new RuntimeException("Timeout!");
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(ExceptionUtils.getRootCause(e)); throw new ThingsboardException("Unable to send mail", ExceptionUtils.getRootCause(e), ThingsboardErrorCode.GENERAL);
} }
} }
@ -463,20 +462,20 @@ public class DefaultMailService implements MailService {
Template template = freemarkerConfig.getTemplate(templateLocation); Template template = freemarkerConfig.getTemplate(templateLocation);
return FreeMarkerTemplateUtils.processTemplateIntoString(template, model); return FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); log.warn("Failed to process mail template: {}", ExceptionUtils.getRootCauseMessage(e));
throw new ThingsboardException("Failed to process mail template: " + e.getMessage(), e, ThingsboardErrorCode.GENERAL);
} }
} }
protected ThingsboardException handleException(Exception exception) { protected ThingsboardException handleException(Throwable exception) {
String message; if (exception instanceof ThingsboardException thingsboardException) {
return thingsboardException;
}
if (exception instanceof NestedRuntimeException) { if (exception instanceof NestedRuntimeException) {
message = ((NestedRuntimeException) exception).getMostSpecificCause().getMessage(); exception = ((NestedRuntimeException) exception).getMostSpecificCause();
} else {
message = exception.getMessage();
} }
log.warn("Unable to send mail: {}", message); log.warn("Unable to send mail: {}", exception.getMessage());
return new ThingsboardException(String.format("Unable to send mail: %s", message), return new ThingsboardException("Unable to send mail: " + exception.getMessage(), ThingsboardErrorCode.GENERAL);
ThingsboardErrorCode.GENERAL);
} }
} }

6
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/EmailTwoFaProvider.java

@ -57,7 +57,11 @@ public class EmailTwoFaProvider extends OtpBasedTwoFaProvider<EmailTwoFaProvider
@Override @Override
protected void sendVerificationCode(SecurityUser user, String verificationCode, EmailTwoFaProviderConfig providerConfig, EmailTwoFaAccountConfig accountConfig) throws ThingsboardException { protected void sendVerificationCode(SecurityUser user, String verificationCode, EmailTwoFaProviderConfig providerConfig, EmailTwoFaAccountConfig accountConfig) throws ThingsboardException {
mailService.sendTwoFaVerificationEmail(accountConfig.getEmail(), verificationCode, providerConfig.getVerificationCodeLifetime()); try {
mailService.sendTwoFaVerificationEmail(accountConfig.getEmail(), verificationCode, providerConfig.getVerificationCodeLifetime());
} catch (Exception e) {
throw new ThingsboardException("Couldn't send 2FA verification email", ThingsboardErrorCode.GENERAL);
}
} }
@Override @Override

2
application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java

@ -265,7 +265,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
} }
} }
if (actionType == ActionType.LOGIN && e == null) { if (actionType == ActionType.LOGIN && e == null) {
userService.setLastLoginTs(user.getTenantId(), user.getId()); userService.updateLastLoginTs(user.getTenantId(), user.getId());
} }
auditLogService.logEntityAction( auditLogService.logEntityAction(
user.getTenantId(), user.getCustomerId(), user.getId(), user.getTenantId(), user.getCustomerId(), user.getId(),

39
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java

@ -282,7 +282,6 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
if (sessionSubscriptions != null) { if (sessionSubscriptions != null) {
TbSubscription<?> subscription = sessionSubscriptions.remove(subscriptionId); TbSubscription<?> subscription = sessionSubscriptions.remove(subscriptionId);
if (subscription != null) { if (subscription != null) {
if (sessionSubscriptions.isEmpty()) { if (sessionSubscriptions.isEmpty()) {
subscriptionsBySessionId.remove(sessionId); subscriptionsBySessionId.remove(sessionId);
} }
@ -304,22 +303,26 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
@Override @Override
public void cancelAllSessionSubscriptions(TenantId tenantId, String sessionId) { public void cancelAllSessionSubscriptions(TenantId tenantId, String sessionId) {
log.debug("[{}][{}] Going to remove session subscriptions.", tenantId, sessionId); log.debug("[{}][{}] Going to remove session subscriptions.", tenantId, sessionId);
List<SubscriptionModificationResult> results = new ArrayList<>();
Lock subsLock = getSubsLock(tenantId); Lock subsLock = getSubsLock(tenantId);
subsLock.lock(); subsLock.lock();
try { try {
Map<Integer, TbSubscription<?>> sessionSubscriptions = subscriptionsBySessionId.remove(sessionId); Map<Integer, TbSubscription<?>> sessionSubscriptions = subscriptionsBySessionId.remove(sessionId);
if (sessionSubscriptions != null) { if (sessionSubscriptions != null) {
for (TbSubscription<?> subscription : sessionSubscriptions.values()) { Map<EntityId, List<TbSubscription<?>>> entitySubscriptions =
results.add(modifySubscription(tenantId, subscription.getEntityId(), subscription, false)); sessionSubscriptions.values().stream().collect(Collectors.groupingBy(TbSubscription::getEntityId));
}
entitySubscriptions.forEach((entityId, subscriptions) -> {
TbEntitySubEvent event = removeAllSubscriptions(tenantId, entityId, subscriptions);
if (event != null) {
pushSubscriptionsEvent(tenantId, entityId, event);
}
});
} else { } else {
log.debug("[{}][{}] No session subscriptions found!", tenantId, sessionId); log.debug("[{}][{}] No session subscriptions found!", tenantId, sessionId);
} }
} finally { } finally {
subsLock.unlock(); subsLock.unlock();
} }
results.stream().filter(SubscriptionModificationResult::hasEvent).forEach(this::pushSubscriptionEvent);
} }
@Override @Override
@ -500,6 +503,30 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
return new SubscriptionModificationResult(tenantId, entityId, subscription, missedUpdatesCandidate, event); return new SubscriptionModificationResult(tenantId, entityId, subscription, missedUpdatesCandidate, event);
} }
private TbEntitySubEvent removeAllSubscriptions(TenantId tenantId, EntityId entityId, List<TbSubscription<?>> subscriptions) {
TbEntitySubEvent event = null;
try {
TbEntityLocalSubsInfo entitySubs = subscriptionsByEntityId.get(entityId.getId());
event = entitySubs.removeAll(subscriptions);
if (entitySubs.isEmpty()) {
subscriptionsByEntityId.remove(entityId.getId());
entityUpdates.remove(entityId.getId());
}
} catch (Exception e) {
log.warn("[{}][{}] Failed to remove all subscriptions {} due to ", tenantId, entityId, subscriptions, e);
}
return event;
}
private void pushSubscriptionsEvent(TenantId tenantId, EntityId entityId, TbEntitySubEvent event) {
try {
log.trace("[{}][{}] Event: {}", tenantId, entityId, event);
pushSubEventToManagerService(tenantId, entityId, event);
} catch (Exception e) {
log.warn("[{}][{}] Failed to push subscription event {} due to ", tenantId, entityId, event, e);
}
}
private void pushSubscriptionEvent(SubscriptionModificationResult modificationResult) { private void pushSubscriptionEvent(SubscriptionModificationResult modificationResult) {
try { try {
TbEntitySubEvent event = modificationResult.getEvent(); TbEntitySubEvent event = modificationResult.getEvent();

64
application/src/main/java/org/thingsboard/server/service/subscription/TbEntityLocalSubsInfo.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -129,13 +130,64 @@ public class TbEntityLocalSubsInfo {
if (!subs.remove(sub)) { if (!subs.remove(sub)) {
return null; return null;
} }
if (subs.isEmpty()) { if (isEmpty()) {
return toEvent(ComponentLifecycleEvent.DELETED); return toEvent(ComponentLifecycleEvent.DELETED);
} }
TbSubscriptionsInfo oldState = state.copy(); TbSubscriptionType type = sub.getType();
TbSubscriptionsInfo newState = new TbSubscriptionsInfo(); TbSubscriptionsInfo newState = state.copy();
clearState(newState, type);
return updateState(Set.of(type), newState);
}
public TbEntitySubEvent removeAll(List<? extends TbSubscription<?>> subsToRemove) {
Set<TbSubscriptionType> changedTypes = new HashSet<>();
TbSubscriptionsInfo newState = state.copy();
for (TbSubscription<?> sub : subsToRemove) {
log.trace("[{}][{}][{}] Removing: {}", tenantId, entityId, sub.getSubscriptionId(), sub);
if (!subs.remove(sub)) {
continue;
}
if (isEmpty()) {
return toEvent(ComponentLifecycleEvent.DELETED);
}
TbSubscriptionType type = sub.getType();
if (changedTypes.contains(type)) {
continue;
}
clearState(newState, type);
changedTypes.add(type);
}
return updateState(changedTypes, newState);
}
private void clearState(TbSubscriptionsInfo state, TbSubscriptionType type) {
switch (type) {
case NOTIFICATIONS:
case NOTIFICATIONS_COUNT:
state.notifications = false;
break;
case ALARMS:
state.alarms = false;
break;
case ATTRIBUTES:
state.attrAllKeys = false;
state.attrKeys = null;
break;
case TIMESERIES:
state.tsAllKeys = false;
state.tsKeys = null;
}
}
private TbEntitySubEvent updateState(Set<TbSubscriptionType> updatedTypes, TbSubscriptionsInfo newState) {
for (TbSubscription<?> subscription : subs) { for (TbSubscription<?> subscription : subs) {
switch (subscription.getType()) { TbSubscriptionType type = subscription.getType();
if (!updatedTypes.contains(type)) {
continue;
}
switch (type) {
case NOTIFICATIONS: case NOTIFICATIONS:
case NOTIFICATIONS_COUNT: case NOTIFICATIONS_COUNT:
if (!newState.notifications) { if (!newState.notifications) {
@ -173,7 +225,7 @@ public class TbEntityLocalSubsInfo {
break; break;
} }
} }
if (newState.equals(oldState)) { if (newState.equals(state)) {
return null; return null;
} else { } else {
this.state = newState; this.state = newState;
@ -196,7 +248,7 @@ public class TbEntityLocalSubsInfo {
public boolean isEmpty() { public boolean isEmpty() {
return state.isEmpty(); return subs.isEmpty();
} }
public TbSubscription<?> registerPendingSubscription(TbSubscription<?> subscription, TbEntitySubEvent event) { public TbSubscription<?> registerPendingSubscription(TbSubscription<?> subscription, TbEntitySubEvent event) {

172
application/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java

@ -0,0 +1,172 @@
/**
* 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.
*/
package org.thingsboard.server.service.sync;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.GitRepository;
import org.thingsboard.server.service.sync.vc.GitRepository.FileType;
import org.thingsboard.server.service.sync.vc.GitRepository.RepoFile;
import java.net.URI;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@TbCoreComponent
@Service
@Slf4j
public class DefaultGitSyncService implements GitSyncService {
@Value("${vc.git.repositories-folder:${java.io.tmpdir}/repositories}")
private String repositoriesFolder;
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("git-sync"));
private final Map<String, GitRepository> repositories = new ConcurrentHashMap<>();
private final Map<String, Runnable> updateListeners = new ConcurrentHashMap<>();
@Override
public void registerSync(String key, String repoUri, String branch, long fetchFrequencyMs, Runnable onUpdate) {
RepositorySettings settings = new RepositorySettings();
settings.setRepositoryUri(repoUri);
settings.setDefaultBranch(branch);
if (onUpdate != null) {
updateListeners.put(key, onUpdate);
}
executor.execute(() -> {
initRepository(key, settings);
});
executor.scheduleWithFixedDelay(() -> {
GitRepository repository = repositories.get(key);
if (repository == null || !GitRepository.exists(repository.getDirectory())) {
initRepository(key, settings);
return;
}
try {
log.debug("[{}] Fetching repository", key);
boolean updated = repository.fetch();
if (updated) {
onUpdate(key);
} else {
log.debug("[{}] No changes in the repository", key);
}
} catch (Throwable e) {
log.error("[{}] Failed to fetch repository", key, e);
}
}, fetchFrequencyMs, fetchFrequencyMs, TimeUnit.MILLISECONDS);
}
@Override
public List<RepoFile> listFiles(String key, String path, int depth, FileType type) {
GitRepository repository = getRepository(key);
return repository.listFilesAtCommit(getBranchRef(repository), path, depth).stream()
.filter(file -> type == null || file.type() == type)
.toList();
}
@Override
public String getFileContent(String key, String path) {
GitRepository repository = getRepository(key);
try {
return repository.getFileContentAtCommit(path, getBranchRef(repository));
} catch (Exception e) {
log.warn("[{}] Failed to get file content for path {}: {}", key, path, e.getMessage());
return "{}";
}
}
@Override
public String getGithubRawContentUrl(String key, String path) {
if (path == null) {
return "";
}
RepositorySettings settings = getRepository(key).getSettings();
return StringUtils.removeEnd(settings.getRepositoryUri(), ".git") + "/blob/" + settings.getDefaultBranch() + "/" + path + "?raw=true";
}
private GitRepository getRepository(String key) {
GitRepository repository = repositories.get(key);
if (repository != null) {
if (!GitRepository.exists(repository.getDirectory())) {
// reinitializing the repository because folder was deleted
initRepository(key, repository.getSettings());
}
}
repository = repositories.get(key);
if (repository == null) {
throw new IllegalStateException(key + " repository is not initialized");
}
return repository;
}
private void initRepository(String key, RepositorySettings settings) {
try {
repositories.remove(key);
Path directory = getRepoDirectory(settings);
GitRepository repository = GitRepository.openOrClone(directory, settings, true);
repositories.put(key, repository);
log.info("[{}] Initialized repository", key);
onUpdate(key);
} catch (Throwable e) {
log.error("[{}] Failed to initialize repository with settings {}", key, settings, e);
}
}
private void onUpdate(String key) {
Runnable listener = updateListeners.get(key);
if (listener != null) {
log.debug("[{}] Handling repository update", key);
try {
listener.run();
} catch (Throwable e) {
log.error("[{}] Failed to handle repository update", key, e);
}
}
}
private Path getRepoDirectory(RepositorySettings settings) {
// using uri to define folder name in case repo url is changed
String name = URI.create(settings.getRepositoryUri()).getPath().replaceAll("[^a-zA-Z]", "");
return Path.of(repositoriesFolder, name);
}
private String getBranchRef(GitRepository repository) {
return "refs/remotes/origin/" + repository.getSettings().getDefaultBranch();
}
@PreDestroy
private void preDestroy() {
executor.shutdownNow();
}
}

42
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.scss → application/src/main/java/org/thingsboard/server/service/sync/GitSyncService.java

@ -13,45 +13,21 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host { package org.thingsboard.server.service.sync;
width: 100%;
height: 100%;
display: block;
.tb-form-row { import org.thingsboard.server.service.sync.vc.GitRepository.FileType;
&.bottom-same-padding { import org.thingsboard.server.service.sync.vc.GitRepository.RepoFile;
padding-bottom: 16px;
}
&.top-same-padding { import java.util.List;
padding-top: 16px;
}
.fixed-title-width { public interface GitSyncService {
width: 19%;
}
}
.table-column { void registerSync(String key, String repoUri, String branch, long fetchFrequencyMs, Runnable onUpdate);
width: 40%;
}
.table-name-column { List<RepoFile> listFiles(String key, String path, int depth, FileType type);
width: 20%;
}
.raw-name { String getFileContent(String key, String path);
width: 19%;
}
.raw-value-option { String getGithubRawContentUrl(String key, String path);
max-width: 40%;
}
} }
:host ::ng-deep {
.mat-mdc-form-field-icon-suffix {
display: flex;
}
}

11
application/src/main/resources/thingsboard.yml

@ -1247,6 +1247,17 @@ transport:
enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" enabled: "${TB_TRANSPORT_STATS_ENABLED:true}"
# Interval of transport statistics logging # Interval of transport statistics logging
print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}"
gateway:
dashboard:
sync:
# Enable/disable gateways dashboard sync with git repository
enabled: "${TB_GATEWAY_DASHBOARD_SYNC_ENABLED:true}"
# URL of gateways dashboard repository
repository_url: "${TB_GATEWAY_DASHBOARD_SYNC_REPOSITORY_URL:https://github.com/thingsboard/gateway-management-extensions-dist.git}"
# Branch of gateways dashboard repository to work with
branch: "${TB_GATEWAY_DASHBOARD_SYNC_BRANCH:main}"
# Fetch frequency in hours for gateways dashboard repository
fetch_frequency: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}"
# CoAP server parameters # CoAP server parameters
coap: coap:

82
application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java

@ -27,6 +27,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserActivationLink; import org.thingsboard.server.common.data.UserActivationLink;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.SecuritySettings;
@ -67,31 +68,30 @@ public class AuthControllerTest extends AbstractControllerTest {
.andExpect(status().isUnauthorized()); .andExpect(status().isUnauthorized());
loginSysAdmin(); loginSysAdmin();
doGet("/api/auth/user") User user = getCurrentUser();
.andExpect(status().isOk()) assertThat(user.getAuthority()).isEqualTo(Authority.SYS_ADMIN);
.andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name()))) assertThat(user.getEmail()).isEqualTo(SYS_ADMIN_EMAIL);
.andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
loginTenantAdmin(); loginTenantAdmin();
doGet("/api/auth/user") user = getCurrentUser();
.andExpect(status().isOk()) assertThat(user.getAuthority()).isEqualTo(Authority.TENANT_ADMIN);
.andExpect(jsonPath("$.authority", is(Authority.TENANT_ADMIN.name()))) assertThat(user.getEmail()).isEqualTo(TENANT_ADMIN_EMAIL);
.andExpect(jsonPath("$.email", is(TENANT_ADMIN_EMAIL)));
loginCustomerUser(); loginCustomerUser();
doGet("/api/auth/user") user = getCurrentUser();
.andExpect(status().isOk()) assertThat(user.getAuthority()).isEqualTo(Authority.CUSTOMER_USER);
.andExpect(jsonPath("$.authority", is(Authority.CUSTOMER_USER.name()))) assertThat(user.getEmail()).isEqualTo(CUSTOMER_USER_EMAIL);
.andExpect(jsonPath("$.email", is(CUSTOMER_USER_EMAIL))); user = getUser(customerUserId);
assertThat(user.getAdditionalInfo().get("userCredentialsEnabled").asBoolean()).isTrue();
assertThat(user.getAdditionalInfo().get("lastLoginTs").asLong()).isCloseTo(System.currentTimeMillis(), within(10000L));
} }
@Test @Test
public void testLoginLogout() throws Exception { public void testLoginLogout() throws Exception {
loginSysAdmin(); loginSysAdmin();
doGet("/api/auth/user") User user = getCurrentUser();
.andExpect(status().isOk()) assertThat(user.getAuthority()).isEqualTo(Authority.SYS_ADMIN);
.andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name()))) assertThat(user.getEmail()).isEqualTo(SYS_ADMIN_EMAIL);
.andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
TimeUnit.SECONDS.sleep(1); //We need to make sure that event for invalidating token was successfully processed TimeUnit.SECONDS.sleep(1); //We need to make sure that event for invalidating token was successfully processed
@ -102,19 +102,45 @@ public class AuthControllerTest extends AbstractControllerTest {
resetTokens(); resetTokens();
} }
@Test
public void testFailedLogin() throws Exception {
int maxFailedLoginAttempts = 3;
loginSysAdmin();
updateSecuritySettings(securitySettings -> {
securitySettings.setMaxFailedLoginAttempts(maxFailedLoginAttempts);
});
loginTenantAdmin();
for (int i = 0; i < maxFailedLoginAttempts; i++) {
String error = getErrorMessage(doPost("/api/auth/login",
new LoginRequest(CUSTOMER_USER_EMAIL, "IncorrectPassword"))
.andExpect(status().isUnauthorized()));
assertThat(error).containsIgnoringCase("invalid username or password");
}
User user = getUser(customerUserId);
assertThat(user.getAdditionalInfo().get("userCredentialsEnabled").asBoolean()).isTrue();
String error = getErrorMessage(doPost("/api/auth/login",
new LoginRequest(CUSTOMER_USER_EMAIL, "IncorrectPassword4"))
.andExpect(status().isUnauthorized()));
assertThat(error).containsIgnoringCase("account is locked");
user = getUser(customerUserId);
assertThat(user.getAdditionalInfo().get("userCredentialsEnabled").asBoolean()).isFalse();
}
@Test @Test
public void testRefreshToken() throws Exception { public void testRefreshToken() throws Exception {
loginSysAdmin(); loginSysAdmin();
doGet("/api/auth/user") User user = getCurrentUser();
.andExpect(status().isOk()) assertThat(user.getAuthority()).isEqualTo(Authority.SYS_ADMIN);
.andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name()))) assertThat(user.getEmail()).isEqualTo(SYS_ADMIN_EMAIL);
.andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
refreshToken(); refreshToken();
doGet("/api/auth/user") user = getCurrentUser();
.andExpect(status().isOk()) assertThat(user.getAuthority()).isEqualTo(Authority.SYS_ADMIN);
.andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name()))) assertThat(user.getEmail()).isEqualTo(SYS_ADMIN_EMAIL);
.andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
} }
@Test @Test
@ -277,6 +303,14 @@ public class AuthControllerTest extends AbstractControllerTest {
doPost("/api/admin/securitySettings", securitySettings).andExpect(status().isOk()); doPost("/api/admin/securitySettings", securitySettings).andExpect(status().isOk());
} }
private User getCurrentUser() throws Exception {
return doGet("/api/auth/user", User.class);
}
private User getUser(UserId id) throws Exception {
return doGet("/api/user/" + id, User.class);
}
private String getActivationLink(User user) throws Exception { private String getActivationLink(User user) throws Exception {
return doGet("/api/user/" + user.getId() + "/activationLink", String.class); return doGet("/api/user/" + user.getId() + "/activationLink", String.class);
} }

4
application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java

@ -319,7 +319,9 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
assertThat(successfulLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.SUCCESS); assertThat(successfulLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.SUCCESS);
assertThat(successfulLogInAuditLog.getUserName()).isEqualTo(username); assertThat(successfulLogInAuditLog.getUserName()).isEqualTo(username);
}); });
assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo()
loginTenantAdmin();
assertThat(doGet("/api/user/" + user.getId(), User.class).getAdditionalInfo()
.get("lastLoginTs").asLong()) .get("lastLoginTs").asLong())
.isGreaterThan(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(3)); .isGreaterThan(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(3));
} }

2
application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java

@ -113,6 +113,7 @@ public class UserControllerTest extends AbstractControllerTest {
Assert.assertEquals(email, savedUser.getEmail()); Assert.assertEquals(email, savedUser.getEmail());
User foundUser = doGet("/api/user/" + savedUser.getId().getId().toString(), User.class); User foundUser = doGet("/api/user/" + savedUser.getId().getId().toString(), User.class);
foundUser.setAdditionalInfo(savedUser.getAdditionalInfo());
Assert.assertEquals(foundUser, savedUser); Assert.assertEquals(foundUser, savedUser);
testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(foundUser, foundUser, testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(foundUser, foundUser,
@ -265,6 +266,7 @@ public class UserControllerTest extends AbstractControllerTest {
User savedUser = doPost("/api/user", user, User.class); User savedUser = doPost("/api/user", user, User.class);
User foundUser = doGet("/api/user/" + savedUser.getId().getId().toString(), User.class); User foundUser = doGet("/api/user/" + savedUser.getId().getId().toString(), User.class);
Assert.assertNotNull(foundUser); Assert.assertNotNull(foundUser);
foundUser.setAdditionalInfo(savedUser.getAdditionalInfo());
Assert.assertEquals(savedUser, foundUser); Assert.assertEquals(savedUser, foundUser);
} }

16
application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java

@ -47,7 +47,7 @@ public class UserEdgeTest extends AbstractEdgeTest {
@Test @Test
public void testCreateUpdateDeleteTenantUser() throws Exception { public void testCreateUpdateDeleteTenantUser() throws Exception {
// create user // create user
edgeImitator.expectMessageAmount(6); edgeImitator.expectMessageAmount(3);
User newTenantAdmin = new User(); User newTenantAdmin = new User();
newTenantAdmin.setAuthority(Authority.TENANT_ADMIN); newTenantAdmin.setAuthority(Authority.TENANT_ADMIN);
newTenantAdmin.setTenantId(tenantId); newTenantAdmin.setTenantId(tenantId);
@ -55,9 +55,9 @@ public class UserEdgeTest extends AbstractEdgeTest {
newTenantAdmin.setFirstName("Boris"); newTenantAdmin.setFirstName("Boris");
newTenantAdmin.setLastName("Johnson"); newTenantAdmin.setLastName("Johnson");
User savedTenantAdmin = createUser(newTenantAdmin, "tenant"); User savedTenantAdmin = createUser(newTenantAdmin, "tenant");
Assert.assertTrue(edgeImitator.waitForMessages()); // wait 6 messages - x2 user update msg and x4 user credentials update msgs (create + authenticate user) Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user)
Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size());
Assert.assertEquals(4, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size());
Optional<UserUpdateMsg> userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); Optional<UserUpdateMsg> userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class);
Assert.assertTrue(userUpdateMsgOpt.isPresent()); Assert.assertTrue(userUpdateMsgOpt.isPresent());
UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get(); UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get();
@ -133,7 +133,7 @@ public class UserEdgeTest extends AbstractEdgeTest {
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
// create user // create user
edgeImitator.expectMessageAmount(6); edgeImitator.expectMessageAmount(3);
User customerUser = new User(); User customerUser = new User();
customerUser.setAuthority(Authority.CUSTOMER_USER); customerUser.setAuthority(Authority.CUSTOMER_USER);
customerUser.setTenantId(tenantId); customerUser.setTenantId(tenantId);
@ -142,9 +142,9 @@ public class UserEdgeTest extends AbstractEdgeTest {
customerUser.setFirstName("John"); customerUser.setFirstName("John");
customerUser.setLastName("Edwards"); customerUser.setLastName("Edwards");
User savedCustomerUser = createUser(customerUser, "customer"); User savedCustomerUser = createUser(customerUser, "customer");
Assert.assertTrue(edgeImitator.waitForMessages()); // wait 6 messages - x2 user update msg and x4 user credentials update msgs (create + authenticate user) Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user)
Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size());
Assert.assertEquals(4, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size());
Optional<UserUpdateMsg> userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); Optional<UserUpdateMsg> userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class);
Assert.assertTrue(userUpdateMsgOpt.isPresent()); Assert.assertTrue(userUpdateMsgOpt.isPresent());
UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get(); UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get();

74
application/src/test/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncServiceTest.java

@ -0,0 +1,74 @@
/**
* 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.
*/
package org.thingsboard.server.service.entitiy.dashboard;
import org.junit.After;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.sql.resource.TbResourceRepository;
import org.thingsboard.server.dao.sql.widget.WidgetTypeRepository;
import org.thingsboard.server.dao.sql.widget.WidgetsBundleRepository;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
@TestPropertySource(properties = {
"transport.gateway.dashboard.sync.enabled=true"
})
public class DashboardSyncServiceTest extends AbstractControllerTest {
@Autowired
private WidgetTypeRepository widgetTypeRepository;
@Autowired
private WidgetsBundleRepository widgetsBundleRepository;
@Autowired
private TbResourceRepository resourceRepository;
@After
public void after() throws Exception {
widgetsBundleRepository.deleteAll();
widgetTypeRepository.deleteAll();
resourceRepository.deleteAll();
}
@Test
public void testGatewaysDashboardSync() throws Exception {
loginTenantAdmin();
await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> {
MockHttpServletResponse response = doGet("/api/resource/dashboard/system/gateways_dashboard.json")
.andExpect(status().isOk())
.andReturn().getResponse();
String dashboardJson = response.getContentAsString();
String etag = response.getHeader("ETag");
Dashboard dashboard = JacksonUtil.fromString(dashboardJson, Dashboard.class);
assertThat(dashboard).isNotNull();
assertThat(dashboard.getTitle()).containsIgnoringCase("gateway");
assertThat(etag).isNotBlank();
});
}
}

5
application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java

@ -39,7 +39,6 @@ import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.service.install.update.ImagesUpdater; import org.thingsboard.server.service.install.update.ImagesUpdater;
import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -87,14 +86,14 @@ class InstallScriptsTest {
} }
@Test @Test
void testDefaultRuleChainsTemplates() throws IOException { void testDefaultRuleChainsTemplates() {
Path dir = installScripts.getTenantRuleChainsDir(); Path dir = installScripts.getTenantRuleChainsDir();
installScripts.findRuleChainsFromPath(dir) installScripts.findRuleChainsFromPath(dir)
.forEach(this::validateRuleChainTemplate); .forEach(this::validateRuleChainTemplate);
} }
@Test @Test
void testDefaultEdgeRuleChainsTemplates() throws IOException { void testDefaultEdgeRuleChainsTemplates() {
Path dir = installScripts.getEdgeRuleChainsDir(); Path dir = installScripts.getEdgeRuleChainsDir();
installScripts.findRuleChainsFromPath(dir) installScripts.findRuleChainsFromPath(dir)
.forEach(this::validateRuleChainTemplate); .forEach(this::validateRuleChainTemplate);

186
application/src/test/java/org/thingsboard/server/service/subscription/TbEntityLocalSubsInfoTest.java

@ -0,0 +1,186 @@
/**
* 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.
*/
package org.thingsboard.server.service.subscription;
import org.junit.jupiter.api.Test;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TbEntityLocalSubsInfoTest {
@Test
public void addTest() {
Set<TbAttributeSubscription> expectedSubs = new HashSet<>();
TbEntityLocalSubsInfo subsInfo = createSubsInfo();
TenantId tenantId = subsInfo.getTenantId();
EntityId entityId = subsInfo.getEntityId();
TbAttributeSubscription attrSubscription1 = TbAttributeSubscription.builder()
.sessionId("session1")
.tenantId(tenantId)
.entityId(entityId)
.keyStates(Map.of("key1", 1L, "key2", 2L))
.build();
expectedSubs.add(attrSubscription1);
TbEntitySubEvent created = subsInfo.add(attrSubscription1);
assertFalse(subsInfo.isEmpty());
assertNotNull(created);
assertEquals(expectedSubs, subsInfo.getSubs());
checkEvent(created, expectedSubs, ComponentLifecycleEvent.CREATED);
assertNull(subsInfo.add(attrSubscription1));
TbAttributeSubscription attrSubscription2 = TbAttributeSubscription.builder()
.sessionId("session2")
.tenantId(tenantId)
.entityId(entityId)
.keyStates(Map.of("key3", 3L, "key4", 4L))
.build();
expectedSubs.add(attrSubscription2);
TbEntitySubEvent updated = subsInfo.add(attrSubscription2);
assertNotNull(updated);
assertEquals(expectedSubs, subsInfo.getSubs());
checkEvent(updated, expectedSubs, ComponentLifecycleEvent.UPDATED);
}
@Test
public void removeTest() {
Set<TbAttributeSubscription> expectedSubs = new HashSet<>();
TbEntityLocalSubsInfo subsInfo = createSubsInfo();
TenantId tenantId = subsInfo.getTenantId();
EntityId entityId = subsInfo.getEntityId();
TbAttributeSubscription attrSubscription1 = TbAttributeSubscription.builder()
.sessionId("session1")
.tenantId(tenantId)
.entityId(entityId)
.keyStates(Map.of("key1", 1L, "key2", 2L))
.build();
TbAttributeSubscription attrSubscription2 = TbAttributeSubscription.builder()
.sessionId("session2")
.tenantId(tenantId)
.entityId(entityId)
.keyStates(Map.of("key3", 3L, "key4", 4L))
.build();
expectedSubs.add(attrSubscription1);
expectedSubs.add(attrSubscription2);
subsInfo.add(attrSubscription1);
subsInfo.add(attrSubscription2);
assertEquals(expectedSubs, subsInfo.getSubs());
TbEntitySubEvent updatedEvent = subsInfo.remove(attrSubscription1);
expectedSubs.remove(attrSubscription1);
assertNotNull(updatedEvent);
assertEquals(expectedSubs, subsInfo.getSubs());
checkEvent(updatedEvent, expectedSubs, ComponentLifecycleEvent.UPDATED);
TbEntitySubEvent deletedEvent = subsInfo.remove(attrSubscription2);
expectedSubs.remove(attrSubscription2);
assertNotNull(deletedEvent);
assertEquals(expectedSubs, subsInfo.getSubs());
checkEvent(deletedEvent, expectedSubs, ComponentLifecycleEvent.DELETED);
assertTrue(subsInfo.isEmpty());
}
@Test
public void removeAllTest() {
TbEntityLocalSubsInfo subsInfo = createSubsInfo();
TenantId tenantId = subsInfo.getTenantId();
EntityId entityId = subsInfo.getEntityId();
TbAttributeSubscription attrSubscription1 = TbAttributeSubscription.builder()
.sessionId("session1")
.tenantId(tenantId)
.entityId(entityId)
.keyStates(Map.of("key1", 1L, "key2", 2L))
.build();
TbAttributeSubscription attrSubscription2 = TbAttributeSubscription.builder()
.sessionId("session2")
.tenantId(tenantId)
.entityId(entityId)
.keyStates(Map.of("key3", 3L, "key4", 4L))
.build();
TbAttributeSubscription attrSubscription3 = TbAttributeSubscription.builder()
.sessionId("session3")
.tenantId(tenantId)
.entityId(entityId)
.keyStates(Map.of("key5", 5L, "key6", 6L))
.build();
subsInfo.add(attrSubscription1);
subsInfo.add(attrSubscription2);
subsInfo.add(attrSubscription3);
assertFalse(subsInfo.isEmpty());
TbEntitySubEvent updatedEvent = subsInfo.removeAll(List.of(attrSubscription1, attrSubscription2));
assertNotNull(updatedEvent);
checkEvent(updatedEvent, Set.of(attrSubscription3), ComponentLifecycleEvent.UPDATED);
assertFalse(subsInfo.isEmpty());
TbEntitySubEvent deletedEvent = subsInfo.removeAll(List.of(attrSubscription3));
assertNotNull(deletedEvent);
checkEvent(deletedEvent, null, ComponentLifecycleEvent.DELETED);
assertTrue(subsInfo.isEmpty());
}
private TbEntityLocalSubsInfo createSubsInfo() {
return new TbEntityLocalSubsInfo(new TenantId(UUID.randomUUID()), new DeviceId(UUID.randomUUID()));
}
private void checkEvent(TbEntitySubEvent event, Set<TbAttributeSubscription> expectedSubs, ComponentLifecycleEvent expectedType) {
assertEquals(expectedType, event.getType());
TbSubscriptionsInfo info = event.getInfo();
if (event.getType() == ComponentLifecycleEvent.DELETED) {
assertNull(info);
return;
}
assertNotNull(info);
assertFalse(info.notifications);
assertFalse(info.alarms);
assertFalse(info.attrAllKeys);
assertFalse(info.tsAllKeys);
assertNull(info.tsKeys);
assertEquals(getAttrKeys(expectedSubs), info.attrKeys);
}
private Set<String> getAttrKeys(Set<TbAttributeSubscription> attributeSubscriptions) {
return attributeSubscriptions.stream().map(s -> s.getKeyStates().keySet()).flatMap(Collection::stream).collect(Collectors.toSet());
}
}

9
application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java

@ -114,6 +114,13 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC
CoapTestCallback callbackCoap = new TestCoapCallbackForRPC(client, false, protobuf); CoapTestCallback callbackCoap = new TestCoapCallbackForRPC(client, false, protobuf);
CoapObserveRelation observeRelation = client.getObserveRelation(callbackCoap); CoapObserveRelation observeRelation = client.getObserveRelation(callbackCoap);
String awaitAlias = "await Two Way Rpc (client.getObserveRelation)";
await(awaitAlias)
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS * 3, TimeUnit.SECONDS)
.until(() -> processTwoWayRpcTestWithAwait(callbackCoap, observeRelation, expectedResponseResult));
}
private boolean processTwoWayRpcTestWithAwait(CoapTestCallback callbackCoap, CoapObserveRelation observeRelation, String expectedResponseResult) throws Exception {
String awaitAlias = "await Two Way Rpc (client.getObserveRelation)"; String awaitAlias = "await Two Way Rpc (client.getObserveRelation)";
await(awaitAlias) await(awaitAlias)
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
@ -146,7 +153,7 @@ public abstract class AbstractCoapServerSideRpcIntegrationTest extends AbstractC
validateTwoWayStateChangedNotification(callbackCoap, expectedResponseResult, actualResult); validateTwoWayStateChangedNotification(callbackCoap, expectedResponseResult, actualResult);
observeRelation.proactiveCancel(); observeRelation.proactiveCancel();
assertTrue(observeRelation.isCanceled()); return observeRelation.isCanceled();
} }
protected void processOnLoadResponse(CoapResponse response, CoapTestClient client) { protected void processOnLoadResponse(CoapResponse response, CoapTestClient client) {

138
application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java

@ -21,15 +21,16 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.eclipse.leshan.client.LeshanClient;
import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.client.object.Security;
import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.server.registration.Registration;
import org.junit.After; import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
@ -54,6 +55,7 @@ import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingC
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.AbstractLwM2MBootstrapServerCredential; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.AbstractLwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.NoSecLwM2MBootstrapServerCredential; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.NoSecLwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataQuery;
@ -70,9 +72,9 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd;
import org.thingsboard.server.transport.AbstractTransportIntegrationTest; import org.thingsboard.server.transport.AbstractTransportIntegrationTest;
import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient; import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import java.io.IOException;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -88,6 +90,8 @@ import static org.awaitility.Awaitility.await;
import static org.eclipse.leshan.client.object.Security.noSec; import static org.eclipse.leshan.client.object.Security.noSec;
import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_STARTED; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_STARTED;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_SUCCESS; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_SUCCESS;
@ -107,7 +111,10 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfil
public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportIntegrationTest { public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportIntegrationTest {
@SpyBean @SpyBean
LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest; protected LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest;
@SpyBean
protected DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest;
@Autowired @Autowired
private LwM2mClientContext clientContextTest; private LwM2mClientContext clientContextTest;
@ -117,7 +124,6 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
public static final int securityPort = 5686; public static final int securityPort = 5686;
public static final int portBs = 5687; public static final int portBs = 5687;
public static final int securityPortBs = 5688; public static final int securityPortBs = 5688;
public static final int[] SERVERS_PORT_NUMBERS = {port, securityPort, portBs, securityPortBs};
public static final String host = "localhost"; public static final String host = "localhost";
public static final String hostBs = "localhost"; public static final String hostBs = "localhost";
@ -172,12 +178,10 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
protected final Set<Lwm2mTestHelper.LwM2MClientState> expectedStatusesRegistrationLwm2mSuccess = new HashSet<>(Arrays.asList(ON_INIT, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS)); protected final Set<Lwm2mTestHelper.LwM2MClientState> expectedStatusesRegistrationLwm2mSuccess = new HashSet<>(Arrays.asList(ON_INIT, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS));
protected final Set<Lwm2mTestHelper.LwM2MClientState> expectedStatusesRegistrationLwm2mSuccessUpdate = new HashSet<>(Arrays.asList(ON_INIT, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS, ON_UPDATE_STARTED, ON_UPDATE_SUCCESS)); protected final Set<Lwm2mTestHelper.LwM2MClientState> expectedStatusesRegistrationLwm2mSuccessUpdate = new HashSet<>(Arrays.asList(ON_INIT, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS, ON_UPDATE_STARTED, ON_UPDATE_SUCCESS));
protected final Set<Lwm2mTestHelper.LwM2MClientState> expectedStatusesRegistrationBsSuccess = new HashSet<>(Arrays.asList(ON_BOOTSTRAP_STARTED, ON_BOOTSTRAP_SUCCESS, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS)); protected final Set<Lwm2mTestHelper.LwM2MClientState> expectedStatusesRegistrationBsSuccess = new HashSet<>(Arrays.asList(ON_BOOTSTRAP_STARTED, ON_BOOTSTRAP_SUCCESS, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS));
protected DeviceProfile deviceProfile;
protected ScheduledExecutorService executor; protected ScheduledExecutorService executor;
protected LwM2MTestClient lwM2MTestClient; protected LwM2MTestClient lwM2MTestClient;
private String[] resources; private String[] resources;
protected String deviceId; protected String deviceId;
protected boolean isWriteAttribute = false;
protected boolean supportFormatOnly_SenMLJSON_SenMLCBOR = false; protected boolean supportFormatOnly_SenMLJSON_SenMLCBOR = false;
@Before @Before
@ -186,14 +190,11 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
} }
@After @After
public void after() { public void after() throws Exception {
clientDestroy(); this.clientDestroy(true);
executor.shutdownNow(); if (executor != null && !executor.isShutdown()) {
} executor.shutdownNow();
}
@AfterClass
public static void afterClass() {
awaitServersDestroy();
} }
private void init() throws Exception { private void init() throws Exception {
@ -218,8 +219,8 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
String endpoint, String endpoint,
boolean queueMode) throws Exception { boolean queueMode) throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS, getBootstrapServerCredentialsNoSec(NONE)); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS, getBootstrapServerCredentialsNoSec(NONE));
createDeviceProfile(transportConfiguration); DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + endpoint, transportConfiguration);
Device device = createDevice(deviceCredentials, endpoint); Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId());
SingleEntityFilter sef = new SingleEntityFilter(); SingleEntityFilter sef = new SingleEntityFilter();
sef.setSingleEntity(device.getId()); sef.setSingleEntity(device.getId());
@ -233,9 +234,8 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
getWsClient().waitForReply(); getWsClient().waitForReply();
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
createNewClient(security, null, false, endpoint, null, queueMode); this.createNewClient(security, null, false, endpoint, null, queueMode, device.getId().getId().toString());
deviceId = device.getId().getId().toString(); awaitObserveReadAll(0, lwM2MTestClient.getDeviceIdStr());
awaitObserveReadAll(0, deviceId);
String msg = getWsClient().waitForUpdate(); String msg = getWsClient().waitForUpdate();
EntityDataUpdate update = JacksonUtil.fromString(msg, EntityDataUpdate.class); EntityDataUpdate update = JacksonUtil.fromString(msg, EntityDataUpdate.class);
@ -255,29 +255,30 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
} }
protected void createDeviceProfile(Lwm2mDeviceProfileTransportConfiguration transportConfiguration) throws Exception { protected DeviceProfile createLwm2mDeviceProfile(String name, Lwm2mDeviceProfileTransportConfiguration transportConfiguration) throws Exception {
deviceProfile = new DeviceProfile(); DeviceProfile lwm2mDeviceProfile = new DeviceProfile();
deviceProfile.setName("LwM2M"); lwm2mDeviceProfile.setName(name);
deviceProfile.setType(DeviceProfileType.DEFAULT); lwm2mDeviceProfile.setType(DeviceProfileType.DEFAULT);
deviceProfile.setTenantId(tenantId); lwm2mDeviceProfile.setTenantId(tenantId);
deviceProfile.setTransportType(DeviceTransportType.LWM2M); lwm2mDeviceProfile.setTransportType(DeviceTransportType.LWM2M);
deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED); lwm2mDeviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
deviceProfile.setDescription(deviceProfile.getName()); lwm2mDeviceProfile.setDescription(name);
DeviceProfileData deviceProfileData = new DeviceProfileData(); DeviceProfileData deviceProfileData = new DeviceProfileData();
deviceProfileData.setConfiguration(new DefaultDeviceProfileConfiguration()); deviceProfileData.setConfiguration(new DefaultDeviceProfileConfiguration());
deviceProfileData.setProvisionConfiguration(new DisabledDeviceProfileProvisionConfiguration(null)); deviceProfileData.setProvisionConfiguration(new DisabledDeviceProfileProvisionConfiguration(null));
deviceProfileData.setTransportConfiguration(transportConfiguration); deviceProfileData.setTransportConfiguration(transportConfiguration);
deviceProfile.setProfileData(deviceProfileData); lwm2mDeviceProfile.setProfileData(deviceProfileData);
deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); lwm2mDeviceProfile = doPost("/api/deviceProfile", lwm2mDeviceProfile, DeviceProfile.class);
Assert.assertNotNull(deviceProfile); Assert.assertNotNull(lwm2mDeviceProfile);
return lwm2mDeviceProfile;
} }
protected Device createDevice(LwM2MDeviceCredentials credentials, String endpoint) throws Exception { protected Device createLwm2mDevice(LwM2MDeviceCredentials credentials, String endpoint, DeviceProfileId deviceProfileId) throws Exception {
Device device = new Device(); Device device = new Device();
device.setName(endpoint); device.setName(endpoint);
device.setDeviceProfileId(deviceProfile.getId()); device.setDeviceProfileId(deviceProfileId);
device.setTenantId(tenantId); device.setTenantId(tenantId);
device = doPost("/api/device", device, Device.class); device = doPost("/api/device", device, Device.class);
Assert.assertNotNull(device); Assert.assertNotNull(device);
@ -302,33 +303,37 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
} }
public void createNewClient(Security security, Security securityBs, boolean isRpc, public void createNewClient(Security security, Security securityBs, boolean isRpc,
String endpoint) throws Exception { String endpoint, String deviceIdStr) throws Exception {
this.createNewClient(security, securityBs, isRpc, endpoint, null, false); this.createNewClient(security, securityBs, isRpc, endpoint, null, false, deviceIdStr);
} }
public void createNewClient(Security security, Security securityBs, boolean isRpc, public void createNewClient(Security security, Security securityBs, boolean isRpc,
String endpoint, Integer clientDtlsCidLength) throws Exception { String endpoint, Integer clientDtlsCidLength, String deviceIdStr) throws Exception {
this.createNewClient(security, securityBs, isRpc, endpoint, clientDtlsCidLength, false); this.createNewClient(security, securityBs, isRpc, endpoint, clientDtlsCidLength, false, deviceIdStr);
} }
public void createNewClient(Security security, Security securityBs, boolean isRpc, public void createNewClient(Security security, Security securityBs, boolean isRpc,
String endpoint, Integer clientDtlsCidLength, boolean queueMode) throws Exception { String endpoint, Integer clientDtlsCidLength, boolean queueMode, String deviceIdStr) throws Exception {
this.clientDestroy(); this.clientDestroy(false);
lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint); lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint);
try (ServerSocket socket = new ServerSocket(0)) { try (ServerSocket socket = new ServerSocket(0)) {
int clientPort = socket.getLocalPort(); int clientPort = socket.getLocalPort();
lwM2MTestClient.init(security, securityBs, clientPort, isRpc, lwM2MTestClient.init(security, securityBs, clientPort, isRpc,
this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest, isWriteAttribute, this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest,
clientDtlsCidLength, queueMode, supportFormatOnly_SenMLJSON_SenMLCBOR); clientDtlsCidLength, queueMode, supportFormatOnly_SenMLJSON_SenMLCBOR);
} }
lwM2MTestClient.setDeviceIdStr(deviceIdStr);
} }
private void clientDestroy() { private void clientDestroy(boolean isAfter) {
try { try {
if (lwM2MTestClient != null) { if (lwM2MTestClient != null) {
if (isAfter) {
sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr());
awaitDeleteDevice(lwM2MTestClient.getDeviceIdStr());
}
lwM2MTestClient.destroy(); lwM2MTestClient.destroy();
awaitClientDestroy(lwM2MTestClient.getLeshanClient());
} }
} catch (Exception e) { } catch (Exception e) {
log.error("Failed client Destroy", e); log.error("Failed client Destroy", e);
@ -385,36 +390,20 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
return credentials; return credentials;
} }
private static void awaitServersDestroy() {
await("One of servers ports number is not free")
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> isServerPortsAvailable() == null);
}
private static String isServerPortsAvailable() {
for (int port : SERVERS_PORT_NUMBERS) {
try (ServerSocket serverSocket = new ServerSocket(port)) {
serverSocket.close();
Assert.assertEquals(true, serverSocket.isClosed());
} catch (IOException e) {
log.warn(String.format("Port %n still in use", port));
return (String.format("Port %n still in use", port));
}
}
return null;
}
private static void awaitClientDestroy(LeshanClient leshanClient) {
await("Destroy LeshanClient: delete All is registered Servers.")
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> leshanClient.getRegisteredServers().size() == 0);
}
protected void awaitObserveReadAll(int cntObserve, String deviceIdStr) throws Exception { protected void awaitObserveReadAll(int cntObserve, String deviceIdStr) throws Exception {
await("ObserveReadAll: countObserve " + cntObserve) await("ObserveReadAll: countObserve " + cntObserve)
.atMost(40, TimeUnit.SECONDS) .atMost(40, TimeUnit.SECONDS)
.until(() -> cntObserve == getCntObserveAll(deviceIdStr)); .until(() -> cntObserve == getCntObserveAll(deviceIdStr));
} }
protected void awaitDeleteDevice(String deviceIdStr) throws Exception {
await("Delete device with id: " + deviceIdStr)
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
doDelete("/api/device/" + deviceIdStr)
.andExpect(status().isOk());
return HttpStatus.NOT_FOUND.value() == doGet("/api/device/" + deviceIdStr).andReturn().getResponse().getStatus();
});
}
protected Integer getCntObserveAll(String deviceIdStr) throws Exception { protected Integer getCntObserveAll(String deviceIdStr) throws Exception {
String actualResult = sendObserveOK("ObserveReadAll", null, deviceIdStr); String actualResult = sendObserveOK("ObserveReadAll", null, deviceIdStr);
@ -428,7 +417,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
String actualResultCancelAll = sendObserveOK("ObserveCancelAll", null, deviceIdStr); String actualResultCancelAll = sendObserveOK("ObserveCancelAll", null, deviceIdStr);
ObjectNode rpcActualResultCancelAll = JacksonUtil.fromString(actualResultCancelAll, ObjectNode.class); ObjectNode rpcActualResultCancelAll = JacksonUtil.fromString(actualResultCancelAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultCancelAll.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultCancelAll.get("result").asText());
awaitObserveReadAll(0, deviceId); awaitObserveReadAll(0, lwM2MTestClient.getDeviceIdStr());
} }
protected String sendRpcObserveOkWithResultValue(String method, String params) throws Exception { protected String sendRpcObserveOkWithResultValue(String method, String params) throws Exception {
@ -438,7 +427,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
return rpcActualResult.get("value").asText(); return rpcActualResult.get("value").asText();
} }
protected String sendRpcObserveOk(String method, String params) throws Exception { protected String sendRpcObserveOk(String method, String params) throws Exception {
return sendObserveOK(method, params, deviceId); return sendObserveOK(method, params, lwM2MTestClient.getDeviceIdStr());
} }
protected String sendObserveOK(String method, String params, String deviceIdStr) throws Exception { protected String sendObserveOK(String method, String params, String deviceIdStr) throws Exception {
String sendRpcRequest; String sendRpcRequest;
@ -456,4 +445,15 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
return JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); return JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
} }
protected long countUpdateReg() {
return Mockito.mockingDetails(defaultUplinkMsgHandlerTest)
.getInvocations().stream()
.filter(invocation -> invocation.getMethod().getName().equals("updatedReg"))
.count();
}
protected void awaitUpdateReg(int cntUpdate) {
verify(defaultUplinkMsgHandlerTest, timeout(50000).atLeast(cntUpdate))
.updatedReg(Mockito.any(Registration.class));
}
} }

4
application/src/test/java/org/thingsboard/server/transport/lwm2m/attributes/LwM2mAttributesTest.java

@ -49,13 +49,13 @@ public class LwM2mAttributesTest {
@ParameterizedTest(name = "Tests {index} : {0}") @ParameterizedTest(name = "Tests {index} : {0}")
@MethodSource("doesntSupportAttributesWithoutValue") @MethodSource("doesntSupportAttributesWithoutValue")
public void check_attribute_can_not_be_created_without_value(LwM2mAttributeModel<?> model) { public void check_attribute_can_not_be_created_without_value(LwM2mAttributeModel<?> model) {
assertThrows(UnsupportedOperationException.class, () -> LwM2mAttributes.create(model)); assertThrows(IllegalArgumentException.class, () -> LwM2mAttributes.create(model));
} }
@ParameterizedTest(name = "Tests {index} : {0}") @ParameterizedTest(name = "Tests {index} : {0}")
@MethodSource("doesntSupportAttributesWithValueNull") @MethodSource("doesntSupportAttributesWithValueNull")
public void check_attribute_can_not_be_created_with_null(LwM2mAttributeModel<?> model) { public void check_attribute_can_not_be_created_with_null(LwM2mAttributeModel<?> model) {
assertThrows(NullPointerException.class, () -> LwM2mAttributes.create(model, null)); assertThrows(IllegalArgumentException.class, () -> LwM2mAttributes.create(model, null));
} }
private static Stream<Arguments> supportNullAttributes() throws InvalidAttributeException { private static Stream<Arguments> supportNullAttributes() throws InvalidAttributeException {

11
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java

@ -137,9 +137,12 @@ public class LwM2MTestClient {
private Map<LwM2MClientState, Integer> clientDtlsCid; private Map<LwM2MClientState, Integer> clientDtlsCid;
private LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest; private LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest;
private LwM2mClientContext clientContext; private LwM2mClientContext clientContext;
private LwM2mTemperatureSensor lwM2mTemperatureSensor12;
private String deviceIdStr;
public void init(Security security, Security securityBs, int port, boolean isRpc, public void init(Security security, Security securityBs, int port, boolean isRpc,
LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler,
LwM2mClientContext clientContext, boolean isWriteAttribute, Integer cIdLength, boolean queueMode, LwM2mClientContext clientContext, Integer cIdLength, boolean queueMode,
boolean supportFormatOnly_SenMLJSON_SenMLCBOR) throws InvalidDDFFileException, IOException { boolean supportFormatOnly_SenMLJSON_SenMLCBOR) throws InvalidDDFFileException, IOException {
Assert.assertNull("client already initialized", leshanClient); Assert.assertNull("client already initialized", leshanClient);
this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler; this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler;
@ -149,7 +152,7 @@ public class LwM2MTestClient {
models.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName)); models.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName));
} }
LwM2mModel model = new StaticModel(models); LwM2mModel model = new StaticModel(models);
ObjectsInitializer initializer = isWriteAttribute ? new TbObjectsInitializer(model) : new ObjectsInitializer(model); ObjectsInitializer initializer = new ObjectsInitializer(model);
if (securityBs != null && security != null) { if (securityBs != null && security != null) {
// SECURITY // SECURITY
security.setId(serverId); security.setId(serverId);
@ -189,7 +192,7 @@ public class LwM2MTestClient {
locationParams.getPos(); locationParams.getPos();
initializer.setInstancesForObject(LOCATION, new LwM2mLocation(locationParams.getLatitude(), locationParams.getLongitude(), locationParams.getScaleFactor(), executor, OBJECT_INSTANCE_ID_0)); initializer.setInstancesForObject(LOCATION, new LwM2mLocation(locationParams.getLatitude(), locationParams.getLongitude(), locationParams.getScaleFactor(), executor, OBJECT_INSTANCE_ID_0));
LwM2mTemperatureSensor lwM2mTemperatureSensor0 = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_0); LwM2mTemperatureSensor lwM2mTemperatureSensor0 = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_0);
LwM2mTemperatureSensor lwM2mTemperatureSensor12 = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_12); lwM2mTemperatureSensor12 = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_12);
initializer.setInstancesForObject(TEMPERATURE_SENSOR, lwM2mTemperatureSensor0, lwM2mTemperatureSensor12); initializer.setInstancesForObject(TEMPERATURE_SENSOR, lwM2mTemperatureSensor0, lwM2mTemperatureSensor12);
List<LwM2mObjectEnabler> enablers = initializer.createAll(); List<LwM2mObjectEnabler> enablers = initializer.createAll();
@ -315,7 +318,6 @@ public class LwM2MTestClient {
clientDtlsCid = new HashMap<>(); clientDtlsCid = new HashMap<>();
clientStates.add(ON_INIT); clientStates.add(ON_INIT);
leshanClient = builder.build(); leshanClient = builder.build();
lwM2mTemperatureSensor12.setLeshanClient(leshanClient);
LwM2mClientObserver observer = new LwM2mClientObserver() { LwM2mClientObserver observer = new LwM2mClientObserver() {
@Override @Override
@ -452,6 +454,7 @@ public class LwM2MTestClient {
if (isStartLw) { if (isStartLw) {
this.awaitClientAfterStartConnectLw(); this.awaitClientAfterStartConnectLw();
} }
lwM2mTemperatureSensor12.setLeshanClient(leshanClient);
} }
} }

2
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java

@ -129,7 +129,7 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements
fireResourceChange(resourceId); fireResourceChange(resourceId);
return WriteResponse.success(); return WriteResponse.success();
} else { } else {
WriteResponse.badRequest("Invalidate value ..."); return WriteResponse.badRequest("Invalidate value ...");
} }
case 1: case 1:
setPriority((Integer) (value.getValue() instanceof Long ? ((Long) value.getValue()).intValue() : value.getValue())); setPriority((Integer) (value.getValue() instanceof Long ? ((Long) value.getValue()).intValue() : value.getValue()));

46
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java

@ -50,14 +50,17 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr
private double maxMeasuredValue = currentTemp; private double maxMeasuredValue = currentTemp;
private LeshanClient leshanClient; private LeshanClient leshanClient;
private int cntRead_5700;
private int cntIdentitySystem; private int cntIdentitySystem;
protected static final Random RANDOM = new Random(); protected static final Random RANDOM = new Random();
private static final List<Integer> supportedResources = Arrays.asList(5601, 5602, 5700, 5701); private static final List<Integer> supportedResources = Arrays.asList(5601, 5602, 5700, 5701);
public LwM2mTemperatureSensor() { private LwM2mServer registeredServer;
private ManualDataSender sender;
private int resourceIdForSendCollected = 5700;
public LwM2mTemperatureSensor() {
} }
public LwM2mTemperatureSensor(ScheduledExecutorService executorService, Integer id) { public LwM2mTemperatureSensor(ScheduledExecutorService executorService, Integer id) {
@ -72,26 +75,33 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr
@Override @Override
public synchronized ReadResponse read(LwM2mServer identity, int resourceId) { public synchronized ReadResponse read(LwM2mServer identity, int resourceId) {
log.info("Read on Temperature resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId); log.trace("Read on Temperature resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId);
if (this.registeredServer == null && this.leshanClient != null && getId() == 12) {
try {
Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_0 = Instant.now().toEpochMilli();
this.registeredServer = this.leshanClient.getRegisteredServers().values().iterator().next();
this.sender = (ManualDataSender) this.leshanClient.getSendService().getDataSender(ManualDataSender.DEFAULT_NAME);
this.sender.collectData(Arrays.asList(getPathForCollectedValue(resourceIdForSendCollected)));
} catch (Exception e) {
log.error("[{}] Sender for SendCollected", e.toString());
e.printStackTrace();
}
}
switch (resourceId) { switch (resourceId) {
case 5601: case 5601:
return ReadResponse.success(resourceId, getTwoDigitValue(minMeasuredValue)); return ReadResponse.success(resourceId, getTwoDigitValue(minMeasuredValue));
case 5602: case 5602:
return ReadResponse.success(resourceId, getTwoDigitValue(maxMeasuredValue)); return ReadResponse.success(resourceId, getTwoDigitValue(maxMeasuredValue));
case 5700: case 5700:
if (identity == LwM2mServer.SYSTEM) { // return value for ForCollectedValue if (identity == LwM2mServer.SYSTEM) {
double val5700 = cntIdentitySystem == 0 ? RESOURCE_ID_3303_12_5700_VALUE_0 : RESOURCE_ID_3303_12_5700_VALUE_1;
cntIdentitySystem++; cntIdentitySystem++;
return ReadResponse.success(resourceId, cntIdentitySystem == 1 ? return ReadResponse.success(resourceId, val5700);
RESOURCE_ID_3303_12_5700_VALUE_0 : RESOURCE_ID_3303_12_5700_VALUE_1);
}
cntRead_5700++;
if (cntRead_5700 == 1) { // read value after start
return ReadResponse.success(resourceId, getTwoDigitValue(currentTemp));
} else { } else {
if (this.getId() == 12 && this.leshanClient != null) { if (cntIdentitySystem == 1 && this.getId() == 12 && this.leshanClient != null) {
sendCollected(); sendCollected();
} }
return ReadResponse.success(resourceId, getTwoDigitValue(currentTemp)); return super.read(identity, resourceId);
} }
case 5701: case 5701:
return ReadResponse.success(resourceId, UNIT_CELSIUS); return ReadResponse.success(resourceId, UNIT_CELSIUS);
@ -163,14 +173,10 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr
private void sendCollected() { private void sendCollected() {
try { try {
int resourceId = 5700; if ((Instant.now().toEpochMilli() - Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_0) < RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS) {
LwM2mServer registeredServer = this.leshanClient.getRegisteredServers().values().iterator().next(); Thread.sleep(RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS);
ManualDataSender sender = this.leshanClient.getSendService().getDataSender(ManualDataSender.DEFAULT_NAME, }
ManualDataSender.class); sender.collectData(Arrays.asList(getPathForCollectedValue(resourceIdForSendCollected)));
sender.collectData(Arrays.asList(getPathForCollectedValue(resourceId)));
Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_0 = Instant.now().toEpochMilli();
Thread.sleep(RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS);
sender.collectData(Arrays.asList(getPathForCollectedValue(resourceId)));
Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_1 = Instant.now().toEpochMilli(); Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_1 = Instant.now().toEpochMilli();
sender.sendCollectedData(registeredServer, ContentFormat.SENML_JSON, 1000, false); sender.sendCollectedData(registeredServer, ContentFormat.SENML_JSON, 1000, false);
} catch (InterruptedException e) { } catch (InterruptedException e) {

73
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java

@ -20,7 +20,7 @@ import org.eclipse.leshan.client.resource.BaseInstanceEnabler;
import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.client.servers.LwM2mServer;
import org.eclipse.leshan.core.Destroyable; import org.eclipse.leshan.core.Destroyable;
import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.model.ResourceModel.Type;
import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.request.argument.Arguments; import org.eclipse.leshan.core.request.argument.Arguments;
import org.eclipse.leshan.core.response.ExecuteResponse; import org.eclipse.leshan.core.response.ExecuteResponse;
@ -30,7 +30,6 @@ import org.eclipse.leshan.core.response.WriteResponse;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Arrays; import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.PrimitiveIterator; import java.util.PrimitiveIterator;
@ -46,9 +45,46 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
private static final Random RANDOM = new Random(); private static final Random RANDOM = new Random();
private static final int min = 5; private static final int min = 5;
private static final int max = 50; private static final int max = 50;
private static final PrimitiveIterator.OfInt randomIterator = new Random().ints(min,max + 1).iterator(); private static final PrimitiveIterator.OfInt randomIterator = new Random().ints(min, max + 1).iterator();
private static final List<Integer> supportedResources = Arrays.asList(0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21); private static final List<Integer> supportedResources = Arrays.asList(0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21);
/**
* 0: DC power
* 1: Internal Battery
* 2: External Battery
* 3: Fuel Cell
* 4: Power over Ethernet
* 5: USB
* 6: AC (Mains) power
* 7: Solar
*/
private static final Map<Integer, Long> availablePowerSources =
Map.of(0, 0L, 1, 1L, 2, 7L);
private static Map<Integer, Long> powerSourceVoltage =
Map.of(0, 12000L, 1, 12400L, 7, 14600L); //mV
private static Map<Integer, Long> powerSourceCurrent =
Map.of(0, 72000L, 1, 2000L, 7, 25000L); // mA
/**
* 0=No error
* 1=Low battery power
* 2=External power supply off
* 3=GPS module failure
* 4=Low received signal strength
* 5=Out of memory
* 6=SMS failure
* 7=IP connectivity failure
* 8=Peripheral malfunction
* 9..15=Reserved for future use
* 16..32=Device specific error codes
*
* When the single Device Object Instance is initiated, there is only one error code Resource Instance whose value is equal to 0 that means no error.
* When the first error happens, the LwM2M Client changes error code Resource Instance to any non-zero value to indicate the error type.
* When any other error happens, a new error code Resource Instance is created.
* When an error associated with a Resource Instance is no longer present, that Resource Instance is deleted.
* When the single existing error is no longer present, the LwM2M Client returns to the original no error state where Instance 0 has value 0.
*/
private static Map<Integer, Long> errorCode =
Map.of(0, 0L); // 0-32
public SimpleLwM2MDevice() { public SimpleLwM2MDevice() {
} }
@ -81,15 +117,17 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
case 3: case 3:
return ReadResponse.success(resourceId, getFirmwareVersion()); return ReadResponse.success(resourceId, getFirmwareVersion());
case 6: case 6:
return ReadResponse.success(resourceId, getAvailablePowerSources(), ResourceModel.Type.INTEGER); return ReadResponse.success(resourceId, getAvailablePowerSources(), Type.INTEGER);
case 7:
return ReadResponse.success(resourceId, getPowerSourceVoltage(), Type.INTEGER);
case 8:
return ReadResponse.success(resourceId, getPowerSourceCurrent(), Type.INTEGER);
case 9: case 9:
return ReadResponse.success(resourceId, getBatteryLevel()); return ReadResponse.success(resourceId, getBatteryLevel());
case 10: case 10:
return ReadResponse.success(resourceId, getMemoryFree()); return ReadResponse.success(resourceId, getMemoryFree());
case 11: case 11:
Map<Integer, Long> errorCodes = new HashMap<>(); return ReadResponse.success(resourceId, getErrorCodes(), Type.INTEGER);
errorCodes.put(0, getErrorCode());
return ReadResponse.success(resourceId, errorCodes, ResourceModel.Type.INTEGER);
case 14: case 14:
return ReadResponse.success(resourceId, getUtcOffset()); return ReadResponse.success(resourceId, getUtcOffset());
case 15: case 15:
@ -156,16 +194,19 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
return "1.0.2"; return "1.0.2";
} }
private long getErrorCode() { private Map<Integer, ?> getAvailablePowerSources() {
return 0; return availablePowerSources;
} }
private Map<Integer, Long> getAvailablePowerSources() { private Map<Integer, ?> getPowerSourceVoltage() {
Map<Integer, Long> availablePowerSources = new HashMap<>(); return powerSourceVoltage;
availablePowerSources.put(0, 1L); }
availablePowerSources.put(1, 2L); private Map<Integer, ?> getPowerSourceCurrent() {
availablePowerSources.put(2, 5L); return powerSourceCurrent;
return availablePowerSources; }
private Map<Integer, ?> getErrorCodes() {
return errorCode;
} }
private int getBatteryLevel() { private int getBatteryLevel() {

732
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbLwm2mObjectEnabler.java

@ -1,732 +0,0 @@
/**
* 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.
*/
package org.thingsboard.server.transport.lwm2m.client;
import org.eclipse.leshan.client.LwM2mClient;
import org.eclipse.leshan.client.resource.BaseObjectEnabler;
import org.eclipse.leshan.client.resource.DummyInstanceEnabler;
import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler;
import org.eclipse.leshan.client.resource.LwM2mInstanceEnablerFactory;
import org.eclipse.leshan.client.resource.listener.ResourceListener;
import org.eclipse.leshan.client.servers.LwM2mServer;
import org.eclipse.leshan.client.servers.ServersInfoExtractor;
import org.eclipse.leshan.client.util.LinkFormatHelper;
import org.eclipse.leshan.core.Destroyable;
import org.eclipse.leshan.core.LwM2mId;
import org.eclipse.leshan.core.Startable;
import org.eclipse.leshan.core.Stoppable;
import org.eclipse.leshan.core.link.lwm2m.LwM2mLink;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet;
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
import org.eclipse.leshan.core.node.LwM2mObject;
import org.eclipse.leshan.core.node.LwM2mObjectInstance;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.LwM2mResourceInstance;
import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
import org.eclipse.leshan.core.request.BootstrapReadRequest;
import org.eclipse.leshan.core.request.BootstrapWriteRequest;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.core.request.CreateRequest;
import org.eclipse.leshan.core.request.DeleteRequest;
import org.eclipse.leshan.core.request.DiscoverRequest;
import org.eclipse.leshan.core.request.DownlinkRequest;
import org.eclipse.leshan.core.request.ExecuteRequest;
import org.eclipse.leshan.core.request.ObserveRequest;
import org.eclipse.leshan.core.request.ReadRequest;
import org.eclipse.leshan.core.request.WriteAttributesRequest;
import org.eclipse.leshan.core.request.WriteRequest;
import org.eclipse.leshan.core.request.WriteRequest.Mode;
import org.eclipse.leshan.core.response.BootstrapDeleteResponse;
import org.eclipse.leshan.core.response.BootstrapReadResponse;
import org.eclipse.leshan.core.response.BootstrapWriteResponse;
import org.eclipse.leshan.core.response.CreateResponse;
import org.eclipse.leshan.core.response.DeleteResponse;
import org.eclipse.leshan.core.response.DiscoverResponse;
import org.eclipse.leshan.core.response.ExecuteResponse;
import org.eclipse.leshan.core.response.ObserveResponse;
import org.eclipse.leshan.core.response.ReadResponse;
import org.eclipse.leshan.core.response.WriteAttributesResponse;
import org.eclipse.leshan.core.response.WriteResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
public class TbLwm2mObjectEnabler extends BaseObjectEnabler implements Destroyable, Startable, Stoppable {
private static Logger LOG = LoggerFactory.getLogger(DummyInstanceEnabler.class);
protected Map<Integer, LwM2mInstanceEnabler> instances;
protected LwM2mInstanceEnablerFactory instanceFactory;
protected ContentFormat defaultContentFormat;
private LinkFormatHelper tbLinkFormatHelper;
protected Map<LwM2mPath, LwM2mAttributeSet> lwM2mAttributes;
public TbLwm2mObjectEnabler(int id, ObjectModel objectModel, Map<Integer, LwM2mInstanceEnabler> instances,
LwM2mInstanceEnablerFactory instanceFactory, ContentFormat defaultContentFormat) {
super(id, objectModel);
this.instances = new HashMap<>(instances);
;
this.instanceFactory = instanceFactory;
this.defaultContentFormat = defaultContentFormat;
for (Entry<Integer, LwM2mInstanceEnabler> entry : this.instances.entrySet()) {
instances.put(entry.getKey(), entry.getValue());
listenInstance(entry.getValue(), entry.getKey());
}
this.lwM2mAttributes = new HashMap<>();
}
public TbLwm2mObjectEnabler(int id, ObjectModel objectModel) {
super(id, objectModel);
}
@Override
public synchronized List<Integer> getAvailableInstanceIds() {
List<Integer> ids = new ArrayList<>(instances.keySet());
Collections.sort(ids);
return ids;
}
@Override
public synchronized List<Integer> getAvailableResourceIds(int instanceId) {
LwM2mInstanceEnabler instanceEnabler = instances.get(instanceId);
if (instanceEnabler != null) {
return instanceEnabler.getAvailableResourceIds(getObjectModel());
} else {
return Collections.emptyList();
}
}
public synchronized void addInstance(int instanceId, LwM2mInstanceEnabler newInstance) {
instances.put(instanceId, newInstance);
listenInstance(newInstance, instanceId);
fireInstancesAdded(instanceId);
}
public synchronized LwM2mInstanceEnabler getInstance(int instanceId) {
return instances.get(instanceId);
}
public synchronized LwM2mInstanceEnabler removeInstance(int instanceId) {
LwM2mInstanceEnabler removedInstance = instances.remove(instanceId);
if (removedInstance != null) {
fireInstancesRemoved(removedInstance.getId());
}
return removedInstance;
}
@Override
protected CreateResponse doCreate(LwM2mServer server, CreateRequest request) {
if (!getObjectModel().multiple && instances.size() > 0) {
return CreateResponse.badRequest("an instance already exist for this single instance object");
}
if (request.unknownObjectInstanceId()) {
// create instance
LwM2mInstanceEnabler newInstance = createInstance(server, getObjectModel().multiple ? null : 0,
request.getResources());
// add new instance to this object
instances.put(newInstance.getId(), newInstance);
listenInstance(newInstance, newInstance.getId());
fireInstancesAdded(newInstance.getId());
return CreateResponse
.success(new LwM2mPath(request.getPath().getObjectId(), newInstance.getId()).toString());
} else {
List<LwM2mObjectInstance> instanceNodes = request.getObjectInstances();
// checks single object instances
if (!getObjectModel().multiple) {
if (request.getObjectInstances().size() > 1) {
return CreateResponse.badRequest("can not create several instances on this single instance object");
}
if (request.getObjectInstances().get(0).getId() != 0) {
return CreateResponse.badRequest("single instance object must use 0 as ID");
}
}
// ensure instance does not already exists
for (LwM2mObjectInstance instance : instanceNodes) {
if (instances.containsKey(instance.getId())) {
return CreateResponse.badRequest(String.format("instance %d already exists", instance.getId()));
}
}
// create the new instances
int[] instanceIds = new int[request.getObjectInstances().size()];
int i = 0;
for (LwM2mObjectInstance instance : request.getObjectInstances()) {
// create instance
LwM2mInstanceEnabler newInstance = createInstance(server, instance.getId(),
instance.getResources().values());
// add new instance to this object
instances.put(newInstance.getId(), newInstance);
listenInstance(newInstance, newInstance.getId());
// store instance ids
instanceIds[i] = newInstance.getId();
i++;
}
fireInstancesAdded(instanceIds);
return CreateResponse.success();
}
}
protected LwM2mInstanceEnabler createInstance(LwM2mServer server, Integer instanceId,
Collection<LwM2mResource> resources) {
// create the new instance
LwM2mInstanceEnabler newInstance = instanceFactory.create(getObjectModel(), instanceId, instances.keySet());
newInstance.setLwM2mClient(getLwm2mClient());
// add/write resource
for (LwM2mResource resource : resources) {
newInstance.write(server, true, resource.getId(), resource);
}
return newInstance;
}
@Override
protected ReadResponse doRead(LwM2mServer server, ReadRequest request) {
LwM2mPath path = request.getPath();
// Manage Object case
if (path.isObject()) {
List<LwM2mObjectInstance> lwM2mObjectInstances = new ArrayList<>();
for (LwM2mInstanceEnabler instance : instances.values()) {
ReadResponse response = instance.read(server);
if (response.isSuccess()) {
lwM2mObjectInstances.add((LwM2mObjectInstance) response.getContent());
}
}
return ReadResponse.success(new LwM2mObject(getId(), lwM2mObjectInstances));
}
// Manage Instance case
LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId());
if (instance == null)
return ReadResponse.notFound();
if (path.getResourceId() == null) {
return instance.read(server);
}
// Manage Resource case
if (path.getResourceInstanceId() == null) {
return instance.read(server, path.getResourceId());
}
// Manage Resource Instance case
return instance.read(server, path.getResourceId(), path.getResourceInstanceId());
}
@Override
protected BootstrapReadResponse doRead(LwM2mServer server, BootstrapReadRequest request) {
// Basic implementation we delegate to classic Read Request
ReadResponse response = doRead(server,
new ReadRequest(request.getContentFormat(), request.getPath(), request.getCoapRequest()));
return new BootstrapReadResponse(response.getCode(), response.getContent(), response.getErrorMessage());
}
@Override
protected ObserveResponse doObserve(final LwM2mServer server, final ObserveRequest request) {
final LwM2mPath path = request.getPath();
// Manage Object case
if (path.isObject()) {
List<LwM2mObjectInstance> lwM2mObjectInstances = new ArrayList<>();
for (LwM2mInstanceEnabler instance : instances.values()) {
ReadResponse response = instance.observe(server);
if (response.isSuccess()) {
lwM2mObjectInstances.add((LwM2mObjectInstance) response.getContent());
}
}
return ObserveResponse.success(new LwM2mObject(getId(), lwM2mObjectInstances));
}
// Manage Instance case
final LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId());
if (instance == null)
return ObserveResponse.notFound();
if (path.getResourceId() == null) {
return instance.observe(server);
}
// Manage Resource case
if (path.getResourceInstanceId() == null) {
return instance.observe(server, path.getResourceId());
}
// Manage Resource Instance case
return instance.observe(server, path.getResourceId(), path.getResourceInstanceId());
}
@Override
protected WriteResponse doWrite(LwM2mServer server, WriteRequest request) {
LwM2mPath path = request.getPath();
// Manage Instance case
LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId());
if (instance == null)
return WriteResponse.notFound();
if (path.isObjectInstance()) {
return instance.write(server, request.isReplaceRequest(), (LwM2mObjectInstance) request.getNode());
}
// Manage Resource case
if (path.getResourceInstanceId() == null) {
return instance.write(server, request.isReplaceRequest(), path.getResourceId(),
(LwM2mResource) request.getNode());
}
// Manage Resource Instance case
return instance.write(server, false, path.getResourceId(), path.getResourceInstanceId(),
((LwM2mResourceInstance) request.getNode()));
}
@Override
protected BootstrapWriteResponse doWrite(LwM2mServer server, BootstrapWriteRequest request) {
LwM2mPath path = request.getPath();
// Manage Object case
if (path.isObject()) {
for (LwM2mObjectInstance instanceNode : ((LwM2mObject) request.getNode()).getInstances().values()) {
LwM2mInstanceEnabler instanceEnabler = instances.get(instanceNode.getId());
if (instanceEnabler == null) {
doCreate(server, new CreateRequest(path.getObjectId(), instanceNode));
} else {
doWrite(server, new WriteRequest(Mode.REPLACE, path.getObjectId(), instanceEnabler.getId(),
instanceNode.getResources().values()));
}
}
return BootstrapWriteResponse.success();
}
// Manage Instance case
if (path.isObjectInstance()) {
LwM2mObjectInstance instanceNode = (LwM2mObjectInstance) request.getNode();
LwM2mInstanceEnabler instanceEnabler = instances.get(path.getObjectInstanceId());
if (instanceEnabler == null) {
doCreate(server, new CreateRequest(path.getObjectId(), instanceNode));
} else {
doWrite(server, new WriteRequest(Mode.REPLACE, request.getContentFormat(), path.getObjectId(),
path.getObjectInstanceId(), instanceNode.getResources().values()));
}
return BootstrapWriteResponse.success();
}
// Manage resource case
LwM2mResource resource = (LwM2mResource) request.getNode();
LwM2mInstanceEnabler instanceEnabler = instances.get(path.getObjectInstanceId());
if (instanceEnabler == null) {
doCreate(server, new CreateRequest(path.getObjectId(),
new LwM2mObjectInstance(path.getObjectInstanceId(), resource)));
} else {
instanceEnabler.write(server, true, path.getResourceId(), resource);
}
return BootstrapWriteResponse.success();
}
@Override
protected ExecuteResponse doExecute(LwM2mServer server, ExecuteRequest request) {
LwM2mPath path = request.getPath();
LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId());
if (instance == null) {
return ExecuteResponse.notFound();
}
return instance.execute(server, path.getResourceId(), request.getArguments());
}
@Override
protected DeleteResponse doDelete(LwM2mServer server, DeleteRequest request) {
LwM2mInstanceEnabler deletedInstance = instances.remove(request.getPath().getObjectInstanceId());
if (deletedInstance != null) {
deletedInstance.onDelete(server);
fireInstancesRemoved(deletedInstance.getId());
return DeleteResponse.success();
}
return DeleteResponse.notFound();
}
@Override
public BootstrapDeleteResponse doDelete(LwM2mServer server, BootstrapDeleteRequest request) {
if (request.getPath().isRoot() || request.getPath().isObject()) {
if (id == LwM2mId.SECURITY) {
// For security object, we clean everything except bootstrap Server account.
// Get bootstrap account and store removed instances ids
Entry<Integer, LwM2mInstanceEnabler> bootstrapServerAccount = null;
int[] instanceIds = new int[instances.size()];
int i = 0;
for (Entry<Integer, LwM2mInstanceEnabler> instance : instances.entrySet()) {
if (ServersInfoExtractor.isBootstrapServer(instance.getValue())) {
bootstrapServerAccount = instance;
} else {
// Store instance ids
instanceIds[i] = instance.getKey();
i++;
}
}
// Clear everything
instances.clear();
// Put bootstrap account again
if (bootstrapServerAccount != null) {
instances.put(bootstrapServerAccount.getKey(), bootstrapServerAccount.getValue());
}
fireInstancesRemoved(instanceIds);
return BootstrapDeleteResponse.success();
} else if (id == LwM2mId.OSCORE) {
// For OSCORE object, we clean everything except OSCORE object link to bootstrap Server account.
// Get bootstrap account
LwM2mObjectInstance bootstrapInstance = ServersInfoExtractor.getBootstrapSecurityInstance(
getLwm2mClient().getObjectTree().getObjectEnabler(LwM2mId.SECURITY));
// Get OSCORE instance ID associated to it
Integer bootstrapOscoreInstanceId = bootstrapInstance != null
? ServersInfoExtractor.getOscoreSecurityMode(bootstrapInstance)
: null;
// if bootstrap server use OSCORE,
// search the OSCORE instance for this ID and store removed instances ids
if (bootstrapOscoreInstanceId != null) {
Entry<Integer, LwM2mInstanceEnabler> bootstrapServerOscore = null;
int[] instanceIds = new int[instances.size()];
int i = 0;
for (Entry<Integer, LwM2mInstanceEnabler> instance : instances.entrySet()) {
if (bootstrapOscoreInstanceId.equals(instance.getKey())) {
bootstrapServerOscore = instance;
} else {
// Store instance ids
instanceIds[i] = instance.getKey();
i++;
}
}
// Clear everything
instances.clear();
// Put bootstrap OSCORE instance again
if (bootstrapServerOscore != null) {
instances.put(bootstrapServerOscore.getKey(), bootstrapServerOscore.getValue());
}
fireInstancesRemoved(instanceIds);
return BootstrapDeleteResponse.success();
}
// else delete everything.
}
// In all other cases, just delete everything
instances.clear();
// fired instances removed
int[] instanceIds = new int[instances.size()];
int i = 0;
for (Entry<Integer, LwM2mInstanceEnabler> instance : instances.entrySet()) {
instanceIds[i] = instance.getKey();
i++;
}
fireInstancesRemoved(instanceIds);
return BootstrapDeleteResponse.success();
} else if (request.getPath().isObjectInstance()) {
if (id == LwM2mId.SECURITY) {
// For security object, deleting bootstrap Server account is not allowed
LwM2mInstanceEnabler instance = instances.get(request.getPath().getObjectInstanceId());
if (instance == null) {
return BootstrapDeleteResponse
.badRequest(String.format("Instance %s not found", request.getPath()));
} else if (ServersInfoExtractor.isBootstrapServer(instance)) {
return BootstrapDeleteResponse.badRequest("bootstrap server can not be deleted");
}
} else if (id == LwM2mId.OSCORE) {
// For OSCORE object, deleting instance linked to Bootstrap account is not allowed
// Get bootstrap instance
LwM2mObjectInstance bootstrapInstance = ServersInfoExtractor.getBootstrapSecurityInstance(
getLwm2mClient().getObjectTree().getObjectEnabler(LwM2mId.SECURITY));
// Get OSCORE instance ID associated to it
Integer bootstrapOscoreInstanceId = bootstrapInstance != null
? ServersInfoExtractor.getOscoreSecurityMode(bootstrapInstance)
: null;
if (bootstrapOscoreInstanceId != null
&& bootstrapOscoreInstanceId.equals(request.getPath().getObjectInstanceId())) {
return BootstrapDeleteResponse
.badRequest("OSCORE instance linked to bootstrap server can not be deleted");
}
}
if (null != instances.remove(request.getPath().getObjectInstanceId())) {
fireInstancesRemoved(request.getPath().getObjectInstanceId());
return BootstrapDeleteResponse.success();
} else {
return BootstrapDeleteResponse.badRequest(String.format("Instance %s not found", request.getPath()));
}
}
return BootstrapDeleteResponse.badRequest(String.format("unexcepted path %s", request.getPath()));
}
protected void listenInstance(LwM2mInstanceEnabler instance, final int instanceId) {
instance.addResourceListener(new ResourceListener() {
@Override
public void resourceChanged(LwM2mPath... paths) {
for (LwM2mPath path : paths) {
if (!isValid(instanceId, path)) {
LOG.warn("InstanceEnabler ({}) of object ({}) try to raise a change of {} which seems invalid.",
instanceId, getId(), path);
}
}
fireResourcesChanged(paths);
}
});
}
protected boolean isValid(int instanceId, LwM2mPath pathToValidate) {
if (!(pathToValidate.isResource() || pathToValidate.isResourceInstance()))
return false;
if (pathToValidate.getObjectId() != getId()) {
return false;
}
if (pathToValidate.getObjectInstanceId() != instanceId) {
return false;
}
return true;
}
@Override
public ContentFormat getDefaultEncodingFormat(DownlinkRequest<?> request) {
return defaultContentFormat;
}
@Override
public void init(LwM2mClient client, LinkFormatHelper linkFormatHelper) {
super.init(client, linkFormatHelper);
this.tbLinkFormatHelper = linkFormatHelper;
for (LwM2mInstanceEnabler instanceEnabler : instances.values()) {
instanceEnabler.setLwM2mClient(client);
}
}
@Override
public void destroy() {
for (LwM2mInstanceEnabler instanceEnabler : instances.values()) {
if (instanceEnabler instanceof Destroyable) {
((Destroyable) instanceEnabler).destroy();
} else if (instanceEnabler instanceof Stoppable) {
((Stoppable) instanceEnabler).stop();
}
}
}
@Override
public void start() {
for (LwM2mInstanceEnabler instanceEnabler : instances.values()) {
if (instanceEnabler instanceof Startable) {
((Startable) instanceEnabler).start();
}
}
}
@Override
public void stop() {
for (LwM2mInstanceEnabler instanceEnabler : instances.values()) {
if (instanceEnabler instanceof Stoppable) {
((Stoppable) instanceEnabler).stop();
}
}
}
@Override
public synchronized WriteAttributesResponse writeAttributes(LwM2mServer server, WriteAttributesRequest request) {
// execute is not supported for bootstrap
if (server.isLwm2mBootstrapServer()) {
return WriteAttributesResponse.methodNotAllowed();
}
// return WriteAttributesResponse.internalServerError("not implemented");
return doWriteAttributes(server, request);
}
/**
* <NOTIFICATION> Class Attributes
* - pmin (def = 0(sec)) Integer Resource/Object Instance/Object Readable Resource
* - pmax (def = -- ) Integer Resource/Object Instance/Object Readable Resource
* - Greater Than gt (def = -- ) Float Resource Numerical&Readable Resource
* - Less Than lt (def = -- ) Float Resource Numerical&Readable Resource
* - Step st (def = -- ) Float Resource Numerical&Readable Resource
*/
public WriteAttributesResponse doWriteAttributes(LwM2mServer server, WriteAttributesRequest request) {
LwM2mPath lwM2mPath = request.getPath();
LwM2mAttributeSet attributeSet = lwM2mAttributes.get(lwM2mPath);
Map <String, LwM2mAttribute<?>> attributes = new HashMap<>();
for (LwM2mAttribute attr : request.getAttributes().getLwM2mAttributes()) {
if (attr.getName().equals("pmax") || attr.getName().equals("pmin")) {
if (lwM2mPath.isObject() || lwM2mPath.isObjectInstance() || lwM2mPath.isResource()) {
attributes.put(attr.getName(), attr);
} else {
return WriteAttributesResponse.badRequest("Attribute " + attr.getName() + " can be used for only Resource/Object Instance/Object.");
}
} else if (attr.getName().equals("gt") || attr.getName().equals("lt") || attr.getName().equals("st")) {
if (lwM2mPath.isResource()) {
attributes.put(attr.getName(), attr);
} else {
return WriteAttributesResponse.badRequest("Attribute " + attr.getName() + " can be used for only Resource.");
}
}
}
if (attributes.size()>0){
if (attributeSet == null) {
attributeSet = new LwM2mAttributeSet(attributes.values());
} else {
Iterable<LwM2mAttribute<?>> lwM2mAttributeIterable = attributeSet.getLwM2mAttributes();
Map <String, LwM2mAttribute<?>> attributesOld = new HashMap<>();
for (LwM2mAttribute<?> attr : lwM2mAttributeIterable) {
attributesOld.put(attr.getName(), attr);
}
attributesOld.putAll(attributes);
attributeSet = new LwM2mAttributeSet(attributesOld.values());
}
lwM2mAttributes.put(lwM2mPath, attributeSet);
return WriteAttributesResponse.success();
}
return WriteAttributesResponse.internalServerError("not implemented");
}
@Override
public synchronized DiscoverResponse discover(LwM2mServer server, DiscoverRequest request) {
if (server.isLwm2mBootstrapServer()) {
// discover is not supported for bootstrap
return DiscoverResponse.methodNotAllowed();
}
if (id == LwM2mId.SECURITY || id == LwM2mId.OSCORE) {
return DiscoverResponse.notFound();
}
return doDiscover(server, request);
}
protected DiscoverResponse doDiscover(LwM2mServer server, DiscoverRequest request) {
LwM2mPath path = request.getPath();
if (path.isObject()) {
LwM2mLink[] ObjectLinks = linkUpdateAttributes(this.tbLinkFormatHelper.getObjectDescription(this, null), server);
return DiscoverResponse.success(ObjectLinks);
} else if (path.isObjectInstance()) {
// Manage discover on instance
if (!getAvailableInstanceIds().contains(path.getObjectInstanceId()))
return DiscoverResponse.notFound();
LwM2mLink[] instanceLink = linkUpdateAttributes(this.tbLinkFormatHelper.getInstanceDescription(this, path.getObjectInstanceId(), null), server);
return DiscoverResponse.success(instanceLink);
} else if (path.isResource()) {
// Manage discover on resource
if (!getAvailableInstanceIds().contains(path.getObjectInstanceId()))
return DiscoverResponse.notFound();
ResourceModel resourceModel = getObjectModel().resources.get(path.getResourceId());
if (resourceModel == null)
return DiscoverResponse.notFound();
if (!getAvailableResourceIds(path.getObjectInstanceId()).contains(path.getResourceId()))
return DiscoverResponse.notFound();
LwM2mLink resourceLink = linkAddAttribute(
this.tbLinkFormatHelper.getResourceDescription(this, path.getObjectInstanceId(), path.getResourceId(), null),
server);
return DiscoverResponse.success(new LwM2mLink[] { resourceLink });
}
return DiscoverResponse.badRequest(null);
}
private LwM2mLink[] linkUpdateAttributes(LwM2mLink[] links, LwM2mServer server) {
return Arrays.stream(links)
.map(link -> linkAddAttribute(link, server))
.toArray(LwM2mLink[]::new);
}
private LwM2mLink linkAddAttribute(LwM2mLink link, LwM2mServer server) {
LwM2mAttributeSet lwM2mAttributeSetDop = null;
if (this.lwM2mAttributes.get(link.getPath())!= null){
lwM2mAttributeSetDop = this.lwM2mAttributes.get(link.getPath());
}
LwM2mAttribute resourceAttributeDim = getResourceAttributes (server, link.getPath());
Map <String, LwM2mAttribute<?>> attributes = new HashMap<>();
if (link.getAttributes() != null) {
for (LwM2mAttribute attr : link.getAttributes().getLwM2mAttributes()) {
attributes.put(attr.getName(), attr);
}
}
if (lwM2mAttributeSetDop != null) {
for (LwM2mAttribute attr : lwM2mAttributeSetDop.getLwM2mAttributes()) {
attributes.put(attr.getName(), attr);
}
}
if (resourceAttributeDim != null) {
attributes.put(resourceAttributeDim.getName(), resourceAttributeDim);
}
return new LwM2mLink(link.getRootPath(), link.getPath(), attributes.values());
}
protected LwM2mAttribute getResourceAttributes (LwM2mServer server, LwM2mPath path) {
ResourceModel resourceModel = getObjectModel().resources.get(path.getResourceId());
if (path.isResource() && resourceModel.multiple) {
return getResourceAttributeDim(path, server);
}
return null;
}
protected LwM2mAttribute getResourceAttributeDim(LwM2mPath path, LwM2mServer server) {
LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId());
try {
ReadResponse readResponse = instance.read(server, path.getResourceId());
if (readResponse.getCode().getCode()==205 && readResponse.getContent() instanceof LwM2mMultipleResource) {
long valueDim = ((LwM2mMultipleResource)readResponse.getContent()).getInstances().size();
return LwM2mAttributes.create(LwM2mAttributes.DIMENSION, valueDim);
} else {
return null;
}
} catch (Exception e ){
return null;
}
}
}

71
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/TbObjectsInitializer.java

@ -1,71 +0,0 @@
/**
* 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.
*/
package org.thingsboard.server.transport.lwm2m.client;
import org.eclipse.leshan.client.resource.BaseInstanceEnablerFactory;
import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler;
import org.eclipse.leshan.client.resource.LwM2mObjectEnabler;
import org.eclipse.leshan.client.resource.ObjectsInitializer;
import org.eclipse.leshan.core.model.LwM2mModel;
import org.eclipse.leshan.core.model.ObjectModel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TbObjectsInitializer extends ObjectsInitializer {
public TbObjectsInitializer(LwM2mModel model) {
super(model);
}
public List<LwM2mObjectEnabler> create(int... objectId) {
List<LwM2mObjectEnabler> enablers = new ArrayList<>();
for (int anObjectId : objectId) {
LwM2mObjectEnabler objectEnabler = create(anObjectId);
if (objectEnabler != null)
enablers.add(objectEnabler);
}
return enablers;
}
public LwM2mObjectEnabler create(int objectId) {
ObjectModel objectModel = model.getObjectModel(objectId);
if (objectModel == null) {
throw new IllegalArgumentException(
"Cannot create object for id " + objectId + " because no model is defined for this id.");
}
return createNodeEnabler(objectModel);
}
protected LwM2mObjectEnabler createNodeEnabler(ObjectModel objectModel) {
Map<Integer, LwM2mInstanceEnabler> instances = new HashMap<>();
LwM2mInstanceEnabler[] newInstances = createInstances(objectModel);
for (LwM2mInstanceEnabler instance : newInstances) {
// set id if not already set
if (instance.getId() == null) {
int id = BaseInstanceEnablerFactory.generateNewInstanceId(instances.keySet());
instance.setId(id);
}
instance.setModel(objectModel);
instances.put(instance.getId(), instance);
}
return new TbLwm2mObjectEnabler(objectModel.id, objectModel, instances, getFactoryFor(objectModel),
getContentFormat(objectModel.id));
}
}

86
application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java

@ -15,17 +15,31 @@
*/ */
package org.thingsboard.server.transport.lwm2m.ota; package org.thingsboard.server.transport.lwm2m.ota;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest; import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.rest.client.utils.RestJsonConverter.toTimeseries;
import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE;
import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE; import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE;
@Slf4j
@DaoSqlTest @DaoSqlTest
public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
@ -33,9 +47,10 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
protected static final String CLIENT_ENDPOINT_WITHOUT_FW_INFO = "WithoutFirmwareInfoDevice"; protected static final String CLIENT_ENDPOINT_WITHOUT_FW_INFO = "WithoutFirmwareInfoDevice";
protected static final String CLIENT_ENDPOINT_OTA5 = "Ota5_Device"; protected static final String CLIENT_ENDPOINT_OTA5 = "Ota5_Device";
protected static final String CLIENT_ENDPOINT_OTA9 = "Ota9_Device"; protected static final String CLIENT_ENDPOINT_OTA9 = "Ota9_Device";
protected List<OtaPackageUpdateStatus> expectedStatuses;
protected final String OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA = protected final String OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA5 =
" {\n" + " {\n" +
" \"keyName\": {\n" + " \"keyName\": {\n" +
@ -43,22 +58,14 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
" \"/5_1.2/0/5\": \"updateResult\",\n" + " \"/5_1.2/0/5\": \"updateResult\",\n" +
" \"/5_1.2/0/6\": \"pkgname\",\n" + " \"/5_1.2/0/6\": \"pkgname\",\n" +
" \"/5_1.2/0/7\": \"pkgversion\",\n" + " \"/5_1.2/0/7\": \"pkgversion\",\n" +
" \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\",\n" + " \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\n" +
" \"/9_1.1/0/0\": \"pkgname\",\n" +
" \"/9_1.1/0/1\": \"pkgversion\",\n" +
" \"/9_1.1/0/7\": \"updateState\",\n" +
" \"/9_1.1/0/9\": \"updateResult\"\n" +
" },\n" + " },\n" +
" \"observe\": [\n" + " \"observe\": [\n" +
" \"/5_1.2/0/3\",\n" + " \"/5_1.2/0/3\",\n" +
" \"/5_1.2/0/5\",\n" + " \"/5_1.2/0/5\",\n" +
" \"/5_1.2/0/6\",\n" + " \"/5_1.2/0/6\",\n" +
" \"/5_1.2/0/7\",\n" + " \"/5_1.2/0/7\",\n" +
" \"/5_1.2/0/9\",\n" + " \"/5_1.2/0/9\"\n" +
" \"/9_1.1/0/0\",\n" +
" \"/9_1.1/0/1\",\n" +
" \"/9_1.1/0/7\",\n" +
" \"/9_1.1/0/9\"\n" +
" ],\n" + " ],\n" +
" \"attribute\": [],\n" + " \"attribute\": [],\n" +
" \"telemetry\": [\n" + " \"telemetry\": [\n" +
@ -66,7 +73,28 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
" \"/5_1.2/0/5\",\n" + " \"/5_1.2/0/5\",\n" +
" \"/5_1.2/0/6\",\n" + " \"/5_1.2/0/6\",\n" +
" \"/5_1.2/0/7\",\n" + " \"/5_1.2/0/7\",\n" +
" \"/5_1.2/0/9\",\n" + " \"/5_1.2/0/9\"\n" +
" ],\n" +
" \"attributeLwm2m\": {}\n" +
" }";
protected final String OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA9 =
" {\n" +
" \"keyName\": {\n" +
" \"/9_1.1/0/0\": \"pkgname\",\n" +
" \"/9_1.1/0/1\": \"pkgversion\",\n" +
" \"/9_1.1/0/7\": \"updateState\",\n" +
" \"/9_1.1/0/9\": \"updateResult\"\n" +
" },\n" +
" \"observe\": [\n" +
" \"/9_1.1/0/0\",\n" +
" \"/9_1.1/0/1\",\n" +
" \"/9_1.1/0/7\",\n" +
" \"/9_1.1/0/9\"\n" +
" ],\n" +
" \"attribute\": [],\n" +
" \"telemetry\": [\n" +
" \"/9_1.1/0/0\",\n" + " \"/9_1.1/0/0\",\n" +
" \"/9_1.1/0/1\",\n" + " \"/9_1.1/0/1\",\n" +
" \"/9_1.1/0/7\",\n" + " \"/9_1.1/0/7\",\n" +
@ -79,14 +107,14 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
setResources(this.RESOURCES_OTA); setResources(this.RESOURCES_OTA);
} }
protected OtaPackageInfo createFirmware() throws Exception { protected OtaPackageInfo createFirmware(String version, DeviceProfileId deviceProfileId) throws Exception {
String CHECKSUM = "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a"; String CHECKSUM = "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a";
OtaPackageInfo firmwareInfo = new OtaPackageInfo(); OtaPackageInfo firmwareInfo = new OtaPackageInfo();
firmwareInfo.setDeviceProfileId(deviceProfile.getId()); firmwareInfo.setDeviceProfileId(deviceProfileId);
firmwareInfo.setType(FIRMWARE); firmwareInfo.setType(FIRMWARE);
firmwareInfo.setTitle("My firmware"); firmwareInfo.setTitle("My firmware");
firmwareInfo.setVersion("v1.0"); firmwareInfo.setVersion(version);
OtaPackageInfo savedFirmwareInfo = doPost("/api/otaPackage", firmwareInfo, OtaPackageInfo.class); OtaPackageInfo savedFirmwareInfo = doPost("/api/otaPackage", firmwareInfo, OtaPackageInfo.class);
@ -95,11 +123,11 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
return savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, "SHA256"); return savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, "SHA256");
} }
protected OtaPackageInfo createSoftware() throws Exception { protected OtaPackageInfo createSoftware(DeviceProfileId deviceProfileId) throws Exception {
String CHECKSUM = "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a"; String CHECKSUM = "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a";
OtaPackageInfo swInfo = new OtaPackageInfo(); OtaPackageInfo swInfo = new OtaPackageInfo();
swInfo.setDeviceProfileId(deviceProfile.getId()); swInfo.setDeviceProfileId(deviceProfileId);
swInfo.setType(SOFTWARE); swInfo.setType(SOFTWARE);
swInfo.setTitle("My sw"); swInfo.setTitle("My sw");
swInfo.setVersion("v1.0"); swInfo.setVersion("v1.0");
@ -117,4 +145,28 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg
setJwtToken(postRequest); setJwtToken(postRequest);
return readResponse(mockMvc.perform(postRequest).andExpect(status().isOk()), OtaPackageInfo.class); return readResponse(mockMvc.perform(postRequest).andExpect(status().isOk()), OtaPackageInfo.class);
} }
protected Device getDeviceFromAPI(UUID deviceId) throws Exception {
final Device device = doGet("/api/device/" + deviceId, Device.class);
log.trace("Fetched device by API for deviceId {}, device is {}", deviceId, device);
return device;
}
protected List<TsKvEntry> getFwSwStateTelemetryFromAPI(UUID deviceId, String type_state) throws Exception {
final List<TsKvEntry> tsKvEntries = toTimeseries(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?orderBy=ASC&keys=" + type_state + "&startTs=0&endTs=" + System.currentTimeMillis(), new TypeReference<>() {
}));
log.warn("Fetched telemetry by API for deviceId {}, list size {}, tsKvEntries {}", deviceId, tsKvEntries.size(), tsKvEntries);
return tsKvEntries;
}
protected boolean predicateForStatuses(List<TsKvEntry> ts) {
List<OtaPackageUpdateStatus> statuses = ts.stream()
.sorted(Comparator.comparingLong(TsKvEntry::getTs))
.map(KvEntry::getValueAsString)
.map(OtaPackageUpdateStatus::valueOf)
.collect(Collectors.toList());
log.warn("{}", statuses);
return statuses.containsAll(expectedStatuses);
}
} }

100
application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java → application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java

@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
@ -29,9 +30,7 @@ import org.thingsboard.server.transport.lwm2m.ota.AbstractOtaLwM2MIntegrationTes
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -45,24 +44,21 @@ import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.INIT
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.QUEUED; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.QUEUED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATED; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.VERIFIED;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
@Slf4j @Slf4j
public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { public class Ota5LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest {
private List<OtaPackageUpdateStatus> expectedStatuses;
@Test @Test
public void testFirmwareUpdateWithClientWithoutFirmwareOtaInfoFromProfile() throws Exception { public void testFirmwareUpdateWithClientWithoutFirmwareOtaInfoFromProfile_IsNotSupported() throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS, getBootstrapServerCredentialsNoSec(NONE)); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS, getBootstrapServerCredentialsNoSec(NONE));
createDeviceProfile(transportConfiguration); DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_WITHOUT_FW_INFO, transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_WITHOUT_FW_INFO)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_WITHOUT_FW_INFO));
final Device device = createDevice(deviceCredentials, this.CLIENT_ENDPOINT_WITHOUT_FW_INFO); final Device device = createLwm2mDevice(deviceCredentials, this.CLIENT_ENDPOINT_WITHOUT_FW_INFO, deviceProfile.getId());
createNewClient(SECURITY_NO_SEC, null, false, this.CLIENT_ENDPOINT_WITHOUT_FW_INFO); createNewClient(SECURITY_NO_SEC, null, false, this.CLIENT_ENDPOINT_WITHOUT_FW_INFO, device.getId().getId().toString());
awaitObserveReadAll(0, device.getId().getId().toString()); awaitObserveReadAll(0, device.getId().getId().toString());
device.setFirmwareId(createFirmware().getId()); device.setFirmwareId(createFirmware("5.1", deviceProfile.getId()).getId());
final Device savedDevice = doPost("/api/device", device, Device.class); final Device savedDevice = doPost("/api/device", device, Device.class);
Thread.sleep(1000); Thread.sleep(1000);
@ -78,81 +74,37 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest {
Assert.assertEquals(expectedStatuses, statuses); Assert.assertEquals(expectedStatuses, statuses);
} }
/**
* /5/0/5 -> Update Result (Res); 5/0/3 -> State;
* => ((Res>=0 && Res<=9) && State=0)
* => Write to Package/Write to Package URI -> DOWNLOADING ((Res>=0 && Res<=9) && State=1)
* => Download Finished -> DOWNLOADED ((Res==0 || Res=8) && State=2)
* => Executable resource Update is triggered / Initiate Firmware Update -> UPDATING (Res=0 && State=3)
* => Update Successful [Res==1]
* => Start / Res=0 -> "IDLE" ....
* @throws Exception
*/
@Test @Test
public void testFirmwareUpdateByObject5() throws Exception { public void testFirmwareUpdateByObject5_Ok() throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA, getBootstrapServerCredentialsNoSec(NONE)); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA5, getBootstrapServerCredentialsNoSec(NONE));
createDeviceProfile(transportConfiguration); DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_OTA5, transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA5)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA5));
final Device device = createDevice(deviceCredentials, this.CLIENT_ENDPOINT_OTA5); final Device device = createLwm2mDevice(deviceCredentials, this.CLIENT_ENDPOINT_OTA5, deviceProfile.getId());
createNewClient(SECURITY_NO_SEC, null, false, this.CLIENT_ENDPOINT_OTA5); createNewClient(SECURITY_NO_SEC, null, false, this.CLIENT_ENDPOINT_OTA5, device.getId().getId().toString());
awaitObserveReadAll(9, device.getId().getId().toString()); awaitObserveReadAll(5, device.getId().getId().toString());
device.setFirmwareId(createFirmware().getId()); device.setFirmwareId(createFirmware("fw.v.1.5.0-update", deviceProfile.getId()).getId());
final Device savedDevice = doPost("/api/device", device, Device.class); final Device savedDevice = doPost("/api/device", device, Device.class);
assertThat(savedDevice).as("saved device").isNotNull(); assertThat(savedDevice).as("saved device").isNotNull();
assertThat(getDeviceFromAPI(device.getId().getId())).as("fetched device").isEqualTo(savedDevice); assertThat(getDeviceFromAPI(device.getId().getId())).as("fetched device").isEqualTo(savedDevice);
expectedStatuses = Arrays.asList(QUEUED, INITIATED, DOWNLOADING, DOWNLOADED, UPDATING, UPDATED); expectedStatuses = Arrays.asList(QUEUED, INITIATED, DOWNLOADING, DOWNLOADED, UPDATING, UPDATED);
List<TsKvEntry> ts = await("await on timeseries") List<TsKvEntry> ts = await("await on timeseries for FW")
.atMost(TIMEOUT, TimeUnit.SECONDS) .atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> toTimeseries(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + .until(() -> getFwSwStateTelemetryFromAPI(device.getId().getId(), "fw_state"), this::predicateForStatuses);
savedDevice.getId().getId() + "/values/timeseries?orderBy=ASC&keys=fw_state&startTs=0&endTs=" +
System.currentTimeMillis(), new TypeReference<>() {
})), this::predicateForStatuses);
log.warn("Object5: Got the ts: {}", ts); log.warn("Object5: Got the ts: {}", ts);
} }
/**
* This is the example how to use the AWAITILITY instead Thread.sleep()
* Test will finish as fast as possible, but will await until TIMEOUT if a build machine is busy or slow
* Check the detailed log output to learn how Awaitility polling the API and when exactly expected result appears
* */
@Test
public void testSoftwareUpdateByObject9() throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA, getBootstrapServerCredentialsNoSec(NONE));
createDeviceProfile(transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA9));
final Device device = createDevice(deviceCredentials, this.CLIENT_ENDPOINT_OTA9);
createNewClient(SECURITY_NO_SEC, null, false, this.CLIENT_ENDPOINT_OTA9);
awaitObserveReadAll(9, device.getId().getId().toString());
device.setSoftwareId(createSoftware().getId());
final Device savedDevice = doPost("/api/device", device, Device.class); //sync call
assertThat(savedDevice).as("saved device").isNotNull();
assertThat(getDeviceFromAPI(device.getId().getId())).as("fetched device").isEqualTo(savedDevice);
expectedStatuses = List.of(
QUEUED, INITIATED, DOWNLOADING, DOWNLOADING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATED);
List<TsKvEntry> ts = await("await on timeseries")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> getSwStateTelemetryFromAPI(device.getId().getId()), this::predicateForStatuses);
log.warn("Object9: Got the ts: {}", ts);
}
private Device getDeviceFromAPI(UUID deviceId) throws Exception {
final Device device = doGet("/api/device/" + deviceId, Device.class);
log.trace("Fetched device by API for deviceId {}, device is {}", deviceId, device);
return device;
}
private List<TsKvEntry> getSwStateTelemetryFromAPI(UUID deviceId) throws Exception {
final List<TsKvEntry> tsKvEntries = toTimeseries(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?orderBy=ASC&keys=sw_state&startTs=0&endTs=" + System.currentTimeMillis(), new TypeReference<>() {
}));
log.warn("Fetched telemetry by API for deviceId {}, list size {}, tsKvEntries {}", deviceId, tsKvEntries.size(), tsKvEntries);
return tsKvEntries;
}
private boolean predicateForStatuses(List<TsKvEntry> ts) {
List<OtaPackageUpdateStatus> statuses = ts.stream()
.sorted(Comparator.comparingLong(TsKvEntry::getTs))
.map(KvEntry::getValueAsString)
.map(OtaPackageUpdateStatus::valueOf)
.collect(Collectors.toList());
log.warn("{}", statuses);
return statuses.containsAll(expectedStatuses);
}
} }

73
application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota9LwM2MIntegrationTest.java

@ -0,0 +1,73 @@
/**
* 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.
*/
package org.thingsboard.server.transport.lwm2m.ota.sql;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.transport.lwm2m.ota.AbstractOtaLwM2MIntegrationTest;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADING;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.INITIATED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.QUEUED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.VERIFIED;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
@Slf4j
public class Ota9LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest {
/**
* => Start -> INITIAL (State=0) -> DOWNLOAD STARTED;
* => PKG / URI Write -> DOWNLOAD STARTED (Res=1 (Downloading) && State=1) -> DOWNLOADED
* => PKG Written -> DOWNLOADED (Res=1 Initial && State=2) -> DELIVERED;
* => PKG integrity verified -> DELIVERED (Res=3 (Successfully Downloaded and package integrity verified) && State=3) -> INSTALLED;
* => Install -> INSTALLED (Res=2 SW successfully installed) && State=4) -> Start
*
* */
@Test
public void testSoftwareUpdateByObject9() throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA9, getBootstrapServerCredentialsNoSec(NONE));
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_OTA9, transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA9));
final Device device = createLwm2mDevice(deviceCredentials, this.CLIENT_ENDPOINT_OTA9, deviceProfile.getId());
createNewClient(SECURITY_NO_SEC, null, false, this.CLIENT_ENDPOINT_OTA9, device.getId().getId().toString());
awaitObserveReadAll(4, device.getId().getId().toString());
device.setSoftwareId(createSoftware(deviceProfile.getId()).getId());
final Device savedDevice = doPost("/api/device", device, Device.class); //sync call
assertThat(savedDevice).as("saved device").isNotNull();
assertThat(getDeviceFromAPI(device.getId().getId())).as("fetched device").isEqualTo(savedDevice);
expectedStatuses = List.of(
QUEUED, INITIATED, DOWNLOADING, DOWNLOADING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATED);
List<TsKvEntry> ts = await("await on timeseries")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> getFwSwStateTelemetryFromAPI(device.getId().getId(), "sw_state"), this::predicateForStatuses);
log.warn("Object9: Got the ts: {}", ts);
}
}

9
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java

@ -15,8 +15,8 @@
*/ */
package org.thingsboard.server.transport.lwm2m.rpc; package org.thingsboard.server.transport.lwm2m.rpc;
import org.junit.Before;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import static org.junit.Assert.assertTrue;
@DaoSqlTest @DaoSqlTest
public abstract class AbstractRpcLwM2MIntegrationObserveTest extends AbstractRpcLwM2MIntegrationTest{ public abstract class AbstractRpcLwM2MIntegrationObserveTest extends AbstractRpcLwM2MIntegrationTest{
@ -26,9 +26,8 @@ public abstract class AbstractRpcLwM2MIntegrationObserveTest extends AbstractRpc
setResources(this.RESOURCES_RPC_MULTIPLE_19); setResources(this.RESOURCES_RPC_MULTIPLE_19);
} }
@Before protected void sendRpcObserveWithContainsLwM2mSingleResource(String params) throws Exception {
public void initTest () throws Exception { String rpcActualResult = sendRpcObserveOkWithResultValue("Observe", params);
awaitObserveReadAll(4, deviceId); assertTrue(rpcActualResult.contains("LwM2mSingleResource") || rpcActualResult.contains("LwM2mMultipleResource"));
} }
} }

96
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java

@ -22,13 +22,13 @@ import org.junit.Before;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest; import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper;
import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -71,7 +71,7 @@ import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fr
public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
protected final LinkParser linkParser = new DefaultLwM2mLinkParser(); protected final LinkParser linkParser = new DefaultLwM2mLinkParser();
protected String OBSERVE_ATTRIBUTES_WITH_PARAMS_RPC; protected String CONFIG_PROFILE_WITH_PARAMS_RPC;
public Set expectedObjects; public Set expectedObjects;
public Set expectedObjectIdVers; public Set expectedObjectIdVers;
public Set expectedInstances; public Set expectedInstances;
@ -98,9 +98,6 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
protected String idVer_19_0_0; protected String idVer_19_0_0;
@SpyBean
protected DefaultLwM2mUplinkMsgHandler defaultUplinkMsgHandlerTest;
@SpyBean @SpyBean
protected LwM2mTransportServerHelper lwM2mTransportServerHelperTest; protected LwM2mTransportServerHelper lwM2mTransportServerHelperTest;
@ -110,18 +107,21 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
@Before @Before
public void startInitRPC() throws Exception { public void startInitRPC() throws Exception {
if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationDiscoverWriteAttributesTest")){ if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationWriteCborTest")) {
isWriteAttribute = true;
}
if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationWriteCborTest")){
supportFormatOnly_SenMLJSON_SenMLCBOR = true; supportFormatOnly_SenMLJSON_SenMLCBOR = true;
} }
initRpc(); if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationObserveTest")) {
initRpc(0);
} else if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationReadCollectedValueTest")) {
initRpc(3303);
} else {
initRpc(1);
}
} }
private void initRpc () throws Exception { protected void initRpc(int typeConfigProfile) throws Exception {
String endpoint = DEVICE_ENDPOINT_RPC_PREF + endpointSequence.incrementAndGet(); String endpoint = DEVICE_ENDPOINT_RPC_PREF + endpointSequence.incrementAndGet();
createNewClient(SECURITY_NO_SEC, null, true, endpoint); createNewClient(SECURITY_NO_SEC, null, true, endpoint, null);
expectedObjects = ConcurrentHashMap.newKeySet(); expectedObjects = ConcurrentHashMap.newKeySet();
expectedObjectIdVers = ConcurrentHashMap.newKeySet(); expectedObjectIdVers = ConcurrentHashMap.newKeySet();
expectedInstances = ConcurrentHashMap.newKeySet(); expectedInstances = ConcurrentHashMap.newKeySet();
@ -154,18 +154,17 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
idVer_3_0_0 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0; idVer_3_0_0 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0;
idVer_3_0_9 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_9; idVer_3_0_9 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_9;
id_3_0_9 = fromVersionedIdToObjectId(idVer_3_0_9); id_3_0_9 = fromVersionedIdToObjectId(idVer_3_0_9);
idVer_19_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0; idVer_19_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0;
OBSERVE_ATTRIBUTES_WITH_PARAMS_RPC = String ATTRIBUTES_TELEMETRY_WITH_PARAMS_RPC_WITH_OBSERVE =
" {\n" + " {\n" +
" \"keyName\": {\n" + " \"keyName\": {\n" +
" \"" + idVer_3_0_9 + "\": \"" + RESOURCE_ID_NAME_3_9 + "\",\n" + " \"" + idVer_3_0_9 + "\": \"" + RESOURCE_ID_NAME_3_9 + "\",\n" +
" \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14 + "\": \"" + RESOURCE_ID_NAME_3_14 + "\",\n" + " \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14 + "\": \"" + RESOURCE_ID_NAME_3_14 + "\",\n" +
" \"" + idVer_19_0_0 + "\": \"" + RESOURCE_ID_NAME_19_0_0 + "\",\n" + " \"" + idVer_19_0_0 + "\": \"" + RESOURCE_ID_NAME_19_0_0 + "\",\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\": \"" + RESOURCE_ID_NAME_19_1_0 + "\",\n" + " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\": \"" + RESOURCE_ID_NAME_19_1_0 + "\",\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2 + "\": \"" + RESOURCE_ID_NAME_19_0_2 + "\",\n" + " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2 + "\": \"" + RESOURCE_ID_NAME_19_0_2 + "\"\n" +
" \"" + objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12 + "/" + RESOURCE_ID_5700 + "\": \"" + RESOURCE_ID_NAME_3303_12_5700 + "\"\n" +
" },\n" + " },\n" +
" \"observe\": [\n" + " \"observe\": [\n" +
" \"" + idVer_3_0_9 + "\",\n" + " \"" + idVer_3_0_9 + "\",\n" +
@ -180,19 +179,60 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
" \"telemetry\": [\n" + " \"telemetry\": [\n" +
" \"" + idVer_3_0_9 + "\",\n" + " \"" + idVer_3_0_9 + "\",\n" +
" \"" + idVer_19_0_0 + "\",\n" + " \"" + idVer_19_0_0 + "\",\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\",\n" + " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\"\n" +
" \"" + objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12 + "/" + RESOURCE_ID_5700 + "\"\n" +
" ],\n" + " ],\n" +
" \"attributeLwm2m\": {}\n" + " \"attributeLwm2m\": {}\n" +
" }"; " }";
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS_RPC, getBootstrapServerCredentialsNoSec(NONE)); String TELEMETRY_WITH_PARAMS_RPC_WITHOUT_OBSERVE =
createDeviceProfile(transportConfiguration); " {\n" +
" \"keyName\": {\n" +
" \"" + idVer_3_0_9 + "\": \"" + RESOURCE_ID_NAME_3_9 + "\",\n" +
" \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14 + "\": \"" + RESOURCE_ID_NAME_3_14 + "\",\n" +
" \"" + idVer_19_0_0 + "\": \"" + RESOURCE_ID_NAME_19_0_0 + "\",\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\": \"" + RESOURCE_ID_NAME_19_1_0 + "\",\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2 + "\": \"" + RESOURCE_ID_NAME_19_0_2 + "\"\n" +
" },\n" +
" \"observe\": [\n" +
" ],\n" +
" \"attribute\": [\n" +
" \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14 + "\",\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2 + "\"\n" +
" ],\n" +
" \"telemetry\": [\n" +
" \"" + idVer_3_0_9 + "\",\n" +
" \"" + idVer_19_0_0 + "\",\n" +
" \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\"\n" +
" ],\n" +
" \"attributeLwm2m\": {}\n" +
" }";
String TELEMETRY_WITH_PARAMS_RPC_COLLECTED_VALUE =
" {\n" +
" \"keyName\": {\n" +
" \"" + objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12 + "/" + RESOURCE_ID_5700 + "\": \"" + RESOURCE_ID_NAME_3303_12_5700 + "\"\n" +
" },\n" +
" \"observe\": [\n" +
" ],\n" +
" \"attribute\": [\n" +
" ],\n" +
" \"telemetry\": [\n" +
" \"" + objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12 + "/" + RESOURCE_ID_5700 + "\"\n" +
" ],\n" +
" \"attributeLwm2m\": {}\n" +
" }";
CONFIG_PROFILE_WITH_PARAMS_RPC =
switch (typeConfigProfile) {
case 0 -> ATTRIBUTES_TELEMETRY_WITH_PARAMS_RPC_WITH_OBSERVE;
case 1 -> TELEMETRY_WITH_PARAMS_RPC_WITHOUT_OBSERVE;
case 3303 -> TELEMETRY_WITH_PARAMS_RPC_COLLECTED_VALUE;
default -> throw new IllegalStateException("Unexpected value: " + typeConfigProfile);
};
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(CONFIG_PROFILE_WITH_PARAMS_RPC, getBootstrapServerCredentialsNoSec(NONE));
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + endpoint, transportConfiguration);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(endpoint)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(endpoint));
final Device device = createDevice(deviceCredentials, endpoint); final Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId());
deviceId = device.getId().getId().toString(); lwM2MTestClient.setDeviceIdStr(device.getId().getId().toString());
lwM2MTestClient.start(true); lwM2MTestClient.start(true);
} }
@ -236,14 +276,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
log.trace("updateRegAtLeastOnceAfterAction: newInvocationCount [{}]", newInvocationCount.get()); log.trace("updateRegAtLeastOnceAfterAction: newInvocationCount [{}]", newInvocationCount.get());
} }
protected long countUpdateReg() { protected long countSendParametersOnThingsboardTelemetryResource(String rezName) {
return Mockito.mockingDetails(defaultUplinkMsgHandlerTest)
.getInvocations().stream()
.filter(invocation -> invocation.getMethod().getName().equals("updatedReg"))
.count();
}
protected long countSendParametersOnThingsboardTelemetryResource(String rezName) {
return Mockito.mockingDetails(lwM2mTransportServerHelperTest) return Mockito.mockingDetails(lwM2mTransportServerHelperTest)
.getInvocations().stream() .getInvocations().stream()
.filter(invocation -> .filter(invocation ->
@ -256,5 +289,4 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
) )
.count(); .count();
} }
} }

74
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java

@ -58,7 +58,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/ */
@Test @Test
public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_LwM2mResourceInstance() throws Exception { public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_LwM2mResourceInstance() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
@ -81,7 +80,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/ */
@Test @Test
public void testObserveComposite_ObjectInstanceWithOtherObjectResourceInstance_Result_CONTENT_Ok() throws Exception { public void testObserveComposite_ObjectInstanceWithOtherObjectResourceInstance_Result_CONTENT_Ok() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0; String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0;
String expectedIdVer5_0 = objectInstanceIdVer_5; String expectedIdVer5_0 = objectInstanceIdVer_5;
String expectedIds = "[\"" + expectedIdVer19_1_0 + "\", \"" + expectedIdVer5_0 + "\"]"; String expectedIds = "[\"" + expectedIdVer19_1_0 + "\", \"" + expectedIdVer5_0 + "\"]";
@ -100,7 +98,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/ */
@Test @Test
public void testObserveReadAll_AfterCompositeObservation_WithResourceNotReadable_Result_CONTENT_ObserveResourceNotReadableIsNull() throws Exception { public void testObserveReadAll_AfterCompositeObservation_WithResourceNotReadable_Result_CONTENT_ObserveResourceNotReadableIsNull() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_2 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2; String expectedIdVer5_0_2 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2;
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_2 + "\"]"; String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_2 + "\"]";
@ -120,7 +117,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/ */
@Test @Test
public void testObserveComposite_Result_BAD_REQUEST_ONE_PATH_CONTAINCE_OTHER() throws Exception { public void testObserveComposite_Result_BAD_REQUEST_ONE_PATH_CONTAINCE_OTHER() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
String expectedIdVer5_0 = objectInstanceIdVer_5; String expectedIdVer5_0 = objectInstanceIdVer_5;
String expectedIdVer5_0_2 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2; String expectedIdVer5_0_2 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2;
String expectedIds = "[\"" + expectedIdVer5_0 + "\", \"" + expectedIdVer5_0_2 + "\"]"; String expectedIds = "[\"" + expectedIdVer5_0 + "\", \"" + expectedIdVer5_0_2 + "\"]";
@ -133,7 +129,7 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
} }
/** /**
* Previous -> "3/0/9", "19/0/2", "19/1/0", "19/0/0", All only SingleObservation; * Previous -> "3/0/9" SingleObservation;
* if at least one of the resource objectIds (Composite) in SingleObservation or CompositeObservation is already registered - return BAD REQUEST * if at least one of the resource objectIds (Composite) in SingleObservation or CompositeObservation is already registered - return BAD REQUEST
* ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9"]} * ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9"]}
* @throws Exception * @throws Exception
@ -145,13 +141,8 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
String actualValues = rpcActualResultReadAll.get("value").asText(); String actualValues = rpcActualResultReadAll.get("value").asText();
String expectedIdVer19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2; assertTrue(actualValues.contains("[]"));
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0; sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9);
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_3_0_9)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_0_2)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_1_0)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_19_0_0)));
// Send Observe composite with "/3/0/9"
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
@ -167,9 +158,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
actualValues = rpcActualResultReadAll.get("value").asText(); actualValues = rpcActualResultReadAll.get("value").asText();
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_3_0_9))); assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_3_0_9)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_0_2)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_1_0)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_19_0_0)));
} }
/** /**
* Previous -> ["5/0/7", "5/0/5", "5/0/3"], CompositeObservation * * Previous -> ["5/0/7", "5/0/5", "5/0/3"], CompositeObservation *
@ -206,12 +194,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
actualValues = rpcActualResultReadAll.get("value").asText(); actualValues = rpcActualResultReadAll.get("value").asText();
String expectedIdVer19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2;
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0;
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_3_0_9)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_0_2)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_1_0)));
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_19_0_0)));
assertTrue(actualValues.contains("CompositeObservation:")); assertTrue(actualValues.contains("CompositeObservation:"));
assertTrue(actualValues.contains(expectedId5_0_7)); assertTrue(actualValues.contains(expectedId5_0_7));
assertTrue(actualValues.contains(expectedId5_0_5)); assertTrue(actualValues.contains(expectedId5_0_5));
@ -224,8 +206,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/ */
@Test @Test
public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_LwM2mMultipleResource() throws Exception { public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_LwM2mMultipleResource() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
@ -248,8 +228,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/ */
@Test @Test
public void testObserveCompositeWithKeyName_Result_CONTENT_Value_SingleResources() throws Exception { public void testObserveCompositeWithKeyName_Result_CONTENT_Value_SingleResources() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9; String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9;
String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14;
String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0; String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0;
@ -274,6 +252,7 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/ */
@Test @Test
public void testObserveCompositeWithKeyName_IfLeastOneResourceIsAlreadyRegistered_return_BadRequest() throws Exception { public void testObserveCompositeWithKeyName_IfLeastOneResourceIsAlreadyRegistered_return_BadRequest() throws Exception {
sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9);
String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9; String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9;
String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14;
String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0; String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0;
@ -292,8 +271,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/ */
@Test @Test
public void testObserveReadAll_AfterbserveCancelAllAndCompositeObservation_Result_CONTENT_Value_CompositeObservation_Only() throws Exception { public void testObserveReadAll_AfterbserveCancelAllAndCompositeObservation_Result_CONTENT_Value_CompositeObservation_Only() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9; String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9;
String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14;
String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0; String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0;
@ -323,7 +300,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/ */
@Test @Test
public void testObserveCancelAllThenObserveCompositeAnyResources_Result_CONTENT_CancelObserveComposite_This_Result_Content_Count_1() throws Exception { public void testObserveCancelAllThenObserveCompositeAnyResources_Result_CONTENT_CancelObserveComposite_This_Result_Content_Count_1() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
// ObserveComposite // ObserveComposite
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
@ -339,7 +315,7 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
assertEquals("1", rpcActualResult.get("value").asText()); assertEquals("1", rpcActualResult.get("value").asText());
assertEquals(0, (Object) getCntObserveAll(deviceId)); assertEquals(0, (Object) getCntObserveAll(lwM2MTestClient.getDeviceIdStr()));
} }
/** /**
@ -349,7 +325,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/ */
@Test @Test
public void testObserveCompositeFiveResources_Result_CONTENT_CancelObserveComposite_TwoAnyResource_Result_BadRequest() throws Exception { public void testObserveCompositeFiveResources_Result_CONTENT_CancelObserveComposite_TwoAnyResource_Result_BadRequest() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
// ObserveComposite five // ObserveComposite five
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7;
String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5;
@ -360,7 +335,7 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
awaitObserveReadAll(1, deviceId); awaitObserveReadAll(1, lwM2MTestClient.getDeviceIdStr());
// ObserveCompositeCancel two // ObserveCompositeCancel two
expectedIds = "[\"" + objectInstanceIdVer_5 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; expectedIds = "[\"" + objectInstanceIdVer_5 + "\", \"" + expectedIdVer19_1_0_0 + "\"]";
@ -377,7 +352,6 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
*/ */
@Test @Test
public void testObserveOneObjectAnyResources_Result_CONTENT_Cancel_OneResourceFromObjectAnyResource_Result_BAD_REQUEST_Cancel_OneObject_Result_CONTENT() throws Exception { public void testObserveOneObjectAnyResources_Result_CONTENT_Cancel_OneResourceFromObjectAnyResource_Result_BAD_REQUEST_Cancel_OneObject_Result_CONTENT() throws Exception {
sendObserveCancelAllWithAwait(deviceId);
// ObserveComposite // ObserveComposite
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3;
String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0; String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0;
@ -412,17 +386,25 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
String idVer_19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2; String idVer_19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2;
String id_19_0_2 = fromVersionedIdToObjectId(idVer_19_0_2); String id_19_0_2 = fromVersionedIdToObjectId(idVer_19_0_2);
// 1 - "ObserveReadAll": at least one update value of all resources we observe - after connection // 1 - Verify after start
String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null); String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
String rpcActualVValuesReadAll = rpcActualResultReadAll.get("value").asText(); String actualValues = rpcActualResultReadAll.get("value").asText();
ArrayNode rpcactualValues = JacksonUtil.fromString(rpcActualVValuesReadAll, ArrayNode.class); assertTrue(actualValues.contains("[]"));
assertEquals(rpcactualValues.size(), 4); sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9);
assertTrue(actualResultReadAll.contains("SingleObservation:" + id_3_0_9)); sendRpcObserveWithContainsLwM2mSingleResource(idVer_19_0_0);
assertTrue(actualResultReadAll.contains("SingleObservation:" + id_19_1_0)); sendRpcObserveWithContainsLwM2mSingleResource(idVer_19_1_0);
assertTrue(actualResultReadAll.contains("SingleObservation:" + id_19_0_2)); sendRpcObserveWithContainsLwM2mSingleResource(idVer_19_0_2);
assertTrue(actualResultReadAll.contains("SingleObservation:" + id_19_0_0));
actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
actualValues = rpcActualResultReadAll.get("value").asText();
assertTrue(actualValues.contains("SingleObservation:" + id_3_0_9));
assertTrue(actualValues.contains("SingleObservation:" + id_19_1_0));
assertTrue(actualValues.contains("SingleObservation:" + id_19_0_2));
assertTrue(actualValues.contains("SingleObservation:" + id_19_0_0));
long initAttrTelemetryAtCount = countUpdateAttrTelemetryAll(); long initAttrTelemetryAtCount = countUpdateAttrTelemetryAll();
long initAttrTelemetryAtCount_3_0_9 = countUpdateAttrTelemetryResource(idVer_3_0_9); long initAttrTelemetryAtCount_3_0_9 = countUpdateAttrTelemetryResource(idVer_3_0_9);
long initAttrTelemetryAtCount_19_0_0 = countUpdateAttrTelemetryResource(idVer_19_0_0); long initAttrTelemetryAtCount_19_0_0 = countUpdateAttrTelemetryResource(idVer_19_0_0);
@ -436,13 +418,13 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
updateAttrTelemetryResourceAtLeastOnceAfterAction(initAttrTelemetryAtCount_19_0_2, idVer_19_0_2); updateAttrTelemetryResourceAtLeastOnceAfterAction(initAttrTelemetryAtCount_19_0_2, idVer_19_0_2);
// 2 - "ObserveReadAll": No update of all resources we are observing - after "ObserveReadCancelAll" // 2 - "ObserveReadAll": No update of all resources we are observing - after "ObserveReadCancelAll"
sendObserveCancelAllWithAwait(deviceId); sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr());
updateRegAtLeastOnceAfterAction(); updateRegAtLeastOnceAfterAction();
actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null); actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null);
rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText());
rpcActualVValuesReadAll = rpcActualResultReadAll.get("value").asText(); String rpcActualVValuesReadAll = rpcActualResultReadAll.get("value").asText();
rpcactualValues = JacksonUtil.fromString(rpcActualVValuesReadAll, ArrayNode.class); ArrayNode rpcactualValues = JacksonUtil.fromString(rpcActualVValuesReadAll, ArrayNode.class);
assertEquals(rpcactualValues.size(), 0); assertEquals(rpcactualValues.size(), 0);
// 2.1 - ObserveComposite: observeCancelAll verify" // 2.1 - ObserveComposite: observeCancelAll verify"
initAttrTelemetryAtCount = countUpdateAttrTelemetryAll(); initAttrTelemetryAtCount = countUpdateAttrTelemetryAll();
@ -497,17 +479,17 @@ public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MInt
} else { } else {
sendRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"id\": \"" + params + "\"}}"; sendRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"id\": \"" + params + "\"}}";
} }
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, sendRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), sendRpcRequest, String.class, status().isOk());
} }
private String sendCompositeRPCByIds(String method, String paths) throws Exception { private String sendCompositeRPCByIds(String method, String paths) throws Exception {
String setRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"ids\":" + paths + "}}"; String setRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"ids\":" + paths + "}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
} }
private String sendCompositeRPCByKeys(String method, String keys) throws Exception { private String sendCompositeRPCByKeys(String method, String keys) throws Exception {
String sendRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"keys\":" + keys + "}}"; String sendRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"keys\":" + keys + "}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, sendRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), sendRpcRequest, String.class, status().isOk());
} }
private void updateAttrTelemetryAllAtLeastOnceAfterAction(long initialInvocationCount) { private void updateAttrTelemetryAllAtLeastOnceAfterAction(long initialInvocationCount) {

2
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java

@ -127,7 +127,7 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe
private String sendRPCreateById(String path, String value) throws Exception { private String sendRPCreateById(String path, String value) throws Exception {
String setRpcRequest = "{\"method\": \"Create\", \"params\": {\"id\": \"" + path + "\", \"value\": " + value + " }}"; String setRpcRequest = "{\"method\": \"Create\", \"params\": {\"id\": \"" + path + "\", \"value\": " + value + " }}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
} }
} }

2
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java

@ -90,7 +90,7 @@ public class RpcLwm2mIntegrationDeleteTest extends AbstractRpcLwM2MIntegrationTe
private String sendRPCDeleteById(String path) throws Exception { private String sendRPCDeleteById(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Delete\", \"params\": {\"id\": \"" + path + "\"}}"; String setRpcRequest = "{\"method\": \"Delete\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
} }
} }

31
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java

@ -22,6 +22,8 @@ import org.eclipse.leshan.core.link.Link;
import org.eclipse.leshan.core.link.LinkParseException; import org.eclipse.leshan.core.link.LinkParseException;
import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mPath;
import org.junit.Test; import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.test.context.event.annotation.BeforeTestClass;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.transport.lwm2m.config.TbLwM2mVersion; import org.thingsboard.server.transport.lwm2m.config.TbLwM2mVersion;
@ -30,8 +32,10 @@ import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTes
import java.util.Arrays; import java.util.Arrays;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -39,10 +43,16 @@ import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPA
import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6;
public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegrationTest { public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegrationTest {
@BeforeEach
public void beforeTest () throws Exception {
testInit();
}
/** /**
* DiscoverAll * DiscoverAll
* *
@ -51,7 +61,7 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration
@Test @Test
public void testDiscoverAll_Return_CONTENT_LinksAllObjectsAllInstancesOfClient() throws Exception { public void testDiscoverAll_Return_CONTENT_LinksAllObjectsAllInstancesOfClient() throws Exception {
String setRpcRequest = "{\"method\":\"DiscoverAll\"}"; String setRpcRequest = "{\"method\":\"DiscoverAll\"}";
String actualResult = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); String actualResult = doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
JsonNode rpcActualValue = JacksonUtil.toJsonNode(rpcActualResult.get("value").asText()); JsonNode rpcActualValue = JacksonUtil.toJsonNode(rpcActualResult.get("value").asText());
@ -171,9 +181,20 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration
assertEquals(ResponseCode.NOT_FOUND.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.NOT_FOUND.getName(), rpcActualResult.get("result").asText());
} }
@Test
public void testDiscoverRequestCannotTargetResourceInstance_Return_INTERNAL_SERVER_ERROR() throws Exception {
// ResourceInstanceId
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6 + "/1";
String actualResult = sendDiscover(expectedPath);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.INTERNAL_SERVER_ERROR.getName(), rpcActualResult.get("result").asText());
String expected = "InvalidRequestException: Discover request cannot target resource instance path: /3/0/6/1";
assertTrue(rpcActualResult.get("error").asText().contains(expected));
}
private String sendDiscover(String path) throws Exception { private String sendDiscover(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}"; String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
} }
private String convertObjectIdToVerId(String path, String ver) { private String convertObjectIdToVerId(String path, String ver) {
@ -190,4 +211,10 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration
return null; return null;
} }
} }
public void testInit() throws Exception {
await("Update Registration at-least-once after start")
.atMost(50, TimeUnit.SECONDS)
.until(() -> countUpdateReg() > 0);
}
} }

201
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java

@ -19,12 +19,12 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.ResponseCode;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.transport.util.JsonUtils;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7;
@ -32,68 +32,22 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID
public class RpcLwm2mIntegrationDiscoverWriteAttributesTest extends AbstractRpcLwM2MIntegrationTest { public class RpcLwm2mIntegrationDiscoverWriteAttributesTest extends AbstractRpcLwM2MIntegrationTest {
/** /**
* WriteAttributes {"id":"/3_1.2/0/6","attributes":{"pmax":100, "pmin":10}} * <PROPERTIES> Class Attributes
* if not implemented: * - dim (0-65535) Integer: Multiple-Instance Resource; R, Number of instances existing for a Multiple-Instance Resource
* {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"} * <NOTIFICATION> Class Attributes
* if implemented: * - pmin (def = 0(sec)) Integer: Object; Object Instance; Resource; Resource Instance; RW, Readable Resource
* {"result":"BAD_REQUEST","error":"Attribute pmax can be used for only Resource/Object Instance/Object."} * - pmax (def = -- ) Integer: Object; Object Instance; Resource; Resource Instance; RW, Readable Resource
* - Greater Than gt (def = -- ) Float: Resource; Resource Instance; RW, Numerical&Readable Resource
* - Less Than lt (def = -- ) Float: Resource; Resource Instance; RW, Numerical&Readable Resource
* - Step st (def = -- ) Float: Resource; Resource Instance; RW, Numerical&Readable Resource
*/ */
@Test
public void testWriteAttributesResourceWithParametersByResourceInstanceId_Result_BAD_REQUEST() throws Exception {
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6 + "/1";
String expectedValue = "{\"pmax\":100, \"pmin\":10}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Attribute pmax can be used for only Resource/Object Instance/Object.";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
}
/**
* WriteAttributes {"id":"/3_1.2/0/6","attributes":{"pmax":100, "pmin":10}}
* if not implemented:
* {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"}
* if implemented:
* {"result":"BAD_REQUEST","error":"Attribute pmax can be used for only Resource/Object Instance/Object."}
*/
@Test
public void testWriteAttributeResourceDimWithParametersByResourceId_Result_BAD_REQUEST() throws Exception {
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6;
String expectedValue = "{\"dim\":3}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Attribute dim is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request.";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
}
@Test
public void testWriteAttributesResourceVerWithParametersById_Result_BAD_REQUEST() throws Exception {
String expectedPath = objectIdVer_3;
String expectedValue = "{\"ver\":1.3}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Attribute ver is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request.";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
}
@Test
public void testWriteAttributesResourceServerUriWithParametersById_Result_BAD_REQUEST() throws Exception {
String expectedPath = objectInstanceIdVer_1;
String actualResult = sendRPCReadById(expectedPath);
String expectedValue = "{\"uri\":\"coaps://localhost:5690\"}";
actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Attribute uri is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request.";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
}
/** /**
* <PROPERTIES> Class Attributes
* Object Version ver Object
* Provide the version of the associated Object.
* "ver" only for objectId
* <PROPERTIES> Class Attributes * <PROPERTIES> Class Attributes
* Dimension dim Integer [0:255] * Dimension dim Integer [0:255]
* Number of instances existing for a Multiple-Instance Resource * Number of instances existing for a Multiple-Instance Resource
@ -105,145 +59,116 @@ public class RpcLwm2mIntegrationDiscoverWriteAttributesTest extends AbstractRpcL
* <Type>Integer</Type> * <Type>Integer</Type>
* <RangeEnumeration>0..7</RangeEnumeration> * <RangeEnumeration>0..7</RangeEnumeration>
* WriteAttributes implemented: Discover {"id":"3/0/6"} -> 'dim' = 3 * WriteAttributes implemented: Discover {"id":"3/0/6"} -> 'dim' = 3
* "ver" only for objectId
*/
@Test
public void testReadDIM_3_0_6_Only_R () throws Exception {
String path = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6;
String actualResult = sendDiscover(path);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expected = "</3/0/6>;dim=3";
assertTrue(rpcActualResult.get("value").asText().equals(expected));
}
/**
* <PROPERTIES> Class Attributes
* Object Version ver Object
* Provide the version of the associated Object.
* "ver" only for objectId
*/ */
@Test @Test
public void testReadVer () throws Exception { public void testReadDIM_3_0_6_Only_R() throws Exception {
String path = objectIdVer_3; String path = objectIdVer_3;
String actualResult = sendDiscover(path); String actualResult = sendDiscover(path);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expected = "</3>;ver=1.2"; String expected = "</3>;ver=1.2";
assertTrue(rpcActualResult.get("value").asText().contains(expected)); assertTrue(rpcActualResult.get("value").asText().contains(expected));
expected = "</3/0/6>;dim=3";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
expected = "</3/0/7>;dim=3";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
expected = "</3/0/8>;dim=3";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
expected = "</3/0/11>;dim=1";
assertTrue(rpcActualResult.get("value").asText().contains(expected));
} }
/** /**
* WriteAttributes {"id":"/3/0/14","attributes":{"pmax":100, "pmin":10}} * WriteAttributes {"id":"/3/0/14","attributes":{"pmax":100, "pmin":10}}
* if not implemented:
* {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"}
* if implemented:
* {"result":"CHANGED"} * {"result":"CHANGED"}
* result changed:
*
*/ */
@Test @Test
public void testWriteAttributesResourceWithParametersById_Result_CHANGED() throws Exception { public void testWriteAttributesResourceWithParametersById_Result_CHANGED() throws Exception {
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6;
String expectedValue = "{\"pmax\":100, \"pmin\":10}"; String expectedValue = "{\"pmax\":100, \"pmin\":10}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
// result changed // result changed
actualResult = sendDiscover(expectedPath); actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expected = "</3/0/14>;pmax=100;pmin=10"; String expected = "</3/0/6>;pmax=100;pmin=10;dim=3";
assertTrue(rpcActualResult.get("value").asText().contains(expected)); assertTrue(rpcActualResult.get("value").asText().contains(expected));
} }
/**
* <NOTIFICATION> Class Attributes
* Minimum/Maximum Period pmin/pmax
* Notes: The Minimum Period Attribute:
* -- indicates the minimum time in seconds the LwM2M Client MUST wait between two notifications. If a notification of an observed Resource is supposed to be generated but it is before pmin expiry, notification MUST be sent as soon as pmin expires. In the absence of this parameter, the Minimum Period is defined by the Default Minimum Period set in the LwM2M Server Account.
* Notes: The Maximum Period Attribute:
* -- indicates the maximum time in seconds the LwM2M Client MAY wait between two notifications. When this "Maximum Period" expires after the last notification, a new notification MUST be sent. In the absence of this parameter, the "Maximum Period" is defined by the Default Maximum Period when set in the LwM2M Server Account or considered as 0 otherwise. The value of 0, means pmax MUST be ignored. The maximum period parameter MUST be greater than the minimum period parameter otherwise pmax will be ignored for the Resource to which such inconsistent timing conditions are applied.
* Greater Than gt Resource
* Less Than lt Resource
* Step st Resource
*
* Object Id = 1
* Default Minimum Period Id = 2 300 or 0
* Default Maximum Period Id = 3 6000 or "-"
* </3/0>;pmax=65, </3/0/1>, <3/0/2>, </3/0/3>, </3/0/4>,
* <3/0/6>;dim=8,<3/0/7>;gt=50;lt=42.2;st=0.5,<3/0/8>;...
*/
@Test @Test
public void testWriteAttributesPeriodLtGt () throws Exception { public void testWriteAttributesResourceVerWithParametersById_Result_BAD_REQUEST() throws Exception {
String expectedPath = objectIdVer_3;
String expectedValue = "{\"ver\":1.3}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText());
String expected = "Attribute ver is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request.";
String actual = rpcActualResult.get("error").asText();
assertTrue(actual.equals(expected));
}
@Test
public void testWriteAttributesObjectInstanceResourcePeriodLtGt_Return_CHANGED() throws Exception {
String expectedPath = objectInstanceIdVer_3; String expectedPath = objectInstanceIdVer_3;
String expectedValue = "{\"pmax\":60}"; String expectedValue = "{\"pmax\":65, \"pmin\":5}";
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
expectedPath = objectInstanceIdVer_3;
expectedValue = "{\"pmax\":65}";
actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_7; expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_7;
expectedValue ="{\"gt\":50, \"lt\":42.2, \"st\":0.5}"; String expectedValueStr = "gt=50;lt=42.2;st=0.5";
JsonUtils.parse("{" + expectedValueStr + "}").toString();
expectedValue = JsonUtils.parse("{" + expectedValueStr + "}").toString();
actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText());
// ObjectId // ObjectId
expectedPath = objectIdVer_3; expectedPath = objectIdVer_3;
actualResult = sendDiscover(expectedPath); actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
// String expected = "</3>;ver=1.2,</3/0>;pmax=60,</3/0/0>,</3/0/1>,</3/0/2>,</3/0/3>,</3/0/6>;dim=3,</3/0/7>;st=0.5;lt=42.2;gt=50.0,</3/0/8>,</3/0/9>,</3/0/10>,</3/0/11>;dim=1,</3/0/13>,</3/0/14>,</3/0/15>,</3/0/16>,</3/0/17>,</3/0/18>,</3/0/19>,</3/0/20>,</3/0/21>"; String actualValue = rpcActualResult.get("value").asText();
String expected = "</3>;ver=1.2,</3/0>;pmax=65"; String expected = "</3>;ver=1.2,</3/0>;pmax=65;pmin=5";
assertTrue(rpcActualResult.get("value").asText().contains(expected)); assertTrue(actualValue.contains(expected));
expected = "</3/0/6>;dim=3,</3/0/7>;st=0.5;lt=42.2;gt=50.0"; expected = "</3/0/6>;dim=3";
assertTrue(rpcActualResult.get("value").asText().contains(expected)); assertTrue(actualValue.contains(expected));
// ObjectInstanceId expected = "</3/0/7>;" + expectedValueStr + ";dim=3";
assertTrue(actualValue.contains(expected));
// ObjectInstanceId
expectedPath = objectInstanceIdVer_3; expectedPath = objectInstanceIdVer_3;
actualResult = sendDiscover(expectedPath); actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
expected = "</3/0>;pmax=65"; actualValue = rpcActualResult.get("value").asText();
assertTrue(rpcActualResult.get("value").asText().contains(expected)); expected = "</3/0>;pmax=65;pmin=5";
expected = "</3/0/6>;dim=3,</3/0/7>;st=0.5;lt=42.2;gt=50.0"; assertTrue(actualValue.contains(expected));
assertTrue(rpcActualResult.get("value").asText().contains(expected)); expected = "</3/0/7>;" + expectedValueStr + ";dim=3";
// ResourceId assertTrue(actualValue.contains(expected));
// ResourceId
expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6; expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6;
actualResult = sendDiscover(expectedPath); actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
expected = "</3/0/6>;dim=3"; expected = "</3/0/6>;dim=3,</3/0/6/0>,</3/0/6/1>,</3/0/6/2>";
assertTrue(rpcActualResult.get("value").asText().contains(expected)); assertTrue(rpcActualResult.get("value").asText().contains(expected));
expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_7; expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_7;
actualResult = sendDiscover(expectedPath); actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
expected = "</3/0/7>;st=0.5;lt=42.2;gt=50.0"; expected = "</3/0/7>;" + expectedValueStr;
assertTrue(rpcActualResult.get("value").asText().contains(expected)); assertTrue(rpcActualResult.get("value").asText().contains(expected));
// ResourceInstanceId
expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6+ "/1";
actualResult = sendDiscover(expectedPath);
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.INTERNAL_SERVER_ERROR.getName(), rpcActualResult.get("result").asText());
expected = "InvalidRequestException: Discover request cannot target resource instance path: /3/0/6/1";
assertTrue(rpcActualResult.get("error").asText().contains(expected));
} }
private String sendRPCExecuteWithValueById(String path, String value) throws Exception { private String sendRPCExecuteWithValueById(String path, String value) throws Exception {
String setRpcRequest = "{\"method\": \"WriteAttributes\", \"params\": {\"id\": \"" + path + "\", \"attributes\": " + value + " }}"; String setRpcRequest = "{\"method\": \"WriteAttributes\", \"params\": {\"id\": \"" + path + "\", \"attributes\": " + value + " }}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
}
private String sendRPCReadById(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk());
} }
private String sendDiscover(String path) throws Exception { private String sendDiscover(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}"; String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
} }
} }

4
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java

@ -174,12 +174,12 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT
private String sendRPCExecuteById(String path) throws Exception { private String sendRPCExecuteById(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Execute\", \"params\": {\"id\": \"" + path + "\"}}"; String setRpcRequest = "{\"method\": \"Execute\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
} }
private String sendRPCExecuteWithValueById(String path, Object value) throws Exception { private String sendRPCExecuteWithValueById(String path, Object value) throws Exception {
String setRpcRequest = "{\"method\": \"Execute\", \"params\": {\"id\": \"" + path + "\", \"value\": " + value + " }}"; String setRpcRequest = "{\"method\": \"Execute\", \"params\": {\"id\": \"" + path + "\", \"value\": " + value + " }}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
} }
} }

44
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java

@ -20,14 +20,12 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.LwM2m.Version; import org.eclipse.leshan.core.LwM2m.Version;
import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.response.ReadResponse;
import org.eclipse.leshan.server.registration.Registration; import org.eclipse.leshan.server.registration.Registration;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest;
import java.util.Optional;
import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -45,13 +43,15 @@ import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fr
@Slf4j @Slf4j
public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationObserveTest { public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationObserveTest {
@Before
public void setupObserveTest() throws Exception {
awaitObserveReadAll(4,lwM2MTestClient.getDeviceIdStr());
}
@Test @Test
public void testObserveReadAll_Count_4_CancelAll_Count_0_Ok() throws Exception { public void testObserveReadAll_Count_4_CancelAll_Count_0_Ok() throws Exception {
String actualValuesReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null); sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr());
assertEquals(4, actualValuesReadAll.split(",").length);
sendObserveCancelAllWithAwait(deviceId);
actualValuesReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
assertEquals("[]", actualValuesReadAll);
} }
/** /**
@ -61,7 +61,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
@Test @Test
public void testObserveOneResource_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { public void testObserveOneResource_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception {
long initSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); long initSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9);
sendObserveCancelAllWithAwait(deviceId); sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr());
sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9); sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9);
updateRegAtLeastOnceAfterAction(); updateRegAtLeastOnceAfterAction();
long lastSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); long lastSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9);
@ -74,7 +74,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
*/ */
@Test @Test
public void testObserveOneObjectInstance_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { public void testObserveOneObjectInstance_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception {
sendObserveCancelAllWithAwait(deviceId); sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr());
String idVer_3_0 = objectInstanceIdVer_3; String idVer_3_0 = objectInstanceIdVer_3;
sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0); sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0);
@ -89,7 +89,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
*/ */
@Test @Test
public void testObserveOneObject_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { public void testObserveOneObject_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception {
sendObserveCancelAllWithAwait(deviceId); sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr());
String idVer_3_0 = objectInstanceIdVer_3; String idVer_3_0 = objectInstanceIdVer_3;
sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0); sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0);
@ -199,7 +199,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
*/ */
@Test @Test
public void testObserves_OverlappedPaths_FirstResource_SecondObjectOrInstance() throws Exception { public void testObserves_OverlappedPaths_FirstResource_SecondObjectOrInstance() throws Exception {
sendObserveCancelAllWithAwait(deviceId); sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr());
// "19/0/0" // "19/0/0"
sendRpcObserveOkWithResultValue("Observe", idVer_19_0_0); sendRpcObserveOkWithResultValue("Observe", idVer_19_0_0);
// PreviousObservation "19/0/0" change to CurrentObservation "19" - object // PreviousObservation "19/0/0" change to CurrentObservation "19" - object
@ -247,7 +247,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
*/ */
@Test @Test
public void testObserveResource_ObserveCancelResource_Result_CONTENT_Count_1() throws Exception { public void testObserveResource_ObserveCancelResource_Result_CONTENT_Count_1() throws Exception {
sendObserveCancelAllWithAwait(deviceId); sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr());
String actualValuesReadAll = sendRpcObserveReadAllWithResult(idVer_3_0_9); String actualValuesReadAll = sendRpcObserveReadAllWithResult(idVer_3_0_9);
assertEquals(1, actualValuesReadAll.split(",").length); assertEquals(1, actualValuesReadAll.split(",").length);
@ -265,7 +265,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
*/ */
@Test @Test
public void testObserveObject_ObserveCancelOneResource_Result_INTERNAL_SERVER_ERROR_Than_Cancel_ObserveObject_Result_CONTENT_Count_1() throws Exception { public void testObserveObject_ObserveCancelOneResource_Result_INTERNAL_SERVER_ERROR_Than_Cancel_ObserveObject_Result_CONTENT_Count_1() throws Exception {
sendObserveCancelAllWithAwait(deviceId); sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr());
String actualValuesReadAll = sendRpcObserveReadAllWithResult(objectIdVer_3); String actualValuesReadAll = sendRpcObserveReadAllWithResult(objectIdVer_3);
assertEquals(1, actualValuesReadAll.split(",").length); assertEquals(1, actualValuesReadAll.split(",").length);
@ -292,7 +292,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
*/ */
@Test @Test
public void testObserveResource_ObserveCancelObject_Result_CONTENT_Count_1() throws Exception { public void testObserveResource_ObserveCancelObject_Result_CONTENT_Count_1() throws Exception {
sendObserveCancelAllWithAwait(deviceId); sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr());
sendRpcObserveWithWithTwoResource(idVer_3_0_0, idVer_3_0_9); sendRpcObserveWithWithTwoResource(idVer_3_0_0, idVer_3_0_9);
String rpcActualResul = sendRpcObserveOkWithResultValue("ObserveReadAll", null); String rpcActualResul = sendRpcObserveOkWithResultValue("ObserveReadAll", null);
assertEquals(2, rpcActualResul.split(",").length); assertEquals(2, rpcActualResul.split(",").length);
@ -320,15 +320,13 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
*/ */
@Test @Test
public void testObserveResource_Update_AfterUpdateRegistration() throws Exception { public void testObserveResource_Update_AfterUpdateRegistration() throws Exception {
sendObserveCancelAllWithAwait(deviceId); sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr());
int cntUpdate = 3; awaitUpdateReg(3);
verify(defaultUplinkMsgHandlerTest, timeout(50000).atLeast(cntUpdate))
.updatedReg(Mockito.any(Registration.class));
sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9); sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9);
cntUpdate = 10; int cntUpdate = 10;
verify(defaultUplinkMsgHandlerTest, timeout(50000).atLeast(cntUpdate)) verify(defaultUplinkMsgHandlerTest, timeout(50000).atLeast(cntUpdate))
.updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null)); .updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null));
} }
@ -344,11 +342,5 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
return rpcActualResult.get("value").asText(); return rpcActualResult.get("value").asText();
} }
private void sendRpcObserveWithContainsLwM2mSingleResource(String params) throws Exception {
String rpcActualResult = sendRpcObserveOkWithResultValue("Observe", params);
assertTrue(rpcActualResult.contains("LwM2mSingleResource"));
assertEquals(Optional.of(1).get(), Optional.ofNullable(getCntObserveAll(deviceId)).get());
}
} }

98
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadCollectedValueTest.java

@ -0,0 +1,98 @@
/**
* 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.
*/
package org.thingsboard.server.transport.lwm2m.rpc.sql;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_VALUE_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_VALUE_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3303_12_5700;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS;
@Slf4j
public class RpcLwm2mIntegrationReadCollectedValueTest extends AbstractRpcLwM2MIntegrationTest {
/**
* Read {"id":"/3303/12/5700"}
* Trigger a Send operation from the client with multiple values for the same resource as a payload
* acked "[{"bn":"/3303/12/5700","bt":1724".. 116 bytes]
* 2 values for the resource /3303/12/5700 should be stored with:
* - timestamps1 = Instance.now() + RESOURCE_ID_VALUE_3303_12_5700_1
* - timestamps2 = (timestamps1 + 3 sec) + RESOURCE_ID_VALUE_3303_12_5700_2
* @throws Exception
*/
@Test
public void testReadSingleResource_sendFromClient_CollectedValue() throws Exception {
// init test
int cntValues = 2;
int resourceId = 5700;
String expectedIdVer = objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12 + "/" + resourceId;
sendRPCById(expectedIdVer);
// verify time start/end send CollectedValue;
await().atMost(40, SECONDS).until(() -> RESOURCE_ID_3303_12_5700_TS_0 > 0
&& RESOURCE_ID_3303_12_5700_TS_1 > 0);
// verify result read: verify count value: 1-2: send CollectedValue;
AtomicReference<ObjectNode> actualValues = new AtomicReference<>();
await().atMost(40, SECONDS).until(() -> {
actualValues.set(doGetAsync(
"/api/plugins/telemetry/DEVICE/" + lwM2MTestClient.getDeviceIdStr() + "/values/timeseries?keys="
+ RESOURCE_ID_NAME_3303_12_5700
+ "&startTs=" + (RESOURCE_ID_3303_12_5700_TS_0 - RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS)
+ "&endTs=" + (RESOURCE_ID_3303_12_5700_TS_1 + RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS)
+ "&interval=0&limit=100&useStrictDataTypes=false",
ObjectNode.class));
return actualValues.get() != null && actualValues.get().size() > 0
&& actualValues.get().get(RESOURCE_ID_NAME_3303_12_5700).size() >= cntValues && verifyTs(actualValues);
});
}
private boolean verifyTs(AtomicReference<ObjectNode> actualValues) {
String expectedVal_0 = String.valueOf(RESOURCE_ID_3303_12_5700_VALUE_0);
String expectedVal_1 = String.valueOf(RESOURCE_ID_3303_12_5700_VALUE_1);
ArrayNode actual = (ArrayNode) actualValues.get().get(RESOURCE_ID_NAME_3303_12_5700);
long actualTS0 = 0;
long actualTS1 = 0;
for (JsonNode tsNode : actual) {
if (tsNode.get("value").asText().equals(expectedVal_0)) {
actualTS0 = tsNode.get("ts").asLong();
} else if (tsNode.get("value").asText().equals(expectedVal_1)) {
actualTS1 = tsNode.get("ts").asLong();
}
}
return actualTS0 >= RESOURCE_ID_3303_12_5700_TS_0
&& actualTS1 <= RESOURCE_ID_3303_12_5700_TS_1
&& (actualTS1 - actualTS0) >= RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS;
}
private String sendRPCById(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
}
}

78
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java

@ -15,23 +15,15 @@
*/ */
package org.thingsboard.server.transport.lwm2m.rpc.sql; package org.thingsboard.server.transport.lwm2m.rpc.sql;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.map.HashedMap;
import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mPath;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.eclipse.leshan.core.LwM2mId.SERVER;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -39,24 +31,17 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_1; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_11; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_11;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_3; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_3;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_1_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_1_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3303_12_5700;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_VALUE_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_VALUE_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS;
@Slf4j @Slf4j
public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest { public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest {
@ -228,59 +213,6 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
assertTrue(actualValues.contains(expected19_1_0)); assertTrue(actualValues.contains(expected19_1_0));
} }
/**
* Read {"id":"/3303/12/5700"}
* Trigger a Send operation from the client with multiple values for the same resource as a payload
* acked "[{"bn":"/3303/12/5700","bt":1724".. 116 bytes]
* 2 values for the resource /3303/12/5700 should be stored with:
* - timestamps1 = Instance.now() + RESOURCE_ID_VALUE_3303_12_5700_1
* - timestamps2 = (timestamps1 + 3 sec) + RESOURCE_ID_VALUE_3303_12_5700_2
* @throws Exception
*/
@Test
public void testReadSingleResource_sendFromClient_CollectedValue() throws Exception {
// init test
long startTs = Instant.now().toEpochMilli();
int cntValues = 4;
int resourceId = 5700;
String expectedIdVer = objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12 + "/" + resourceId;
sendRPCById(expectedIdVer);
// verify result read: verify count value: 1-2: send CollectedValue; 3 - response for read;
long endTs = Instant.now().toEpochMilli() + RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS * 4;
String expectedVal_1 = String.valueOf(RESOURCE_ID_3303_12_5700_VALUE_0);
String expectedVal_2 = String.valueOf(RESOURCE_ID_3303_12_5700_VALUE_1);
AtomicReference<ObjectNode> actualValues = new AtomicReference<>();
await().atMost(40, SECONDS).until(() -> {
actualValues.set(doGetAsync(
"/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?keys="
+ RESOURCE_ID_NAME_3303_12_5700
+ "&startTs=" + startTs
+ "&endTs=" + endTs
+ "&interval=0&limit=100&useStrictDataTypes=false",
ObjectNode.class));
// verify cntValues
return actualValues.get() != null && actualValues.get().get(RESOURCE_ID_NAME_3303_12_5700).size() == cntValues;
});
// verify ts
ArrayNode actual = (ArrayNode) actualValues.get().get(RESOURCE_ID_NAME_3303_12_5700);
Map<String, Long> keyTsMaps = new HashedMap();
for (JsonNode tsNode: actual) {
if (tsNode.get("value").asText().equals(expectedVal_1) || tsNode.get("value").asText().equals(expectedVal_2)) {
keyTsMaps.put(tsNode.get("value").asText(), tsNode.get("ts").asLong());
}
}
assertTrue(keyTsMaps.size() == 2);
long actualTS0 = keyTsMaps.get(expectedVal_1).longValue();
long actualTS1 = keyTsMaps.get(expectedVal_2).longValue();
assertTrue(actualTS0 > 0);
assertTrue(actualTS1 > 0);
assertTrue(actualTS1 > actualTS0);
assertTrue((actualTS1 - actualTS0) >= RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS);
assertTrue(actualTS0 <= RESOURCE_ID_3303_12_5700_TS_0);
assertTrue(actualTS1 <= RESOURCE_ID_3303_12_5700_TS_1);
}
/** /**
* ReadComposite {"keys":["batteryLevel", "UtfOffset", "dataDescription"]} * ReadComposite {"keys":["batteryLevel", "UtfOffset", "dataDescription"]}
*/ */
@ -300,21 +232,21 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
private String sendRPCById(String path) throws Exception { private String sendRPCById(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}"; String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
} }
private String sendRPCByKey(String key) throws Exception { private String sendRPCByKey(String key) throws Exception {
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"key\": \"" + key + "\"}}"; String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"key\": \"" + key + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
} }
private String sendCompositeRPCByIds(String paths) throws Exception { private String sendCompositeRPCByIds(String paths) throws Exception {
String setRpcRequest = "{\"method\": \"ReadComposite\", \"params\": {\"ids\":" + paths + "}}"; String setRpcRequest = "{\"method\": \"ReadComposite\", \"params\": {\"ids\":" + paths + "}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
} }
private String sendCompositeRPCByKeys(String keys) throws Exception { private String sendCompositeRPCByKeys(String keys) throws Exception {
String setRpcRequest = "{\"method\": \"ReadComposite\", \"params\": {\"keys\":" + keys + "}}"; String setRpcRequest = "{\"method\": \"ReadComposite\", \"params\": {\"keys\":" + keys + "}}";
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
} }
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save